<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>cn_boris (Boris Ding)</title>
    <link>https://ruby-china.org/cn_boris</link>
    <description>An Entropy Slayer's Thoughts on Design, Code and the Poetic Life.</description>
    <language>en-us</language>
    <item>
      <title>[远程或宁波] 30-60K Brickdoc 招聘软件开发工程师</title>
      <description>&lt;h2 id="我们是谁"&gt;我们是谁&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;人类一直寻找能够用视觉符号方式表达思想感情的方法，寻找能够利用图形储存自己的记忆和知识的方法，寻找能够把信息的传达程序化和简单化的方法。文字的产生、印刷的发展、相机的发明、互联网的普及，都代表了这种努力。思维与技术的交替成长，直到今天依然代表了这种探索的继续。
──《世界现代设计史》王受之&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Brickdoc 是一个以 Open-core 模式商业化开源的新一代协同文档系统（目前正在开发中，计划于今年年中正式以 Apache-2.0 协议开源）。通过整合在线文档，RPA 和云事件总线功能，其模糊了文档与软件之间的边界，从而使得用户无需编写任何代码即可拥有将一部分工作自动化的能力。
以 Word 和 Excel 为代表的通用办公软件已经成为数字世界不可或缺的重要支柱，但事实上诞生于 40 多年前（以 VisiCalc 的发布日期记）的它们是建立在对于纸质文件的拟物化隐喻之上的。在那个时代，以数字化方式存储信息仅仅是为了加速处理信息所产生的「临时文件」，信息最初的来源和最终的去向依然是被构建于纸张之上的。&lt;/p&gt;

&lt;p&gt;在 21 世纪 20 年代的当下，信息时代的「童年」已经终结。信息爆炸意味着以数字化方式存储的信息已经成为了世界运作所不可或缺的一部分──而不仅仅是过程产物。正如当下的 UI 风格已经完成了从拟物化到扁平化的转变一样，我们迫切需要一款面向下一个十年的通用办公软件来增强人类获取，处理以及分发信息的能力。并最终实现 Douglas C. Engelbart 所畅想的藉由人机协同来提高人类智能（augmenting human intellect）美好愿景。&lt;/p&gt;
&lt;h2 id="我们怎么做事"&gt;我们怎么做事&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;类似 Basecamp（原 37signals）的精英化小团队&lt;/li&gt;
&lt;li&gt;从开源中来，到开源中去──我们是 CNCF 和 W3C 的成员单位&lt;/li&gt;
&lt;li&gt;通过商业化实现可持续的开源──稳健营收 而非 用爱发电&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们的技术栈"&gt;我们的技术栈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ruby on Rails 单体应用&lt;/li&gt;
&lt;li&gt;一小部分使用 Rust 和 C 编写 Ruby Gem&lt;/li&gt;
&lt;li&gt;基于 React 开发的 PWA（重度使用 RxJS）&lt;/li&gt;
&lt;li&gt;GraphQL API&lt;/li&gt;
&lt;li&gt;基于 Single SPA 和 serverless 的插件化扩展体系&lt;/li&gt;
&lt;li&gt;Kubernetes &amp;amp; Helm&lt;/li&gt;
&lt;li&gt;基于 WebAssembly 实现的 mruby 前端解释器&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们在找怎样的人"&gt;我们在找怎样的人&lt;/h2&gt;&lt;h3 id="软件开发工程师（前端或全栈）"&gt;软件开发工程师（前端或全栈）&lt;/h3&gt;
&lt;p&gt;你将负责下述的一项或多项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;参与 WYSIWYG editor 和 blockly-like visual programming editor 的开发和持续优化；&lt;/li&gt;
&lt;li&gt;参与 React PWA 和 GraphQL API 的业务开发；&lt;/li&gt;
&lt;li&gt;参与开源社区的治理工作──处理各类 Issue 和 PR, 解答社区疑问；&lt;/li&gt;
&lt;li&gt;代表公司参与 W3C 关于未来 Web 标准的技术研讨；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们期望你：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;掌握或愿意学习右述技术/算法：React(or Angular), TypeScript, Apollo GraphQL, RxJS, Module Federation, ProseMirror, Service Worker, Custom elements, WebAssembly, Conflict-free Replicated Data Types, Finite-state machine&lt;/li&gt;
&lt;li&gt;拥有基本的服务器端编程经验（NodeJS 或其他语言均可），基本掌握至少一种关系型或非关系型数据库的使用;&lt;/li&gt;
&lt;li&gt;拥有基本的算法和数据结构常识；&lt;/li&gt;
&lt;li&gt;具备强烈的技术进取心和好奇心，热衷于关注相关技术前沿发展趋势，有良好的沟通与合作精神，拥有优秀的问题分析及解决能力；&lt;/li&gt;
&lt;li&gt;能够无障碍读写英文技术文档（不要求听说）;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;有较强的自驱力和自控力，能够适应远程工作&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;认同「Develop a team as a product」。参与前端工程化建设以及各类内部工程效率工具的开发，致力于持续不断地通过自动化手段和方法论提升团队及自身的工作效率；&lt;/li&gt;
&lt;li&gt;具有高度的 Ownership 参与产品设计和需求梳理，愿意从自身的专业角度出发与团队的其他成员一起探索持续提升用户体验的可能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="软件开发工程师（后端或全栈）"&gt;软件开发工程师（后端或全栈）&lt;/h3&gt;
&lt;p&gt;你将负责下述的一项或多项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;参与 WebAssembly 的相关开发工作；&lt;/li&gt;
&lt;li&gt;参与 GraphQL API, serverless functions 和各类后端业务逻辑的开发，交付高质量的代码并持续进行性能优化；&lt;/li&gt;
&lt;li&gt;践行 DevOps 思想，负责相关服务在 Kubernetes 集群中的部署和维护，确保生产环境的 SLA；&lt;/li&gt;
&lt;li&gt;参与开源社区的治理工作──处理各类 Issue 和 PR, 解答社区疑问；&lt;/li&gt;
&lt;li&gt;代表公司参与到 CNCF 的社区工作，特别是 cloudevents spec 相关的工作；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们期望你：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;拥有良好的算法和数据结构功底&lt;/strong&gt;，熟悉常用的设计模式；&lt;/li&gt;
&lt;li&gt;熟悉 Ruby 语言或有兴趣以 Ruby 语言作为主要工作语言;&lt;/li&gt;
&lt;li&gt;掌握或愿意学习右述技术：Ruby on Rails, PostgreSQL, Redis, RabbitMQ(or Kafka/ NATS / AWS SQS), Kubernetes, Knative；&lt;/li&gt;
&lt;li&gt;基本掌握右述语言中的一种或多种：Java/Kotlin, Go, C/C++, C#, Rust, Haskell/Lisp/Clojure, TypeScript；&lt;/li&gt;
&lt;li&gt;基本掌握 HTML, CSS 和 ECMAScript;&lt;/li&gt;
&lt;li&gt;了解 Web 应用常见漏洞，了解静态漏洞扫描工具，安全意识高&lt;/li&gt;
&lt;li&gt;有编写自动化测试习惯，熟悉 CI/CD 流程，编码习惯良好；&lt;/li&gt;
&lt;li&gt;具备强烈的技术进取心和好奇心，热衷于关注相关技术前沿发展趋势，有良好的沟通与合作精神，拥有优秀的问题分析及解决能力；&lt;/li&gt;
&lt;li&gt;能够无障碍读写英文技术文档（不要求听说）;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;有较强的自驱力和自控力，能够适应远程工作&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;认同「Develop a team as a product」。参与各类内部工程效率工具的开发，致力于持续不断地通过自动化手段和方法论提升团队及自身的工作效率；&lt;/li&gt;
&lt;li&gt;具有高度的 Ownership 参与产品设计和需求梳理，愿意从自身的专业角度出发与团队的其他成员一起探索持续提升用户体验的可能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="如何联系我们"&gt;如何联系我们&lt;/h2&gt;
&lt;p&gt;请投递&lt;strong&gt;PDF 格式&lt;/strong&gt;的简历至 careers@brickdoc.com，简历中随附 Github ID 有加分。&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Sun, 11 Apr 2021 12:44:19 +0800</pubDate>
      <link>https://ruby-china.org/topics/41138</link>
      <guid>https://ruby-china.org/topics/41138</guid>
    </item>
    <item>
      <title>Shopify 的 GraphQL API 设计指南</title>
      <description>&lt;p&gt;最近在研究 GraphQL 相关的最佳实现，然后看到了 &lt;a href="https://github.com/Shopify/graphql-design-tutorial" rel="nofollow" target="_blank" title=""&gt;Shopify 的 GraphQL API 设计指南&lt;/a&gt; 感觉收益颇丰。顺手翻译成了中文版分享给大家。&lt;/p&gt;
&lt;h2 id="教程: GraphQL API 的准则"&gt;教程：GraphQL API 的准则&lt;/h2&gt;
&lt;p&gt;本教程最初由 &lt;a href="https://www.shopify.ca/" rel="nofollow" target="_blank" title=""&gt;Shopify&lt;/a&gt; 基于内部需求而撰写，但考虑到它对于任何
准备编写 GraphQL API 的人有帮助因此我们创建了这一对外的公开版本。&lt;/p&gt;

&lt;p&gt;它参考了最近 3 年来 Shopify 在生产环境中开发和迭代 GraphQL API 所获得的经验和教训，并将持续
更新。&lt;/p&gt;

&lt;p&gt;我们相信这些设计准则在绝大多数情况下都是适用的。但它们可能未必完全适用于你的需求，即使在 Shopify 内部
这些规则也未必 100% 的适用于所有场景。因此请不要盲目的照搬和实施下述的所有设计准则。&lt;/p&gt;
&lt;h2 id="目录"&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E7%AE%80%E4%BB%8B" title=""&gt;简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%AD%A5%E9%AA%A40-%E5%81%87%E5%AE%9A%E8%83%8C%E6%99%AF" title=""&gt;步骤 0: 假定背景&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%AD%A5%E9%AA%A41:-%E5%85%A8%E5%B1%80%E6%A6%82%E8%A7%88" title=""&gt;步骤 1: 全局概览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="#%E6%AD%A5%E9%AA%A42-%E5%B0%91%E6%97%A2%E6%98%AF%E5%A4%9A" title=""&gt;步骤 2: 少既是多&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E9%81%BF%E5%85%8D%E6%9A%B4%E9%9C%B2-collectionmembership-%E8%A1%A8" title=""&gt;避免暴露 &lt;code&gt;CollectionMembership&lt;/code&gt; 表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E9%87%8D%E6%96%B0%E6%80%9D%E8%80%83%E5%90%88%E9%9B%86" title=""&gt;重新思考合集&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%BB%93%E8%AE%BA" title=""&gt;结论&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="#%E6%AD%A5%E9%AA%A43-%E5%A2%9E%E5%8A%A0%E7%BB%86%E8%8A%82" title=""&gt;步骤 3: 增加细节&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E6%9C%80%E5%88%9D%E5%AE%9E%E7%8E%B0" title=""&gt;最初实现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#id-%E5%92%8C-node-%E6%8E%A5%E5%8F%A3" title=""&gt;ID 和 &lt;code&gt;Node&lt;/code&gt; 接口&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#rules-%E5%AD%97%E6%AE%B5%E5%92%8C%E5%AD%90%E5%AF%B9%E8%B1%A1" title=""&gt;&lt;code&gt;Rules&lt;/code&gt; 字段和子对象&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%88%97%E8%A1%A8%E5%92%8C%E5%88%86%E9%A1%B5" title=""&gt;列表和分页&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%AD%97%E7%AC%A6%E4%B8%B2" title=""&gt;字符串&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%85%B3%E8%81%94%E5%AF%B9%E8%B1%A1%E7%9A%84-id" title=""&gt;关联对象的 ID&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%91%BD%E5%90%8D%E4%B8%8E%E6%A0%87%E9%87%8Fscalar" title=""&gt;命名与标量（Scalar）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%88%86%E9%A1%B5%E7%9A%84%E5%86%8D%E6%80%9D%E8%80%83" title=""&gt;分页的再思考&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%9E%9A%E4%B8%BEenum" title=""&gt;枚举（Enum）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%AD%A5%E9%AA%A44-%E5%95%86%E4%B8%9A%E9%80%BB%E8%BE%91" title=""&gt;步骤 4: 商业逻辑&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="#%E6%AD%A5%E9%AA%A45-%E5%8F%98%E6%9B%B4mutation" title=""&gt;步骤 5: 变更（Mutation）&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E6%A0%B9%E6%8D%AE%E5%85%B7%E4%BD%93%E4%B8%9A%E5%8A%A1%E6%9D%A5%E5%88%92%E5%88%86%E6%94%AF%E6%8C%81%E7%9A%84%E6%93%8D%E4%BD%9C%E7%A7%8D%E7%B1%BB" title=""&gt;根据具体业务来划分支持的操作种类&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%80%9D%E8%80%83%E5%AF%B9%E8%B1%A1%E4%B8%8E%E5%AF%B9%E8%B1%A1%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB" title=""&gt;思考对象与对象间的关系&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#input-%E7%BB%93%E6%9E%84---%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86" title=""&gt;Input: 结构 - 第一部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#input-%E6%A0%87%E9%87%8F" title=""&gt;Input: 标量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#input-%E7%BB%93%E6%9E%84---%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86" title=""&gt;Input: 结构 - 第二部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#mutation-%E7%9A%84%E8%BF%94%E5%9B%9E%E5%80%BC" title=""&gt;Mutation 的返回值&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#tldr-%E8%AE%BE%E8%AE%A1%E5%87%86%E5%88%99%E6%80%BB%E7%BB%93" title=""&gt;TLDR: 设计准则总结&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%BB%93%E5%B0%BE" title=""&gt;结尾&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="简介"&gt;简介&lt;/h2&gt;
&lt;p&gt;欢迎！本文档将指导您如何设计新的 GraphQL API。API 设计是一项极具挑战性的任务，它需要你对于
业务场景拥有深刻地理解并且不断的进行迭代和实验。&lt;/p&gt;
&lt;h2 id="步骤0: 假定背景"&gt;步骤 0: 假定背景&lt;/h2&gt;
&lt;p&gt;为了本教程之目的，你需要想象自己正为一家电商公司工作。目前这一电商平台已经拥有 1 个可以查询商品信息的 GraphQL API。在最近的产品迭代中，你的团队刚刚完成了「商品合集」功能的后端开发，你被指派负责开发该功能的 GraphQL 接口。&lt;/p&gt;

&lt;p&gt;商品合集本质上是一个类似收藏夹的功能，允许商家对于商品进行分组——从而实现例如在某个专题促销页中仅展示某个特定合集中的商品或「某个特定合集中的商品打 85 折」之类的程序化任务。&lt;/p&gt;

&lt;p&gt;你的同事已经完成了这一需求的业务逻辑设计，具体如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;所有的合集都包含诸如：标题、详情描述（可能包括 HTML 片段）、缩略图 等基础字段。&lt;/li&gt;
&lt;li&gt;合集有 2 种类型，分别是 需要人工添加商品的手动合集（ManualCollections）和 能够按照规则生成的自动合集（AutomaticCollections）——例如可以创建 1 个自动合集，该合集会将店铺内的存在 XL 码库存的所有男装都加入进去。&lt;/li&gt;
&lt;li&gt;商品和合集之间是多对多（many-to-many）关系，在数据库层面存在一个叫做 &lt;code&gt;CollectionMembership&lt;/code&gt;的中间关联表。&lt;/li&gt;
&lt;li&gt;就像商品可以设置上下架一样，合集也有一个字段可以设置其是否生效。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们将基于上述的假定需求来进行 API 设计。&lt;/p&gt;
&lt;h2 id="步骤1: 全局概览"&gt;步骤 1: 全局概览&lt;/h2&gt;
&lt;p&gt;不考虑架构的话，这一功能最为简单粗暴的 GraphQL Schema 设计可能是这样：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bodyHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AutomaticCollection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AutomaticCollectionRule&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rulesApplyDisjunctively&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bodyHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ManualCollection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bodyHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AutomaticCollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即便只有 4 个对象和 1 个接口，但这样设计依然看起来有点过于复杂。而且，它似乎并没有完全地满足业务需求，
例如我们需要使用这样的 API 来完成手机 App 上的合集功能似乎就不太够用了。&lt;/p&gt;

&lt;p&gt;基于我们的经验，用一个由多个对象、数十个字段揉杂在一起所构成的 Graphql API 来实现某一业务需求往往是混乱和错误的开端。你应该从更高的抽象层级进行思考，着眼于类型（GraphQL Type）及各个类型之间的关系，而非数据库层面的具体字段或对 CRUD 接口进行简单罗列。从&lt;a href="https://zh.wikipedia.org/zh-hans/ER%E6%A8%A1%E5%9E%8B" rel="nofollow" target="_blank" title=""&gt;实体关系模型（Entity-relationship model）&lt;/a&gt;开始思考会是一个好的选择。&lt;/p&gt;

&lt;p&gt;在开始正式重构前，我们先把当前设计中所有不需要关注的细节隐藏起来以利于我们接下来的思考：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AutomaticCollection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AutomaticCollectionRule&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ManualCollection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AutomaticCollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionMembership&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;规则一：永远先从更高的抽象层级进行设计，先考虑类型与类型之间的关系，再去考虑具体的字段。&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="步骤2: 少既是多"&gt;步骤 2: 少既是多&lt;/h2&gt;
&lt;p&gt;接下来，让我们着力于解决简单粗暴版本中的核心问题。&lt;/p&gt;

&lt;p&gt;如前所述，在数据库中本需求由通过 自动合集表、手动合集表以及用于实现商品与合集间多对多关系的中间表 这 3 张表来实现。我们现有的 GraphQL API 设计完全照搬了这一关系模型，但这实际上是错误的。&lt;/p&gt;

&lt;p&gt;这一错误的核心问题在于，API 设计和数据库设计有着不同的抽象层级和使用目的。在 API 设计中不加思考的照搬数据库表结构，往往会把我们引入歧途。&lt;/p&gt;
&lt;h3 id="避免暴露 CollectionMembership 表"&gt;避免暴露 &lt;code&gt;CollectionMembership&lt;/code&gt; 表&lt;/h3&gt;
&lt;p&gt;你可能已经注意到 &lt;code&gt;CollectionMembership&lt;/code&gt; 表实质上是一种技术细节，对于业务场景而言它实质上应该是一种黑盒实现。&lt;/p&gt;

&lt;p&gt;也就是说我们不应该在 API 设计中将它暴露出来，因为我们的 API 是对于业务模型而非技术细节的抽象。因此我们可以进一步将 Schema 重构成下述版本：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AutomaticCollection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AutomaticCollectionRule&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ManualCollection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AutomaticCollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much better.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则二：永远不要在 API 中暴露不必要的实现细节。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="重新思考合集"&gt;重新思考合集&lt;/h3&gt;
&lt;p&gt;现有的 API Schema 设计依然着一个因为我们没有深入理解业务知识而产生的缺陷。我们把自动合集和手动合集设计成两种不同的 GraphQL 类型，他们都继承于同一个公共的集合接口。从技术上说，这一的设计似乎是符合逻辑的——他们有许多相同的字段，但本质上它们的功能由存在着明显差异。&lt;/p&gt;

&lt;p&gt;但从业务场景角度思考，这种差异实现是也只是一种实现细节上的差异。它们在业务场景中所实现的功能是完全一致的——将若干个商品聚合在一起。而且未来，可能会有新的需求导致第三种甚至第四种合集类型的出现，例如可能会新增一种主要由规则生成同时允许手工加入商品的合集类型。但无论需求怎么变更，有一点是不会变的——它们从逻辑上永远是一种用于将若干个商品聚合在一起的功能。因此或许我们可以进一步的这样重构：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就看起来简洁多了，也许你会担心对于手动合集而言，聚合规则（CollectionRule）字段是不存在的。但实际上只要返回一个空数组，那么这一设计完全是符合逻辑和满足需求的。&lt;/p&gt;
&lt;h3 id="结论"&gt;结论&lt;/h3&gt;
&lt;p&gt;在较高的抽象层级下进行 API 设计，要求你必须对于你所建模的业务场景有着非常深刻的认知。不要急于进行细节实现，花费足够时间理解业务场景及其上下文对于你而言至关重要。&lt;/p&gt;

&lt;p&gt;与此同时，一个好的 API 设计也不应该是对于某个 UI 设计稿的建模——你不应该在设计 API 时只考虑设计稿中要求体现哪些字段。即便 UI 设计稿和数据库表结构对于 API 设计而言非常有参考价值，但你务必记得你的核心关注点应该是更为抽象的业务场景。&lt;/p&gt;

&lt;p&gt;更重要的是，务必不要照搬现有的 REST API 设计（如果有的话）。REST 和 GraphQL 背后是不同的思考逻辑，你在 REST API 领域的设计经验对于 GraphQL API 而言未必是能够适用的。 &lt;/p&gt;

&lt;p&gt;既往不恋，纵情向前，当下不杂，未来不迎。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则三：围绕着业务背景重新思考你的 GraphQL API，切忌直接照搬数据库表结构、视觉稿或已有的 REST API。&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="步骤3: 增加细节"&gt;步骤 3: 增加细节&lt;/h2&gt;
&lt;p&gt;现在我们已经有了一个大体合适的抽象模型，我们可以逐步开始考虑细节——把隐去的具体字段加回来。&lt;/p&gt;

&lt;p&gt;在我们开始增加每一个字段时，我们都需要仔细思考这个字段是否真的有存在的必要。我们的 GraphQL 类型中存在某个字段是因为我们的业务场景需要用到，而不应该是因为这个字段在数据库中存在或在过去的 REST API 中存在。&lt;/p&gt;

&lt;p&gt;在 GraphQL 中暴露一个字段、参数或类型，非常简单。但一旦发布上线后，你想要将之去除和改名将变得异常苦难，GraphQL 的灵活意味着你很难预测哪些地方会用到它们。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则四：永远记得在 GraphQL 中去掉一个字段要比新增一个字段困难的多。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="最初实现"&gt;最初实现&lt;/h3&gt;
&lt;p&gt;如果不逐一考虑每个字段存在的必要性而是先全部加回来的话，那么当前的 GraphQL Schema 大概是这样的：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rulesApplyDisjunctively&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bodyHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们来从上到下具体思考每一个字段是否有其存在的必要。&lt;/p&gt;
&lt;h3 id="ID 和 Node 接口"&gt;ID 和 &lt;code&gt;Node&lt;/code&gt; 接口&lt;/h3&gt;
&lt;p&gt;在我们的合集类型中，第一个字段是 ID 字段。非常合理，而且确实是必须的——我们在做增删该查时都会用的到这一字段。
不过，在 GraphQL API 中往往会存在一个&lt;code&gt;Node&lt;/code&gt; 接口，它的具体结构如下：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 GraphQL 中，它用来告示客户端基于其实现的对象是可以基于唯一 ID 进行持久化和搜索的，这有助与客户端更高效的实现本地缓存和其他功能。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;译者注：&lt;code&gt;Node&lt;/code&gt; 接口实际上 Relay 规范的一部分，可以在 &lt;a href="https://relay.dev/docs/en/graphql-server-specification.html" rel="nofollow" target="_blank" title=""&gt;GraphQL Sever Specification&lt;/a&gt; 中了解具体信息。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;因此我们的业务对象应该继承于 &lt;code&gt;Node&lt;/code&gt;接口：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;规则五：绝大多数业务对象都应该集成自 &lt;code&gt;Node&lt;/code&gt; 接口。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="Rules 字段和子对象"&gt;
&lt;code&gt;Rules&lt;/code&gt; 字段和子对象&lt;/h3&gt;
&lt;p&gt;接下来我们开始审视 &lt;code&gt;rules&lt;/code&gt; 和 &lt;code&gt;rulesApplyDisjunctively&lt;/code&gt; 这两个字段。&lt;/p&gt;

&lt;p&gt;第一个字段 &lt;code&gt;rules&lt;/code&gt; 非常直观，返回一个包含自动匹配规则的列表。不过请注意，这一字段并标记为了非空字段——这是一个不错的设计。在 GraphQL 中 &lt;code&gt;null&lt;/code&gt;、&lt;code&gt;[]&lt;/code&gt; 和 &lt;code&gt;[null]&lt;/code&gt;是不同的，因此这一设计有助于我们确保当合集的类型是手动合集时 &lt;code&gt;rules&lt;/code&gt; 的值可以为 &lt;code&gt;[]&lt;/code&gt;，但不能为 &lt;code&gt;null&lt;/code&gt; 或者 &lt;code&gt;[null]&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;小贴士：一个空数组和&lt;code&gt;null&lt;/code&gt;在逻辑上有着不同的语义。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;第二个字段 &lt;code&gt;rulesApplyDisjunctively&lt;/code&gt; 返回的是布尔值——用来表示&lt;code&gt;rules&lt;/code&gt;数组中的多条规则之间是取交集还是并集（or 还是 and）。但对于不适用于规则的手动合集而言，这一字段的值应该是什么似乎是个问题。&lt;/p&gt;

&lt;p&gt;对此而言，引入一种的 GraphQL 类型——&lt;code&gt;CollectionRuleSet&lt;/code&gt;可能是个好的选择。当你在同一 GraphQL 类型中有一组值和行为密切相关的字段时，将他们单独抽出来作为一个新的类型往往是很有必要的，因为这可以避免语义上出现歧义。对于我们现在关注的这一场景而言，采用如何下的设计可以确保允许 &lt;code&gt;CollectionRuleSet&lt;/code&gt; 为空并同时保证布尔类型的 &lt;code&gt;appliesDisjunctively&lt;/code&gt; 字段的非空性：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ruleSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSet&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bodyHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;appliesDisjunctively&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;小贴士：确保布尔类型的字段永远是非空的。如果你想要一种允许为空的布尔类型，请确保该字段的（空/真/假）三种状态在语义上存在区别的并请务必三思&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则六：将同一类型中互相密切相关的几个字段单独抽出来作为子对象。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="列表和分页"&gt;列表和分页&lt;/h3&gt;
&lt;p&gt;接下来我们开始关注 &lt;code&gt;products&lt;/code&gt; 字段，这个字段看起来是合理的——毕竟我们在移除&lt;code&gt;CollectionMembership&lt;/code&gt;以及梳理它的关联逻辑，但实际这里还是存在着一些问题。&lt;/p&gt;

&lt;p&gt;从语义上来说，该字段返回的是一个包含集合中所有商品的数组——一个集合中可能包含着成千上万款商品。直接罗列全部商品显然是非常低效和代价高昂地，因此我们非常有必要考虑分页问题。&lt;/p&gt;

&lt;p&gt;请记得在设计 GraphQL API 时，一旦遇到数组数字就记得考虑下其是否存在分页的必要。尤其是，记得确认下在真实的业务场景中，这一数字通常会包含多少个元素。&lt;/p&gt;

&lt;p&gt;在 GraphQL API 中实现分页功能存在着许多不同的设计方案。本教程中使用的是由  &lt;a href="https://facebook.github.io/relay/graphql/connections.htm" rel="nofollow" target="_blank" title=""&gt;Relay Connection spec&lt;/a&gt; 所提出的 &lt;a href="https://graphql.org/learn/pagination/#complete-connection-model" rel="nofollow" target="_blank" title=""&gt;Connections&lt;/a&gt; 方案。&lt;/p&gt;

&lt;p&gt;对我们的业务场景来说，将商品字段重新定义为 &lt;code&gt;products: ProductConnection!&lt;/code&gt; 即可实现分页——假设你已经实现了 Relay 规范中所定义的 Connections 的话：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ProductConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProductEdge&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PageInfo&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ProductEdge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PageInfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasNextPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasPreviousPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;规则七：始终记得检查数组字段是否有必要支持分页。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="字符串"&gt;字符串&lt;/h3&gt;
&lt;p&gt;接下来我们开始关注 &lt;code&gt;title&lt;/code&gt;字段，它的设计完全没有问题。考虑到业务上要求所有合集都有 1 个名字，所以它被标记为非空是合理的。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;小提升：在 GraphQL 中 &lt;code&gt;""&lt;/code&gt; 和 &lt;code&gt;null&lt;/code&gt; 是不同的——就像前述的 &lt;code&gt;[]&lt;/code&gt; 、&lt;code&gt;[null]&lt;/code&gt; 和 &lt;code&gt;null&lt;/code&gt;一样。因为从逻辑上来说，它们有着不同的语义：&lt;code&gt;”“&lt;/code&gt;意味着这个字段是存在的，只是恰好值是空白的，而&lt;code&gt;null&lt;/code&gt;则意味着这个字段对于当前实例来说是不适用的。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="关联对象的 ID"&gt;关联对象的 ID&lt;/h3&gt;
&lt;p&gt;接着是 &lt;code&gt;imageId&lt;/code&gt; 字段，这个字段是一个用来说明如果你完全把 REST API 的设计照搬到 GraphQL 中会发生什么的典型例子。在 REST API 中返回其他对象的 ID 是非常常见的行为，但在 GraphQL 中这是一个反模式。因为仅提供其他对象的 ID，意味着我们需要再发起一条新的 GraphQL 查询来查找它们。在 GraphQL 中服务器会仅返回查询中显式列出的字段而不是一股脑的返回该对象所包含的所有字段，因此不必像 REST API 那样担心返回的冗余字段过多。&lt;/p&gt;

&lt;p&gt;作为普遍性规则，我们通常在 GraphQL Schema 设计中尽可能地返回它的关联对象而不仅是关联对象的 ID：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ruleSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSet&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ProductConnection&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;bodyHtml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;appliesDisjunctively&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;规则八：尽可能直接返回关联对象本身而不是仅仅返回关联对象的 ID。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="命名与标量（Scalar）"&gt;命名与标量（Scalar）&lt;/h3&gt;
&lt;p&gt;最后，我们来关注下 &lt;code&gt;Collection&lt;/code&gt; 类型中的 &lt;code&gt;bodyHtml&lt;/code&gt; 字段。对于一个不熟悉技术细节的用户而言，这个字段名可能会存在歧义，因此将之改名为 &lt;code&gt;description&lt;/code&gt; 会是更为合理的选择。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则九：给字段起名时尽可能体现其在业务上的语义，而不是简单照搬数据库中的字段名。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;另外，我们其实应该思考下 &lt;code&gt;String&lt;/code&gt; 对于这个字段而言是否是合理的类型。GraphQL 内置了一些标量类型（例如： &lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Int&lt;/code&gt;、&lt;code&gt;Boolean&lt;/code&gt; 等等），但与此同时它也允许你定义更多的标量类型。基于我们的场景自定义一个叫做 &lt;code&gt;HTML&lt;/code&gt;的标量类型，来说明这个字段的值应该是有效的 HTML 片段，要比直接把它视为一个叫做 &lt;code&gt;bodyHtml&lt;/code&gt; 的字符串字段更为符合语义。&lt;/p&gt;

&lt;p&gt;不过每当你想要添加一个标量字段时，请记得先检查下现有的自定义标量字段中是否已经存在符合语义的选择。如果计划增加一个新的标量类型，最好确保和团队中其他成员达成共识。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则十：使用自定义的标量类型，有助于更好地说明字段隐含的上下文。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="分页的再思考"&gt;分页的再思考&lt;/h3&gt;
&lt;p&gt;审视完 &lt;code&gt;Collection&lt;/code&gt; 之后，我们接下来思考下 &lt;code&gt;CollectionRuleSet&lt;/code&gt; 类型作为一个数组是否有进行分页的必要。&lt;/p&gt;

&lt;p&gt;虽然在大多数场景下分页是必要的，但就 &lt;code&gt;CollectionRuleSet&lt;/code&gt; 而言分页却会是一个极低性价比的选择。因为通常来说一个集合中往往之后包含极少数的规则，甚至对于手动集合来说这直接会上一个空数组。&lt;/p&gt;
&lt;h3 id="枚举（Enum）"&gt;枚举（Enum）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CollectionRule&lt;/code&gt; 类型有着 &lt;code&gt;column&lt;/code&gt;、&lt;code&gt;relation&lt;/code&gt;、&lt;code&gt;condition&lt;/code&gt; 三个字段，分布表示要匹配的属性（值如商品表的 &lt;code&gt;title&lt;/code&gt;字段）、操作符（例如可能是 &lt;code&gt;=&lt;/code&gt; 或者 &lt;code&gt;start_with&lt;/code&gt;）以及期望的值。&lt;/p&gt;

&lt;p&gt;考虑到 &lt;code&gt;column&lt;/code&gt; 是一个数据库术语，因此我们先把这个字段重新命名为 GraphQL 术语—— &lt;code&gt;field&lt;/code&gt; 会比较符合语义。&lt;/p&gt;

&lt;p&gt;接下来，由于 &lt;code&gt;fields&lt;/code&gt; 和 &lt;code&gt;relation&lt;/code&gt; 这两个字段的值完全是能够枚举的，我们可以在 GraphQL 中用枚举类型实现它们来确保 API 拥有更高的健壮性：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ruleSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSet&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ProductConnection&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;appliesDisjunctively&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleField&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleRelation&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleField&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;TAG&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;TITLE&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;TYPE&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;INVENTORY&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;PRICE&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;VENDOR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleRelation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;CONTAINS&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ENDS_WITH&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;EQUALS&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;GREATER_THAN&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;LESS_THAN&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;NOT_CONTAINS&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;NOT_EQUALS&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;STARTS_WITH&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;规则十一：对于值可以被穷举的字段，尽可能使用枚举类型。&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="步骤4: 商业逻辑"&gt;步骤 4: 商业逻辑&lt;/h2&gt;
&lt;p&gt;我们现在已经有了一个基本可用且设计良好的 GraphQL Schema。即便像如何处理商品排序之类的细节问题还没有处理，但完全可以照着前文列出的规则来逐一实现。接下来，我们需要做的是审视现有的 API 设计是否完全满足了产品需求。&lt;/p&gt;

&lt;p&gt;在这一步骤中最为简单的思考方式或许是——想象下假设你同时需要使用这些 API 来开发客户端，那么对于实现客户端中的功能而言目前所能提供的 API 是否能够满足需求。&lt;/p&gt;

&lt;p&gt;不过这种思考范式存在着一些问题——对于大规模应用或者对外提供公开 API 的应用来说，你很难想象客户端中的哪些功能会用到这一接口。一个 GraphQL Type 应该是为很多种不同客户端的很多种不同业务场景提供服务的，你必须同时兼顾到其通用性。因为通用性不足导致的接口冗余意味着额外的工作量和更多的出错概率。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则十二：GraphQL API 提供的应该是业务逻辑而非数据。尽可能把业务逻辑放在 API 中实现，而非任由各个客户端自行实现。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;例如对于我们的例子而言，很有必要实现如下的方法：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;hasProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其用于帮助判断某一商品是否在合集中存在。不同于 Rest API，在 GraphQL 中某个类型存在大量的辅助字段（或方法）不会带来任何的额外开销。&lt;/p&gt;

&lt;p&gt;不过也请注意，即便我们可能尽可能多地提供辅助字段但我们不可能穷举出所有的使用场景。因此提供辅助字段的同时，请千万不要将原始数据隐藏起来。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;译者注：也就是说假设 User 类型存在 &lt;code&gt;lastName&lt;/code&gt; 和 &lt;code&gt;firstName&lt;/code&gt;，额外提供 1 个 &lt;code&gt;fullName&lt;/code&gt; 字段很有帮助，但这不意味着你可以只提供 &lt;code&gt;fullName&lt;/code&gt; 字段。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;规则十三：记得同时提供原始字段与业务相关的计算字段。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;最后，如果在基于业务逻辑设计 GraphQL API 时发现现有的数据库结构或底层实现难以满足，那么不要因此对业务模型有所妥协。一切都应该为业务模型服务的，你可能要做的是改动你的数据库结构或技术实现。&lt;/p&gt;
&lt;h2 id="步骤5: 变更（Mutation）"&gt;步骤 5: 变更（Mutation）&lt;/h2&gt;
&lt;p&gt;我们现有的 GraphQL Schema 只满足了查询需求，接下来我们需要来设计一些 Mutation 以满足对于合集进行创建、修改和删除的需求。&lt;/p&gt;

&lt;p&gt;如同我们在一开始进行 Query 设计时一样，我们先从更高的抽象层级开始思考——这意味着我们先去思考我们需要实现哪些种类的变更，而先不考虑每种 Mutation 的具体实现细节。&lt;/p&gt;

&lt;p&gt;最为「大力出奇迹」的思路是——照着 CRUD 的范式我们给所有需要的类型都创建 &lt;code&gt;增&lt;/code&gt;、&lt;code&gt;删&lt;/code&gt;、&lt;code&gt;改&lt;/code&gt; 三种形式的变更。在 REST API 中这是不错的选择，但对于 GraphQL API 来说这样做是远远不够用的。&lt;/p&gt;
&lt;h3 id="根据具体业务来划分支持的操作种类"&gt;根据具体业务来划分支持的操作种类&lt;/h3&gt;
&lt;p&gt;你可能会注意到如果仅按照 CRUD 范式来做的话，用于实现更新操作的 Mutation 会变得极为臃肿且承担了太多职能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;更新合集的名称或描述&lt;/li&gt;
&lt;li&gt;设置合集生效与否（publish/unpublish）&lt;/li&gt;
&lt;li&gt;修改规则&lt;/li&gt;
&lt;li&gt;添加、移除、重新排序商品&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;等等。这样的设计对于服务器端和客户端来说显然都是一种麻烦的困扰。因此将 GraphQL Mutation 拆分成更细粒度会是个好主意。比方说，我们可以想把 &lt;code&gt;publish&lt;/code&gt;和&lt;code&gt;unpublish&lt;/code&gt; 作为两种不同的操作给拆分出来：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create&lt;/li&gt;
&lt;li&gt;delete&lt;/li&gt;
&lt;li&gt;update&lt;/li&gt;
&lt;li&gt;publish&lt;/li&gt;
&lt;li&gt;unpublish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;规则十四：根据真实的业务需要思考 GraphQL 类型支持哪些种类的操作&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="思考对象与对象间的关系"&gt;思考对象与对象间的关系&lt;/h3&gt;
&lt;p&gt;在我们拆出 &lt;code&gt;publish&lt;/code&gt; &amp;amp; &lt;code&gt;unpublish&lt;/code&gt; 之后，&lt;code&gt;update&lt;/code&gt; Mutation 仍然显得非常臃肿因此有必要做进一步的拆分。，我们可以从对象与对象间的关系作为思考的切入点。
具体到商品与合集直接的关系，我们可以作出如下结论：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;按照 CRUD 范式，当我们需要改变合集中所保护的商品就需要提供一个新的商品数组（形如 &lt;code&gt;products: [ProductInput!]!&lt;/code&gt;），但假设某一集合中包含了非常多种商品，显然这样做会存在不小的性能问题。&lt;/li&gt;
&lt;li&gt;为了实现增量更新，在 &lt;code&gt;update&lt;/code&gt; Mutation 中提供诸如  &lt;code&gt;productsToAdd: [ID!]!&lt;/code&gt; 、&lt;code&gt;productsToReorder: [ID!]!&lt;/code&gt; 和 &lt;code&gt;productsToRemove: [ID!]!&lt;/code&gt; 的字段会是一个好的选择。&lt;/li&gt;
&lt;li&gt;但与其将之作为 &lt;code&gt;update&lt;/code&gt; 的 3 个 input 字段，直接将它们拆分成 3 个新的 Mutation 显然会来的更为简单直观。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不过当我们将上述原则用于其他的场景时，请记得考虑如下问题而不是教条地照搬：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;这个数组是否需要分页？对于小数组而言，提供单独的增量更新很可能是一种过度设计。&lt;/li&gt;
&lt;li&gt;数组中包含的元素是否拥有自己的 ID？例如在 合集和合集规则的关系中，&lt;code&gt;CollectionRule&lt;/code&gt; 类型并没有 &lt;code&gt;id&lt;/code&gt; 字段。因此它并不应该被抽离成单独的 Mutation。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;规则十五：设计 Mutation 很复杂，不能教条式地进行照搬。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;在完成上述改动之后，现在我们拥有这些 Mutation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create&lt;/li&gt;
&lt;li&gt;delete&lt;/li&gt;
&lt;li&gt;update&lt;/li&gt;
&lt;li&gt;publish&lt;/li&gt;
&lt;li&gt;unpublish&lt;/li&gt;
&lt;li&gt;addProducts&lt;/li&gt;
&lt;li&gt;removeProducts&lt;/li&gt;
&lt;li&gt;reorderProducts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;你可能已经注意到了和商品相关的那 3 个 Mutation 都是复数，因为如果我们能够提供在统一合集内同时增加或删除多个商品的方法，显然能满足更多地客户端实际使用场景——除非 PRD 中明确禁止我们这么做。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则十六：尽可能让 Mutation 支持批量操作。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="Input: 结构 - 第一部分"&gt;Input: 结构 - 第一部分&lt;/h3&gt;
&lt;p&gt;在明确了我们需要实现哪些 Mutation 之后，我们还需要明确下每个 Mutation 要支持的入参（Input）有哪些。如果你看过其他人写的 GraphQL Schema，你可能会注意到很多 Mutation 统称都只定义了一个名叫 input 的入参并单独为之定义了一个全局唯一的 Input 类型来描述它实际所需要的入参。&lt;strong&gt;实际上这是传统的 Relay 客户端（Relay Classic API）所要求遵循的规范，我们不建议你在新代码中继续遵循这种约定。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;对于一些简单的 Mutation 而言提供 ID 或 ID 数组就完全足够了，比方说&lt;code&gt;delete&lt;/code&gt;、&lt;code&gt;publish&lt;/code&gt; 和 &lt;code&gt;removeProducts&lt;/code&gt; 等等。&lt;/p&gt;

&lt;p&gt;我们只需要重点考虑下述 3 个 Mutation 的 Input 要如何设计：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create&lt;/li&gt;
&lt;li&gt;update&lt;/li&gt;
&lt;li&gt;reorderProducts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;首先是 &lt;code&gt;create&lt;/code&gt;。仍旧是先写一个「大力出奇迹」的版本出来：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionUnpublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionAddProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;productIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!]!)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionRemoveProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;productIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!])&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ruleSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSetInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ImageInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSetInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CollectionRuleInput&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;appliesDisjunctively&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleField&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleRelation&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能注意到这些 Mutation 的名字都是以形如 &lt;code&gt;类型-行为&lt;/code&gt;(例如 &lt;code&gt;collectionDelete&lt;/code&gt;) 而非 &lt;code&gt;行为-对象&lt;/code&gt; (例如 deleteCollection) 的形式命名，即便后者更符合英文语法。&lt;/p&gt;

&lt;p&gt;这是因为 GraphQL 目前没有提供对于 Mutation 进行分组的办法，因此将类型前置有助于我们在 Schema 中更显眼的看到某一类型支持哪些种类的 Mutation。&lt;/p&gt;

&lt;p&gt;*规则十七：使用形如 &lt;code&gt;orderCancel&lt;/code&gt; 而不是 &lt;code&gt;cancelOrder&lt;/code&gt;的命名风格来个 Mutation 命名 *&lt;/p&gt;
&lt;h3 id="Input: 标量"&gt;Input: 标量&lt;/h3&gt;
&lt;p&gt;在现有的方案中 &lt;code&gt;description&lt;/code&gt; 字段存在这几个问题，首先我们有必要将其设为允许为空。即便是同一类型其被作为 Mutation 的 Input 时和在 Query 中调用时，其是否必填可能是不一致的。需要根据实际情况仔细审视。
&lt;em&gt;规则十八：仅将真的必填的字段在 Input 中设为必填&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;我们还需要根据实际情况审视&lt;code&gt;descripition&lt;/code&gt;的数据类型，例如我们很可能允许用户在输入时仅提供 String 而不是 HTML 片段，而由服务器端在将之保存到数据库前从字符串格式化为 HTML 片段。因此除非你希望这一 字符串到 HTML 片段的格式化逻辑在客户端实现，否则 &lt;code&gt;String&lt;/code&gt; 会是个更好的选择。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则十九：当 Input 类型比较复杂导致客户端进行验证过于复杂时，可以将之弱化成更通用的类型以便于由服务器进行验证。例如用 &lt;code&gt;string&lt;/code&gt; 标量 替代 &lt;code&gt;email&lt;/code&gt; 标量，然后在服务器端进行验证并将所有的错误提示一次性返回给客户端。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;但需要注意的是，这并不是要求你在所有场景下都对输入值使用更弱的类型。例如在本教程的例子中 &lt;code&gt;field&lt;/code&gt; 和 &lt;code&gt;relation&lt;/code&gt; 的值显然必须依然是枚举类型。此外诸如 DateTime 之类的类型显然也不应该被弱化成字符串。关键的区别因素的客户端进行强类型验证的成本以及格式本身的模糊性。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则二十：当输入的格式可能有歧义而且客户端验证并不困难的时候，应该有限考虑使用强类型（例如 &lt;code&gt;DateTime&lt;/code&gt; 优于 &lt;code&gt;String&lt;/code&gt;）&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="Input: 结构 - 第二部分"&gt;Input: 结构 - 第二部分&lt;/h3&gt;
&lt;p&gt;接下来我们把关注点放在 &lt;code&gt;update&lt;/code&gt; Mutation 上：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ruleSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSetInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ImageInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ruleSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSetInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ImageInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会注意到，这与我们的 &lt;code&gt;create&lt;/code&gt; Mutation 非常相似，但有两个不同之处：增加了一个 &lt;code&gt;collectionId&lt;/code&gt; 参数，它决定了要更新哪个合集同时&lt;code&gt;title&lt;/code&gt;不再是必需的，因为&lt;code&gt;title&lt;/code&gt;可能不需要被更新。即便不算上允许为空的 &lt;code&gt;title&lt;/code&gt;，在 &lt;code&gt;create&lt;/code&gt; 和 &lt;code&gt;update&lt;/code&gt; 中有四个 Input 参数是重复的。&lt;/p&gt;

&lt;p&gt;考虑到「DRY（不要重复你自己）」原则，我们觉得在 &lt;code&gt;create&lt;/code&gt; 和 &lt;code&gt;update&lt;/code&gt; Mutation 共享一部分逻辑会是更好的选择：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c"&gt;# ...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionInput&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collectionUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionInput&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ruleSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionRuleSetInput&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ImageInput&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽管这样一来 &lt;code&gt;create&lt;/code&gt; 时的 &lt;code&gt;title&lt;/code&gt; 也变成了允许为空，但如果需要的话我们完全可以在 &lt;code&gt;create&lt;/code&gt; 的 resolver 中再行验证。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则二十一：结构化 Mutation 的 Inpute 以减少重复，即使是以在类型层面上放宽对于某些字段的要求性约束为代价。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="Mutation 的返回值"&gt;Mutation 的返回值&lt;/h3&gt;
&lt;p&gt;我们需要处理的最后一个设计问题是 Mutation 的返回值。通常情况下，Mutation 是可能成功但也可能报错，虽然 GraphQL 确实包含了对查询层面错误的明确支持，但这些错误对于业务层面的 Mutation 报错来说并不够用。因此每个 Mutation 都应该定义一个包含有用户错误字段的 "payload " 类型。对于&lt;code&gt;create&lt;/code&gt; 来说，它可能是这样的：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionCreatePayload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;userErrors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserError&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c"&gt;# Path to input field which caused the error.&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于执行成功的 Mutation，它将返回一个空的 &lt;code&gt;UserErros&lt;/code&gt; 数组，和一个包含了新加入商品的 &lt;code&gt;collection&lt;/code&gt; 数组。而执行失败的 Mutation 则恰恰与之相反。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则二十二：Mutation 的中应该包含一个标识业务层面错误的数组。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;对与 &lt;code&gt;update&lt;/code&gt; 来说也是相似的：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CollectionUpdatePayload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;userErrors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;!]!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得注意的是，即使是在&lt;code&gt;update&lt;/code&gt;里 Payload 的 &lt;code&gt;collection&lt;/code&gt;仍然是可空的，因为如果提供 input 中提供的 ID 数组 不代表一个有效的集合，就没有要返回的集合。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;规则二十三：大多数的 Payload .字段都应该是可以为空的，除非确保其在错误的情况下也有返回值。&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="TLDR: 设计准则总结"&gt;TLDR: 设计准则总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;规则一：永远先从更高的抽象层级进行设计，先考虑类型与类型之间的关系，再去考虑具体的字段。&lt;/li&gt;
&lt;li&gt;规则二：永远不要在 API 中暴露不必要的实现细节。&lt;/li&gt;
&lt;li&gt;规则三：围绕着业务背景重新思考你的 GraphQL API，切忌直接照搬数据库表结构、视觉稿或已有的 REST API。&lt;/li&gt;
&lt;li&gt;规则四：永远记得在 GraphQL 中去掉一个字段要比新增一个字段困难的多。&lt;/li&gt;
&lt;li&gt;规则五：绝大多数业务对象都应该集成自 &lt;code&gt;Node&lt;/code&gt; 接口。&lt;/li&gt;
&lt;li&gt;规则六：将同一类型中互相密切相关的几个字段单独抽出来作为子对象。&lt;/li&gt;
&lt;li&gt;规则七：始终记得检查数组字段是否有必要支持分页。&lt;/li&gt;
&lt;li&gt;规则八：尽可能直接返回关联对象本身而不是仅仅返回关联对象的 ID。&lt;/li&gt;
&lt;li&gt;规则九：给字段起名时尽可能体现其在业务上的语义，而不是简单照搬数据库中的字段名。&lt;/li&gt;
&lt;li&gt;规则十：使用自定义的标量类型，有助于更好地说明字段隐含的上下文。&lt;/li&gt;
&lt;li&gt;规则十一：对于值可以被穷举的字段，尽可能使用枚举类型。&lt;/li&gt;
&lt;li&gt;规则十二：GraphQL API 提供的应该是业务逻辑而非数据。尽可能把业务逻辑放在 API 中实现，而非任由各个客户端自行实现。&lt;/li&gt;
&lt;li&gt;规则十三：记得同时提供原始字段与业务相关的计算字段。&lt;/li&gt;
&lt;li&gt;规则十四：根据真实的业务需要思考 GraphQL 类型支持哪些种类的操作。&lt;/li&gt;
&lt;li&gt;规则十五：设计 Mutation 很复杂，不能教条式地进行照搬。&lt;/li&gt;
&lt;li&gt;规则十六：尽可能让 Mutation 支持批量操作。&lt;/li&gt;
&lt;li&gt;规则十七：使用形如 &lt;code&gt;orderCancel&lt;/code&gt; 而不是 &lt;code&gt;cancelOrder&lt;/code&gt;的命名风格来个 Mutation 命名。&lt;/li&gt;
&lt;li&gt;规则十八：仅将真的必填的字段在 Input 中设为必填。&lt;/li&gt;
&lt;li&gt;规则十九：当 Input 类型比较复杂导致客户端进行验证过于复杂时，可以将之弱化成更通用的类型以便于由服务器进行验证。例如用 &lt;code&gt;string&lt;/code&gt; 标量 替代 &lt;code&gt;email&lt;/code&gt; 标量，然后在服务器端进行验证并将所有的错误提示一次性返回给客户端。&lt;/li&gt;
&lt;li&gt;规则二十：当输入的格式可能有歧义而且客户端验证并不困难的时候，应该有限考虑使用强类型（例如 &lt;code&gt;DateTime&lt;/code&gt; 优于 &lt;code&gt;string&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;规则二十一：结构化 Mutation 的 Inpute 以减少重复，既是是以在类型层面上放宽对于某些字段的要求性约束为代价。&lt;/li&gt;
&lt;li&gt;规则二十二：Mutation 的中应该包含一个标识业务层面错误的数组。&lt;/li&gt;
&lt;li&gt;规则二十三：大多数的 Payload .字段都应该是可以为空的，除非确保其在错误的情况下也有返回值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="结尾"&gt;结尾&lt;/h2&gt;
&lt;p&gt;感谢你阅读我们的教程。希望它有助于你设计一个好的 GraphQL API。&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Sun, 13 Dec 2020 22:35:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/40695</link>
      <guid>https://ruby-china.org/topics/40695</guid>
    </item>
    <item>
      <title>null</title>
      <description>&lt;p&gt;null&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Mon, 06 Apr 2020 15:50:35 +0800</pubDate>
      <link>https://ruby-china.org/topics/39702</link>
      <guid>https://ruby-china.org/topics/39702</guid>
    </item>
    <item>
      <title>[绍兴柯桥 / 杭州萧山] 有数派 诚聘 Ruby 后端、React、React Native 前端工程师 / 长期有效 ( 15K - 30K )</title>
      <description>&lt;h2 id="什么是有数派 ？"&gt;什么是有数派？&lt;/h2&gt;
&lt;p&gt;有数派  ( &lt;a href="https://youshupai.com" rel="nofollow" target="_blank"&gt;https://youshupai.com&lt;/a&gt; ) 是&lt;/p&gt;

&lt;p&gt;国内领先的纺织产业互联网践行者，我们为纺织 贸易 / 工业 企业提供软硬件结合的 SaaS 解决方案帮助传统企业对内提升企业运营效率对外依靠大小数据结合的方式链接产业上下游并提供 B2B 交易服务。&lt;/p&gt;

&lt;p&gt;目前产品功能覆盖了 交易、⽣产、质检、仓储、工业控制 ( 物联网 ) 等业务场景，终端覆盖了 TV、手机、PC、iPad、PDA 等设备，我们通过 SaaS 工具提升纺织企业交易/运营效率，同时通过实时数据监控、数据可视化、数据分析和数据追溯协助 企业做业务决策，用数据驱动业务增长。&lt;/p&gt;

&lt;p&gt;系统目前管理了⼏十亿的⾯料交易订单与几亿匹的布匹数据。UGG、Zara、海澜之家、七匹狼、Tommy、CK 等几十家知名服装厂商的面料供应商都在使用我们的产品。 &lt;/p&gt;
&lt;h2 id="融资情况 ？"&gt;融资情况？&lt;/h2&gt;
&lt;p&gt;我们已经获得一线的美元基金 贝塔斯曼亚洲投资基金（BAI）和 真格基金 的各数百万美元风险投资，其也是 UCloud、Keep、摩拜、哈罗单车、拉钩、寺库、网易云音乐等知名互联网公司的投资人&lt;/p&gt;

&lt;p&gt;相关媒体报道：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5243898" rel="nofollow" target="_blank" title=""&gt;36 氪首发｜36 氪首发｜纺织业 SaaS「有数派」完成数百万美元 A 轮融资，由真格基金独家投资&lt;/a&gt; - &lt;code&gt;[36氪首发｜纺织业SaaS「有数派」完成数百万美元A轮融资，由真格基金独家投资](https://36kr.com/p/5243898)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5230465" rel="nofollow" target="_blank" title=""&gt;36 氪首发｜纺织业 SaaS「有数派」完成数百万美元 Pre-A 轮融资，BAI 独家投资&lt;/a&gt; - &lt;code&gt;[36氪首发｜纺织业SaaS「有数派」完成数百万美元Pre-A轮融资，BAI独家投资](https://36kr.com/p/5230465)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.lieyunwang.com/archives/446073" rel="nofollow" target="_blank" title=""&gt;自主研发了技术架构 + 设计模式，有数派要帮助纺织企业更好地进行管理与运营&lt;/a&gt; - &lt;code&gt;[自主研发了技术架构+设计模式，有数派要帮助纺织企业更好地进行管理与运营](https://www.lieyunwang.com/archives/446073)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5148992" rel="nofollow" target="_blank" title=""&gt;当互联网嵌入传统纺织业，有数派想以 SaaS 工具切入 B2B 交易&lt;/a&gt; - &lt;code&gt;[当互联网嵌入传统纺织业，有数派想以SaaS工具切入B2B交易](https://36kr.com/p/5148992)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们的工程师团队："&gt;我们的工程师团队：&lt;/h2&gt;
&lt;p&gt;目前我们的研发团队一共有 40 多位同事&lt;/p&gt;

&lt;p&gt;前端（React Native + React + iOS &amp;amp; Android）10 + 位&lt;/p&gt;

&lt;p&gt;后端（纯后端 Ruby）10 + 位&lt;/p&gt;

&lt;p&gt;产品 &amp;amp; 设计 若干&lt;/p&gt;

&lt;p&gt;我们来自：&lt;/p&gt;

&lt;p&gt;华兴资本、Tower、搜狐、阿里巴巴、华为、京东、美团、挖财、大搜车、多伦多大学、西北大学、湖南大学、电子科技大学 等一线互联网公司与高等学府。&lt;/p&gt;

&lt;p&gt;同时我们也赞助了今年上海的 RubyConf 活动&lt;/p&gt;
&lt;h2 id="我们的技术栈："&gt;我们的技术栈：&lt;/h2&gt;
&lt;p&gt;全套 GraphQL  + Ruby on rails + PG + React + React Native + MQTT（IoT） . &lt;/p&gt;

&lt;p&gt;我们可能是国内为数不多在后端架构上全套使用 GraphQL 技术的公司，目前用 GraphQL 支撑了 6 个客户端的业务覆盖了近 200 个 Model。&lt;/p&gt;

&lt;p&gt;团队撰写了国内第一本 GraphQL 中文书籍（即将出版） &amp;amp; GraphQL 布道者&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://www.huodongxing.com/event/7441170737900?from=groupmessage" rel="nofollow" target="_blank" title=""&gt;杭州第一届 GraphQLParty&lt;/a&gt; - &lt;code&gt;[杭州第一届 GraphQLParty](http://www.huodongxing.com/event/7441170737900?from=groupmessage)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们在找什么样的人"&gt;我们在找什么样的人&lt;/h2&gt;&lt;h3 id="Ruby 工程师"&gt;Ruby 工程师&lt;/h3&gt;
&lt;p&gt;【基础要求】&lt;/p&gt;

&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上 Ruby on Rails 开发经验；&lt;/p&gt;

&lt;p&gt;2、具有扎实的语言基础，良好的面向对象编程思想；&lt;/p&gt;

&lt;p&gt;3、掌握常用的设计模式，了解移动互联网相关技术；&lt;/p&gt;

&lt;p&gt;4、熟悉 Linux 类环境，能熟练使用 Unix 类系统下的 ruby 编程；&lt;/p&gt;

&lt;p&gt;5、熟悉 MYSQL、PG 或其他大型数据库，能够快速根据需求完成高性能数据库设计，有大负载高可靠设计经验；&lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或大规模数据应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、对前端技术有一定的应用者大大加分（React 方向）&lt;/p&gt;

&lt;p&gt;3、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 5 位 后端工程师&lt;/p&gt;
&lt;h3 id="React 工程师"&gt;React 工程师&lt;/h3&gt;
&lt;p&gt;【基础要求】&lt;/p&gt;

&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上前端开发经验，精通各种 Web 前端的原理及技术；&lt;/p&gt;

&lt;p&gt;2、精通 React 全家桶，对 React, Redux 等源码及核心技术有一定的研究和自己的理解；&lt;/p&gt;

&lt;p&gt;3、对前端组件化和工程化有一定经验，熟悉 webpack，了解其原理并能进行一定的开发工作；&lt;/p&gt;

&lt;p&gt;4、熟悉函数式编程，熟悉 ES6 规范，掌握常用的 JS 设计模式，有良好的编码风格；&lt;/p&gt;

&lt;p&gt;5、对前端前沿技术保持热情的态度，例如 React Hooks, React Fiber，SSR 等； &lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或复杂企业后台应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、有小程序开发经验者加分&lt;/p&gt;

&lt;p&gt;3、对后端技术有实际项目开发经验者加分（Ruby 方向）&lt;/p&gt;

&lt;p&gt;4、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 3 位 React 前端工程师&lt;/p&gt;
&lt;h3 id="React Native工程师"&gt;React Native 工程师&lt;/h3&gt;
&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上前端开发经验，1 年以上 React Native 开发经验，熟练掌握各种 Web 前端的原理及技术；&lt;/p&gt;

&lt;p&gt;2、熟悉 React 和 React Native 基本工作原理，有完整参与或主导过一个 React Ntive 实际项目开发经验；&lt;/p&gt;

&lt;p&gt;3、有一定 Android 或 iOS 原生研发能力；&lt;/p&gt;

&lt;p&gt;4、熟悉函数式编程，熟悉 ES6 规范，掌握常用的 JS 设计模式，有良好的编码风格；&lt;/p&gt;

&lt;p&gt;5、熟悉 React Native 性能优化&lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或复杂企业后台应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、有丰富的原生开发经验者加分（Android 或 iOS）&lt;/p&gt;

&lt;p&gt;3、对后端技术有实际项目开发经验者加分（Ruby 方向）&lt;/p&gt;

&lt;p&gt;4、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 2 位 React Native 前端工程师&lt;/p&gt;
&lt;h2 id="与我联系"&gt;与我联系&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/c77ed3e9-30bd-4449-b539-71de05fb399e.jpg!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Tue, 05 Nov 2019 12:42:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/39213</link>
      <guid>https://ruby-china.org/topics/39213</guid>
    </item>
    <item>
      <title>[北京][实习] Oortcast 招聘 SDE Intern </title>
      <description>&lt;h2 id="技能要求"&gt;技能要求&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;拥有强烈的求知欲与好奇心，相信「个体能够通过创造力推动世界变得更好」；&lt;/li&gt;
&lt;li&gt;熟悉 TypeScript 或 EcmaScript 6；&lt;/li&gt;
&lt;li&gt;基本掌握右列 Web 前端技术栈的一种或多种：React、Immutable.js、Web Components、RxJS、Vue、Angular 9；&lt;/li&gt;
&lt;li&gt;基本掌握右列语言中的一种或多种：Rust、Ruby、Python、Perl、Lisp / Clojure、Go、Scala、Haskell；&lt;/li&gt;
&lt;li&gt;熟悉常用的数据结构，对动态规划、分治、回溯等常用算法有基本的了解；&lt;/li&gt;
&lt;li&gt;了解右列技术中一种或多种者优先：WebAssembly、GraphQL、PostgreSQL、Kubernetes、Solid、RDF；&lt;/li&gt;
&lt;li&gt;足够聪明或有能力让我们觉得你足够聪明。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="岗位收益"&gt;岗位收益&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;200 - 400 元/ 天的薪资，视乎能力而定；&lt;/li&gt;
&lt;li&gt;参与知识图谱与 Web 3.0 领域的前沿创新，与世界级的团队一起参与世界级的项目，提升个人视野与能力极限；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="工作地点"&gt;工作地点&lt;/h2&gt;
&lt;p&gt;北京市朝阳区&lt;/p&gt;
&lt;h2 id="与我们联系"&gt;与我们联系&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;careers{at}oortcast.com&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="我们是谁"&gt;我们是谁&lt;/h2&gt;
&lt;p&gt;Oortcast 为试图改变世界的创业公司和创意工作者提供效率工具、解决技术难题。我们的开源项目——42Page，旨在通过重新定义「文档」软件的形态来指数级提升创意工作者的生产力。&lt;/p&gt;
&lt;h3 id="我们的价值观"&gt;我们的价值观&lt;/h3&gt;&lt;h4 id="科技向善的市井雄心"&gt;科技向善的市井雄心&lt;/h4&gt;
&lt;p&gt;我们相信收入并不仅仅只是货币，它还包括「我们在致力于让世界变得更好的过程中所能够收获到的成就感与影响力」。&lt;/p&gt;
&lt;h4 id="绝不重复你自己"&gt;绝不重复你自己&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;有人说认为花大量的时间做自动化的工具，还不如人肉的效率高，比如，写自动化的脚本花 5 个小时，而重复做这件事 200 次只花 3 个小时。有这样的理解的人根本不懂工程。
一方面，这个工具可以共享重用，更多的人可以从中受益，这次我花 5 个小时开发这个工具，下次只用 1 小时改一下就可以用在别的地方，这是着眼于未来而不是眼下的成本。更重要的是，这是一种文化，一种提高效率的文化，他会鼓励和激发出更多的这样的事情发生。
摘录自 &lt;a href="http://coolshell.cn/articles/17497.html" rel="nofollow" target="_blank" title=""&gt;什么是工程师文化？&lt;/a&gt; - 陈皓&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我们认为工程师文化的精髓并不在于无限量的零售或者有意思的文化衫（尽管这两者其实我们都提供 XD），而在于「&lt;strong&gt;发现根本原因 彻底解决问题&lt;/strong&gt;」。具体而言我们：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;残酷无情的推进自动化&lt;/strong&gt; 你的时间&lt;strong&gt;必需&lt;/strong&gt;花费在最具创造力的事务上。对我们而言，生产力工具与用户产品同等重要。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;保持克制&lt;/strong&gt; 复杂系统意味着更多的熵，我们倾向于优先考虑更简洁的解决方案。同时也意味着，&lt;strong&gt;在能满足需求的情况下理应优先考虑现有的 SaaS 产品或基于开源项目&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;持续改善&lt;/strong&gt; 我们极度重视回顾会议，并对所发现的问题寻求&lt;strong&gt;根本性的解决&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="人人都是斜杠青年"&gt;人人都是斜杠青年&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;做一个异端是有回报的，不仅是在科学领域，在任何有竞争的地方，只要你能看到别人看不到或不敢看的东西，你就有很大的优势。训练自己去想那些不能想的事情，你获得的好处会超过所得到的想法本身。
如果自己就是潮水的一部分，怎么能看见潮流的方向呢？你只能永远保持质疑。问自己，什么话是我不能说的？为什么？
摘录自《黑客与画家》- Paul Graham
TRIZ 理论认为，产品创新的标志是解决或移走设计中的冲突，而产生新的有竞争力的解。设计人员在设计过程中不断的发现并解决冲突是推动产品进化的动力。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;很大程度上跨学科的知识，通过类比往往可以发挥 1+1 › 2 的效率，从而有力的解决设计中的冲突。&lt;/strong&gt; 设计（Design）的词源最早可以被追溯到拉丁文的 de(tomake) 和 signare(mark), 意即制造某一事务并将之符号化从而赋予其与众不同的意义。无论我们的大脑之于真实世界或者系统架构之于赛博世界，符号化本身的抽象意味都是在复杂系统中解构「熵」的重要方式。架构的本质不外乎寻求合适的切入点进行恰到好处的抽象以便复杂度得到有效的管理。&lt;/p&gt;

&lt;p&gt;因而我们往往能够从诸如建筑、法学、经济学等等「设计性」领域获取到新的架构养分与启迪。例如众所周知的「设计模式」概念最早来源于建筑学。&lt;strong&gt;我们鼓励团队成员学习专业领域以外的事物，包括但不限于：通识知识和社会科学。我们也将尽可能的为团队成员学习这些能力提供支持与便利。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Mon, 21 Oct 2019 22:20:59 +0800</pubDate>
      <link>https://ruby-china.org/topics/39175</link>
      <guid>https://ruby-china.org/topics/39175</guid>
    </item>
    <item>
      <title>[绍兴柯桥 / 杭州萧山] 有数派 诚聘 Ruby 后端、React、React Native 前端工程师 / 长期有效 ( 15K - 30K )</title>
      <description>&lt;h2 id="什么是有数派 ？"&gt;什么是有数派？&lt;/h2&gt;
&lt;p&gt;有数派  ( &lt;a href="https://youshupai.com" rel="nofollow" target="_blank"&gt;https://youshupai.com&lt;/a&gt; ) 是&lt;/p&gt;

&lt;p&gt;国内领先的纺织产业互联网践行者，我们为纺织 贸易 / 工业 企业提供软硬件结合的 SaaS 解决方案帮助传统企业对内提升企业运营效率对外依靠大小数据结合的方式链接产业上下游并提供 B2B 交易服务。&lt;/p&gt;

&lt;p&gt;目前产品功能覆盖了 交易、⽣产、质检、仓储、工业控制 ( 物联网 ) 等业务场景，终端覆盖了 TV、手机、PC、iPad、PDA 等设备，我们通过 SaaS 工具提升纺织企业交易/运营效率，同时通过实时数据监控、数据可视化、数据分析和数据追溯协助 企业做业务决策，用数据驱动业务增长。&lt;/p&gt;

&lt;p&gt;系统目前管理了⼏十亿的⾯料交易订单与几亿匹的布匹数据。UGG、Zara、海澜之家、七匹狼、Tommy、CK 等几十家知名服装厂商的面料供应商都在使用我们的产品。 &lt;/p&gt;
&lt;h2 id="融资情况 ？"&gt;融资情况？&lt;/h2&gt;
&lt;p&gt;我们已经获得一线的美元基金 贝塔斯曼亚洲投资基金（BAI）和 真格基金 的各数百万美元风险投资，其也是 UCloud、Keep、摩拜、哈罗单车、拉钩、寺库、网易云音乐等知名互联网公司的投资人&lt;/p&gt;

&lt;p&gt;相关媒体报道：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5243898" rel="nofollow" target="_blank" title=""&gt;36 氪首发｜36 氪首发｜纺织业 SaaS「有数派」完成数百万美元 A 轮融资，由真格基金独家投资&lt;/a&gt; - &lt;code&gt;[36氪首发｜纺织业SaaS「有数派」完成数百万美元A轮融资，由真格基金独家投资](https://36kr.com/p/5243898)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5230465" rel="nofollow" target="_blank" title=""&gt;36 氪首发｜纺织业 SaaS「有数派」完成数百万美元 Pre-A 轮融资，BAI 独家投资&lt;/a&gt; - &lt;code&gt;[36氪首发｜纺织业SaaS「有数派」完成数百万美元Pre-A轮融资，BAI独家投资](https://36kr.com/p/5230465)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.lieyunwang.com/archives/446073" rel="nofollow" target="_blank" title=""&gt;自主研发了技术架构 + 设计模式，有数派要帮助纺织企业更好地进行管理与运营&lt;/a&gt; - &lt;code&gt;[自主研发了技术架构+设计模式，有数派要帮助纺织企业更好地进行管理与运营](https://www.lieyunwang.com/archives/446073)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5148992" rel="nofollow" target="_blank" title=""&gt;当互联网嵌入传统纺织业，有数派想以 SaaS 工具切入 B2B 交易&lt;/a&gt; - &lt;code&gt;[当互联网嵌入传统纺织业，有数派想以SaaS工具切入B2B交易](https://36kr.com/p/5148992)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们的工程师团队："&gt;我们的工程师团队：&lt;/h2&gt;
&lt;p&gt;目前我们的研发团队一共有 40 多位同事&lt;/p&gt;

&lt;p&gt;前端（React Native + React + iOS &amp;amp; Android）10 + 位&lt;/p&gt;

&lt;p&gt;后端（纯后端 Ruby）10 + 位&lt;/p&gt;

&lt;p&gt;产品 &amp;amp; 设计 若干&lt;/p&gt;

&lt;p&gt;我们来自：&lt;/p&gt;

&lt;p&gt;华兴资本、Tower、搜狐、阿里巴巴、华为、京东、美团、挖财、大搜车、多伦多大学、西北大学、湖南大学、电子科技大学 等一线互联网公司与高等学府。&lt;/p&gt;

&lt;p&gt;同时我们也赞助了今年上海的 RubyConf 活动&lt;/p&gt;
&lt;h2 id="我们的技术栈："&gt;我们的技术栈：&lt;/h2&gt;
&lt;p&gt;全套 GraphQL  + Ruby on rails + PG + React + React Native + MQTT（IoT） . &lt;/p&gt;

&lt;p&gt;我们可能是国内为数不多在后端架构上全套使用 GraphQL 技术的公司，目前用 GraphQL 支撑了 6 个客户端的业务覆盖了近 200 个 Model。&lt;/p&gt;

&lt;p&gt;团队撰写了国内第一本 GraphQL 中文书籍（即将出版） &amp;amp; GraphQL 布道者&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://www.huodongxing.com/event/7441170737900?from=groupmessage" rel="nofollow" target="_blank" title=""&gt;杭州第一届 GraphQLParty&lt;/a&gt; - &lt;code&gt;[杭州第一届 GraphQLParty](http://www.huodongxing.com/event/7441170737900?from=groupmessage)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们在找什么样的人"&gt;我们在找什么样的人&lt;/h2&gt;&lt;h3 id="Ruby 工程师"&gt;Ruby 工程师&lt;/h3&gt;
&lt;p&gt;【基础要求】&lt;/p&gt;

&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上 Ruby on Rails 开发经验；&lt;/p&gt;

&lt;p&gt;2、具有扎实的语言基础，良好的面向对象编程思想；&lt;/p&gt;

&lt;p&gt;3、掌握常用的设计模式，了解移动互联网相关技术；&lt;/p&gt;

&lt;p&gt;4、熟悉 Linux 类环境，能熟练使用 Unix 类系统下的 ruby 编程；&lt;/p&gt;

&lt;p&gt;5、熟悉 MYSQL、PG 或其他大型数据库，能够快速根据需求完成高性能数据库设计，有大负载高可靠设计经验；&lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或大规模数据应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、对前端技术有一定的应用者大大加分（React 方向）&lt;/p&gt;

&lt;p&gt;3、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 5 位 后端工程师&lt;/p&gt;
&lt;h3 id="React 工程师"&gt;React 工程师&lt;/h3&gt;
&lt;p&gt;【基础要求】&lt;/p&gt;

&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上前端开发经验，精通各种 Web 前端的原理及技术；&lt;/p&gt;

&lt;p&gt;2、精通 React 全家桶，对 React, Redux 等源码及核心技术有一定的研究和自己的理解；&lt;/p&gt;

&lt;p&gt;3、对前端组件化和工程化有一定经验，熟悉 webpack，了解其原理并能进行一定的开发工作；&lt;/p&gt;

&lt;p&gt;4、熟悉函数式编程，熟悉 ES6 规范，掌握常用的 JS 设计模式，有良好的编码风格；&lt;/p&gt;

&lt;p&gt;5、对前端前沿技术保持热情的态度，例如 React Hooks, React Fiber，SSR 等； &lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或复杂企业后台应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、有小程序开发经验者加分&lt;/p&gt;

&lt;p&gt;3、对后端技术有实际项目开发经验者加分（Ruby 方向）&lt;/p&gt;

&lt;p&gt;4、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 3 位 React 前端工程师&lt;/p&gt;
&lt;h3 id="React Native工程师"&gt;React Native 工程师&lt;/h3&gt;
&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上前端开发经验，1 年以上 React Native 开发经验，熟练掌握各种 Web 前端的原理及技术；&lt;/p&gt;

&lt;p&gt;2、熟悉 React 和 React Native 基本工作原理，有完整参与或主导过一个 React Ntive 实际项目开发经验；&lt;/p&gt;

&lt;p&gt;3、有一定 Android 或 iOS 原生研发能力；&lt;/p&gt;

&lt;p&gt;4、熟悉函数式编程，熟悉 ES6 规范，掌握常用的 JS 设计模式，有良好的编码风格；&lt;/p&gt;

&lt;p&gt;5、熟悉 React Native 性能优化&lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或复杂企业后台应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、有丰富的原生开发经验者加分（Android 或 iOS）&lt;/p&gt;

&lt;p&gt;3、对后端技术有实际项目开发经验者加分（Ruby 方向）&lt;/p&gt;

&lt;p&gt;4、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 2 位 React Native 前端工程师&lt;/p&gt;
&lt;h2 id="与我联系"&gt;与我联系&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/c77ed3e9-30bd-4449-b539-71de05fb399e.jpg!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Thu, 29 Aug 2019 10:45:28 +0800</pubDate>
      <link>https://ruby-china.org/topics/38993</link>
      <guid>https://ruby-china.org/topics/38993</guid>
    </item>
    <item>
      <title>[绍兴柯桥 / 杭州萧山] 有数派 诚聘 Ruby 后端工程师 / 长期有效 ( 15K - 32K )</title>
      <description>&lt;h2 id="什么是有数派 ？"&gt;什么是有数派？&lt;/h2&gt;
&lt;p&gt;有数派  ( &lt;a href="https://youshupai.com" rel="nofollow" target="_blank"&gt;https://youshupai.com&lt;/a&gt; ) 是&lt;/p&gt;

&lt;p&gt;国内领先的纺织产业互联网践行者，我们为纺织 贸易 / 工业 企业提供软硬件结合的 SaaS 解决方案帮助传统企业对内提升企业运营效率对外依靠大小数据结合的方式链接产业上下游并提供 B2B 交易服务。&lt;/p&gt;

&lt;p&gt;目前产品功能覆盖了 交易、⽣产、质检、仓储、工业控制 ( 物联网 ) 等业务场景，终端覆盖了 TV、手机、PC、iPad、PDA 等设备，我们通过 SaaS 工具提升纺织企业交易/运营效率，同时通过实时数据监控、数据可视化、数据分析和数据追溯协助 企业做业务决策，用数据驱动业务增长。&lt;/p&gt;

&lt;p&gt;系统目前管理了⼏十亿的⾯料交易订单与几亿匹的布匹数据。UGG、Zara、海澜之家、七匹狼、Tommy、CK 等几十家知名服装厂商的面料供应商都在使用我们的产品。 &lt;/p&gt;

&lt;p&gt;相关媒体报道：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.lieyunwang.com/archives/446073" rel="nofollow" target="_blank" title=""&gt;自主研发了技术架构 + 设计模式，有数派要帮助纺织企业更好地进行管理与运营&lt;/a&gt; - &lt;code&gt;[自主研发了技术架构+设计模式，有数派要帮助纺织企业更好地进行管理与运营](https://www.lieyunwang.com/archives/446073)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5148992" rel="nofollow" target="_blank" title=""&gt;当互联网嵌入传统纺织业，有数派想以 SaaS 工具切入 B2B 交易&lt;/a&gt; - &lt;code&gt;[当互联网嵌入传统纺织业，有数派想以SaaS工具切入B2B交易](https://36kr.com/p/5148992)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们的工程师团队："&gt;我们的工程师团队：&lt;/h2&gt;
&lt;p&gt;目前我们的研发团队一共有 20 多位同事&lt;/p&gt;

&lt;p&gt;前端（React Native + React + iOS &amp;amp; Android）10 位&lt;/p&gt;

&lt;p&gt;后端（纯后端 Ruby）7 位&lt;/p&gt;

&lt;p&gt;产品 &amp;amp; 设计 若干&lt;/p&gt;

&lt;p&gt;我们来自：&lt;/p&gt;

&lt;p&gt;华兴资本、Tower、搜狐、阿里巴巴、华为、京东、美团、挖财、大搜车、多伦多大学、西北大学、湖南大学、电子科技大学 等一线互联网公司与高等学府。&lt;/p&gt;
&lt;h2 id="我们的技术栈："&gt;我们的技术栈：&lt;/h2&gt;
&lt;p&gt;全套 GraphQL  + Ruby on rails + PG + React + React Native + MQTT（IoT） . &lt;/p&gt;

&lt;p&gt;我们可能是国内为数不多在后端架构上全套使用 GraphQL 技术的公司，目前用 GraphQL 支撑了 6 个客户端的业务覆盖了近 200 个 Model。&lt;/p&gt;

&lt;p&gt;团队撰写了国内第一本 GraphQL 中文书籍（即将出版） &amp;amp; GraphQL 布道者&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://www.huodongxing.com/event/7441170737900?from=groupmessage" rel="nofollow" target="_blank" title=""&gt;杭州第一届 GraphQLParty&lt;/a&gt; - &lt;code&gt;[杭州第一届 GraphQLParty](http://www.huodongxing.com/event/7441170737900?from=groupmessage)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们在找什么样的人"&gt;我们在找什么样的人&lt;/h2&gt;
&lt;p&gt;【基础要求】&lt;/p&gt;

&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上 Ruby on Rails 开发经验；&lt;/p&gt;

&lt;p&gt;2、具有扎实的语言基础，良好的面向对象编程思想；&lt;/p&gt;

&lt;p&gt;3、掌握常用的设计模式，了解移动互联网相关技术；&lt;/p&gt;

&lt;p&gt;4、熟悉 Linux 类环境，能熟练使用 Unix 类系统下的 ruby 编程；&lt;/p&gt;

&lt;p&gt;5、熟悉 MYSQL、PG 或其他大型数据库，能够快速根据需求完成高性能数据库设计，有大负载高可靠设计经验；&lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或大规模数据应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、对前端技术有一定的应用者大大加分（React 方向）&lt;/p&gt;

&lt;p&gt;3、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 3 位 后端工程师&lt;/p&gt;
&lt;h2 id="与我联系"&gt;与我联系&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/c77ed3e9-30bd-4449-b539-71de05fb399e.jpg!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Mon, 27 May 2019 20:04:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/38579</link>
      <guid>https://ruby-china.org/topics/38579</guid>
    </item>
    <item>
      <title>[绍兴柯桥 / 杭州萧山] 有数派 诚聘 Ruby 后端工程师 / 长期有效 ( 15K - 30K )</title>
      <description>&lt;h2 id="什么是有数派 ？"&gt;什么是有数派？&lt;/h2&gt;
&lt;p&gt;有数派  ( &lt;a href="https://youshupai.com" rel="nofollow" target="_blank"&gt;https://youshupai.com&lt;/a&gt; ) 是&lt;/p&gt;

&lt;p&gt;国内领先的纺织产业互联网践行者，我们为纺织 贸易 / 工业 企业提供软硬件结合的 SaaS 解决方案帮助传统企业对内提升企业运营效率对外依靠大小数据结合的方式链接产业上下游并提供 B2B 交易服务。&lt;/p&gt;

&lt;p&gt;目前产品功能覆盖了 交易、⽣产、质检、仓储、工业控制 ( 物联网 ) 等业务场景，终端覆盖了 TV、手机、PC、iPad、PDA 等设备，我们通过 SaaS 工具提升纺织企业交易/运营效率，同时通过实时数据监控、数据可视化、数据分析和数据追溯协助 企业做业务决策，用数据驱动业务增长。&lt;/p&gt;

&lt;p&gt;系统目前管理了⼏十亿的⾯料交易订单与几亿匹的布匹数据。UGG、Zara、海澜之家、七匹狼、Tommy、CK 等几十家知名服装厂商的面料供应商都在使用我们的产品。 &lt;/p&gt;

&lt;p&gt;相关媒体报道：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.lieyunwang.com/archives/446073" rel="nofollow" target="_blank" title=""&gt;自主研发了技术架构 + 设计模式，有数派要帮助纺织企业更好地进行管理与运营&lt;/a&gt; - &lt;code&gt;[自主研发了技术架构+设计模式，有数派要帮助纺织企业更好地进行管理与运营](https://www.lieyunwang.com/archives/446073)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://36kr.com/p/5148992" rel="nofollow" target="_blank" title=""&gt;当互联网嵌入传统纺织业，有数派想以 SaaS 工具切入 B2B 交易&lt;/a&gt; - &lt;code&gt;[当互联网嵌入传统纺织业，有数派想以SaaS工具切入B2B交易](https://36kr.com/p/5148992)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们的工程师团队："&gt;我们的工程师团队：&lt;/h2&gt;
&lt;p&gt;目前我们的研发团队一共有 20 多位同事&lt;/p&gt;

&lt;p&gt;前端（React Native + React + iOS &amp;amp; Android）10 位&lt;/p&gt;

&lt;p&gt;后端（纯后端 Ruby）7 位&lt;/p&gt;

&lt;p&gt;产品 &amp;amp; 设计 若干&lt;/p&gt;

&lt;p&gt;我们来自：&lt;/p&gt;

&lt;p&gt;华兴资本、Tower、搜狐、阿里巴巴、华为、京东、美团、挖财、大搜车、多伦多大学、西北大学、湖南大学、电子科技大学 等一线互联网公司与高等学府。&lt;/p&gt;
&lt;h2 id="我们的技术栈："&gt;我们的技术栈：&lt;/h2&gt;
&lt;p&gt;全套 GraphQL  + Ruby on rails + PG + React + React Native + MQTT（IoT） . &lt;/p&gt;

&lt;p&gt;我们可能是国内为数不多在后端架构上全套使用 GraphQL 技术的公司，目前用 GraphQL 支撑了 6 个客户端的业务覆盖了近 200 个 Model。&lt;/p&gt;

&lt;p&gt;团队撰写了国内第一本 GraphQL 中文书籍（即将出版） &amp;amp; GraphQL 布道者&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://www.huodongxing.com/event/7441170737900?from=groupmessage" rel="nofollow" target="_blank" title=""&gt;杭州第一届 GraphQLParty&lt;/a&gt; - &lt;code&gt;[杭州第一届 GraphQLParty](http://www.huodongxing.com/event/7441170737900?from=groupmessage)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="我们在找什么样的人"&gt;我们在找什么样的人&lt;/h2&gt;
&lt;p&gt;【基础要求】&lt;/p&gt;

&lt;p&gt;0、相信产业互联网的机会，并愿意投身传统行业改造浪潮的工程师&lt;/p&gt;

&lt;p&gt;1、3 年以上 Ruby on Rails 开发经验；&lt;/p&gt;

&lt;p&gt;2、具有扎实的语言基础，良好的面向对象编程思想；&lt;/p&gt;

&lt;p&gt;3、掌握常用的设计模式，了解移动互联网相关技术；&lt;/p&gt;

&lt;p&gt;4、熟悉 Linux 类环境，能熟练使用 Unix 类系统下的 ruby 编程；&lt;/p&gt;

&lt;p&gt;5、熟悉 MYSQL、PG 或其他大型数据库，能够快速根据需求完成高性能数据库设计，有大负载高可靠设计经验；&lt;/p&gt;

&lt;p&gt;【加分项】&lt;/p&gt;

&lt;p&gt;1、有 Iot 工业产品或大规模数据应用开发经验者加分&lt;/p&gt;

&lt;p&gt;2、对前端技术有一定的应用者大大加分（React 方向）&lt;/p&gt;

&lt;p&gt;3、对 GraphQL 技术栈有一定的研究或者应用者大大加分&lt;/p&gt;

&lt;p&gt;【薪资 &amp;amp; HeadCount】&lt;/p&gt;

&lt;p&gt;薪资范围：15k - 30k&lt;/p&gt;

&lt;p&gt;本次招聘 3 位 后端工程师&lt;/p&gt;
&lt;h2 id="与我联系"&gt;与我联系&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/c77ed3e9-30bd-4449-b539-71de05fb399e.jpg!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Mon, 08 Apr 2019 14:48:24 +0800</pubDate>
      <link>https://ruby-china.org/topics/38359</link>
      <guid>https://ruby-china.org/topics/38359</guid>
    </item>
    <item>
      <title>[上海] 红杉资本招聘数据开发&amp; web 全栈实习生</title>
      <description>&lt;h2 id="我们是谁"&gt;我们是谁&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://sequoiacap.com/" rel="nofollow" target="_blank" title=""&gt;红杉资本&lt;/a&gt;始终致力于帮助创业者成就基业常青的伟大公司，为成员企业带来丰富的全球资源和宝贵的历史经验。&lt;strong&gt;45 年来，红杉资本投资了众多创新企业，包括苹果、思科、甲骨文、谷歌、阿里巴巴、Airbnb、京东等产业潮流领导者。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;红杉资本在中国、印度、美国三个全球最具创新力或发展潜力的国家设有本地化基金。&lt;/p&gt;

&lt;p&gt;红杉资本中国基金作为「&lt;strong&gt;创业者背后的创业者&lt;/strong&gt;」，专注于科技/传媒、医疗健康、消费品/服务、工业科技四个方向的投资机遇。&lt;strong&gt;十二年来，红杉资本中国基金投资了包括阿里巴巴、阿里影业、贝达药业、铂涛酒店集团、大众点评网、德邦物流、DJI 大疆创新、赶集网、高德软件、光环新网、华大基因、今日头条、京东、聚美优品、美丽说、美团网、陌陌、诺亚财富、奇虎 360、万达院线、威高集团、唯品会、文思创新、新产业生物、新浪网、英雄互娱、鱼跃医疗、掌趣科技、中通快递在内的 300 余家企业。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="工作地点"&gt;工作地点&lt;/h2&gt;
&lt;p&gt;上海市静安区恒隆广场&lt;/p&gt;
&lt;h2 id="工作内容"&gt;工作内容&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;与合作方对接 API 接口，进行各类外部数据源的接入与维护；&lt;/li&gt;
&lt;li&gt;分布式爬虫的开发与维护、数据的清洗与加工；&lt;/li&gt;
&lt;li&gt;参与 BI 系统的 web 开发；&lt;/li&gt;
&lt;li&gt;参与自然语言处理和数据挖掘。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="能力要求"&gt;能力要求&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;能熟练使用 &lt;code&gt;Python&lt;/code&gt;、&lt;code&gt;Ruby&lt;/code&gt;、&lt;code&gt;Go&lt;/code&gt;、&lt;code&gt;Scala&lt;/code&gt;、&lt;code&gt;Grovvy&lt;/code&gt;、&lt;code&gt;Java&lt;/code&gt; 语言中的一种或多种（&lt;strong&gt;Ruby 优先&lt;/strong&gt;），熟悉 SQL 语言和常用 Linux 命令；&lt;/li&gt;
&lt;li&gt;熟悉常用的数据结构，对分类算法、聚类分析、回归模型等有基本了解；&lt;/li&gt;
&lt;li&gt;基本掌握 &lt;code&gt;React&lt;/code&gt;、&lt;code&gt;VUE&lt;/code&gt;、&lt;code&gt;Angular&lt;/code&gt;等前端开发框架的一种或多种，有 web 开发经验；&lt;/li&gt;
&lt;li&gt;在爬虫、反爬虫、ETL、数据仓库、数据可视化方面拥有实际工作经验者优先；&lt;/li&gt;
&lt;li&gt;曾在信息学奥赛、ACM 竞赛中获奖者优先;&lt;/li&gt;
&lt;li&gt;参与过开源项目，github star 数量多者优先;&lt;/li&gt;
&lt;li&gt;了解右列技术中一种或多种者优先： &lt;code&gt;functional programming&lt;/code&gt;、 &lt;code&gt;Spark&lt;/code&gt;、&lt;code&gt;Elasticsearch&lt;/code&gt;、&lt;code&gt;Neo4j&lt;/code&gt;、&lt;code&gt;GraphQL&lt;/code&gt;、&lt;code&gt;Docker&lt;/code&gt;、&lt;code&gt;Serverless&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;简历请投 &lt;code&gt;echo aHJpbnRlcm5Ac2VxdW9pYWNhcC5jb20K | base64 -D&lt;/code&gt; (标题注明 ruby-china 有加分)&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>cn_boris</author>
      <pubDate>Tue, 13 Mar 2018 15:27:17 +0800</pubDate>
      <link>https://ruby-china.org/topics/35223</link>
      <guid>https://ruby-china.org/topics/35223</guid>
    </item>
    <item>
      <title>[北京] 15-30K 逐鹿 X 招聘 Ruby/ 全栈工程师 资深前端工程师</title>
      <description>&lt;h2 id="为什么是我们"&gt;为什么是我们&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;我们通过知识、经验和思考改变世界，而不是——「我有一个改变世界的 idea，只差一个程序员了」。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;高于 BAT 的顶尖薪资 / 五险一金及商业补充保险 / 餐费及话费补助&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;站在硅谷与华尔街的十字入口，你的工作将极大的提高整个行业的投融资效率从而让这个世界变得更加美好。换言之，&lt;strong&gt;这份工作能让你真真切切的走在改变世界的路上&lt;/strong&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;完善的技术成长体系，我们与知名技术服务平台——极牛合作，&lt;strong&gt;每月邀请业界大牛进行技术交流&lt;/strong&gt; （目前已组织了和豆瓣前端负责人 张克军、美团 iOS 负责人 陈晓亮等业内大牛的交流活动），同时我们也是 Rubyconf China 2015 的银牌赞助商。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="招聘岗位"&gt;招聘岗位&lt;/h2&gt;&lt;h3 id="资深前端工程师（ 15-30K ）"&gt;资深前端工程师（15-30K）&lt;/h3&gt;
&lt;p&gt;岗位职责：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;负责 Hybrid App 的开发工作&lt;/li&gt;
&lt;li&gt;负责前端团队工具链的选型及持续优化&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;任职要求：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;拥有丰富的前端工程经验，特别是 Hybird App 开发经验，熟悉 Cordova 相关 API 接口；&lt;/li&gt;
&lt;li&gt;深刻理解 web 标准，对前端性能、可访问性、可维护性等知识拥有实践经验；&lt;/li&gt;
&lt;li&gt;熟悉 webpack 或其他前端工具链，对于提升前端团队开发效率有自己的独道见解；&lt;/li&gt;
&lt;li&gt;熟悉至少一门非前端脚本语言；&lt;/li&gt;
&lt;li&gt;在 Github 上有开源项目；&lt;/li&gt;
&lt;li&gt;了解 iOS 或 Android 原生应用开发者优先考虑。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="Ruby/全栈工程师 （ 15-30K ）"&gt;Ruby/全栈工程师（15-30K）&lt;/h3&gt;
&lt;p&gt;岗位职责：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;负责业务支撑系统的研发及 DevOps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;任职要求：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;拥有 2 年以上 Ruby on Rails 项目经验；&lt;/li&gt;
&lt;li&gt;如果应聘全栈工程师熟悉至少一种前端 MVC 框架，包括但不限于 AngularJS、React、Vue.js；&lt;/li&gt;
&lt;li&gt;熟悉 PostgreSQL 数据库或有 OLAP 系统开发经验者优先。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="我们是谁"&gt;我们是谁&lt;/h2&gt;
&lt;p&gt;逐鹿 X 是一款以创业者需求为核心的投融资工具。我们期望通过技术的力量，为尚处于刀耕火种时代的创投领域带来文明之光。
&lt;a href="http://e.vhall.com/874122178" rel="nofollow" target="_blank"&gt;http://e.vhall.com/874122178&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="如何勾搭"&gt;如何勾搭&lt;/h2&gt;
&lt;p&gt;请 Email 简历至 hr[at]zhulux.com&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;同时务请附上您的 Github 帐号或技术博客地址 这将作为我们研判的重要依据&lt;/strong&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Wed, 27 Jan 2016 21:18:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/28886</link>
      <guid>https://ruby-china.org/topics/28886</guid>
    </item>
    <item>
      <title>使用 Nginx 解決 Google Analytics 在大陆被牆的問題</title>
      <description>&lt;h4 id="需求點"&gt;需求點&lt;/h4&gt;
&lt;p&gt;由於眾所周之的原因，Google Analytics 在大陸地區總是「不&lt;del&gt;太&lt;/del&gt;穩定」，且無其他可真正比肩者。故而需要通過海外服務器作為反向代理以解決此種問題。同時為統計結果之準確計，必須確保用戶 IP 等信息得以透明傳輸。&lt;/p&gt;
&lt;h4 id="解決方案"&gt;解決方案&lt;/h4&gt;
&lt;p&gt;nginx 配置文件：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;server &lt;span class="o"&gt;{&lt;/span&gt;
    listen 80&lt;span class="p"&gt;;&lt;/span&gt;
    listen 443 ssl spdy&lt;span class="p"&gt;;&lt;/span&gt;
  server_name analytics.example.com&lt;span class="p"&gt;;&lt;/span&gt;      
    ssl_certificate /usr/local/tengine/certs/example.crt&lt;span class="p"&gt;;&lt;/span&gt;
  ssl_certificate_key /usr/local/tengine/certs/example.key&lt;span class="p"&gt;;&lt;/span&gt; 
  location /ga_proxy &lt;span class="o"&gt;{&lt;/span&gt;
    proxy_set_header            X-real-ip &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    rewrite ^/ga_proxy/&lt;span class="o"&gt;(&lt;/span&gt;.&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;/&lt;span class="nv"&gt;$1&lt;/span&gt;?&lt;span class="nv"&gt;$args&lt;/span&gt;&amp;amp;uip&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    proxy_pass http://www.google-analytics.com&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  location /analytics.js &lt;span class="o"&gt;{&lt;/span&gt;
    default_type text/html&lt;span class="p"&gt;;&lt;/span&gt;
    subs_filter_types text/css text/xml text/javascript&lt;span class="p"&gt;;&lt;/span&gt;
    subs_filter &lt;span class="s1"&gt;'www.google-analytics.com'&lt;/span&gt; &lt;span class="s1"&gt;'analytics.example.com/ga_proxy'&lt;/span&gt; g&lt;span class="p"&gt;;&lt;/span&gt;
    proxy_set_header            X-real-ip &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    proxy_set_header X-Forwarded-For &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    proxy_set_header Referer https://www.google-analytics.com&lt;span class="p"&gt;;&lt;/span&gt;
    proxy_set_header Host www.google-analytics.com&lt;span class="p"&gt;;&lt;/span&gt;
    proxy_pass https://www.google-analytics.com&lt;span class="p"&gt;;&lt;/span&gt;
    proxy_set_header Accept-Encoding &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客戶端 JS 代碼：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GoogleAnalyticsObject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;//analytics.example.com/analytics.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ga&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;ga&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UA-EXAMPLE-0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;ga&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;send&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pageview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原文首發於我的博客 &lt;a href="https://hacker.design/ji-yu-nginxfan-xiang-dai-li-jie-jue-google/" rel="nofollow" target="_blank"&gt;https://hacker.design/ji-yu-nginxfan-xiang-dai-li-jie-jue-google/&lt;/a&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Sat, 19 Sep 2015 05:35:41 +0800</pubDate>
      <link>https://ruby-china.org/topics/27400</link>
      <guid>https://ruby-china.org/topics/27400</guid>
    </item>
    <item>
      <title>複雜邏輯下解耦 CANCANCAN 的 Ability 模型</title>
      <description>&lt;h4 id="業務場景"&gt;業務場景&lt;/h4&gt;
&lt;p&gt;假定系統中存在 &lt;code&gt;developer&lt;/code&gt; &lt;code&gt;hacker&lt;/code&gt; &lt;code&gt;designer&lt;/code&gt; &lt;code&gt;startup&lt;/code&gt; 四種角色，且每種角色具獨立的權限需求。倘全部定義於&lt;code&gt;ability.rb&lt;/code&gt;則未免過於臃腫而不可讀。&lt;/p&gt;
&lt;h4 id="解決方案"&gt;解決方案&lt;/h4&gt;
&lt;p&gt;基於角色維度進行&lt;code&gt;ability.rb&lt;/code&gt;的切分如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/ability.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ability&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CanCan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Ability&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
      &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt; &lt;span class="no"&gt;Abilities&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LoginedUserAbility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt; &lt;span class="no"&gt;Abilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;const_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_ability"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt; &lt;span class="no"&gt;Abilities&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;GuestAbility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/abilities/logined_user_ability.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Abilities&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginedUserAbility&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CanCan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Ability&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/abilities/developer_ability.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Abilities&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeveloperAbility&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CanCan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Ability&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其餘角色同上&lt;/p&gt;

&lt;p&gt;原文首發於我的博客 &lt;a href="https://blog.boris.tech/posts/fu-za-luo-ji-xia-jie-ou-cancancan-de-ability-mo-xing" rel="nofollow" target="_blank"&gt;https://blog.boris.tech/posts/fu-za-luo-ji-xia-jie-ou-cancancan-de-ability-mo-xing&lt;/a&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Sat, 19 Sep 2015 05:32:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/27399</link>
      <guid>https://ruby-china.org/topics/27399</guid>
    </item>
    <item>
      <title>[北京] 10-40K 诚聘 Ruby 开发者及 NLP 工程师 投行内部创业项目</title>
      <description>&lt;h2 id="我们是谁"&gt;我们是谁&lt;/h2&gt;
&lt;p&gt;华兴资本是国内 TMT 领域最顶尖的投资银行（没有之一），亦是 &lt;strong&gt;滴滴快滴合并&lt;/strong&gt;、&lt;strong&gt;京东&amp;amp;微博&amp;amp;陌陌上市&lt;/strong&gt;、 &lt;strong&gt;豌豆荚&amp;amp;饿了吗融资&lt;/strong&gt; 等等一系列互联网圈大案要案的&lt;strong&gt;「幕后推手」&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;逐鹿 X 是华兴资本旗下的一个内部创业项目。我们期望通过技术的力量，为尚处于&lt;strong&gt;刀耕火种&lt;/strong&gt;时代的创投领域带来文明之光。&lt;/p&gt;

&lt;p&gt;我们的主要技术栈是：Ruby on Rails、PostgreSQL、Redis、AngularJS &amp;amp; Ionic、RSpec &amp;amp; Karma、Sass&amp;amp;CoffeeScript，以及我们在团队内部实践 TDD、Scrum 和 DevOps。&lt;/p&gt;
&lt;h2 id="为什么是我们"&gt;为什么是我们&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt; 我们提供最卓越的薪酬福利及工作环境。包括但不限于：&lt;strong&gt;高于 BAT&lt;/strong&gt;的顶尖薪资、补充商业保险、每年至少&lt;strong&gt;15 天年假&lt;/strong&gt;、（已经成为互联网公司标配的）人体工学座椅、顶配 MBP、大屏显示器、无限量零食供应。&lt;/li&gt;
&lt;li&gt; 作为&lt;strong&gt;大型企业的内部创业项目&lt;/strong&gt;，尽管我们从零开始但拥有最丰富的资源和行业经验。&lt;/li&gt;
&lt;li&gt; 我们真的在改变世界。我们真的在改变世界。我们真的在改变世界（重要的事情一定要说三遍）。你的每一行代码，都旨在让整个真实世界的创业融资更有效率、将数以万计的人们从繁琐机械的重复劳动中解救出来，并基于此&lt;strong&gt;让整个世界变得更加美好&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt; 我们是一支由心怀壮志的专业人士组成的团队——每个人都在自己的专业领域具备丰富经验及独到见解。我们通过&lt;strong&gt;知识&lt;/strong&gt;、&lt;strong&gt;经验&lt;/strong&gt;和&lt;strong&gt;思考&lt;/strong&gt;改变世界，而不是——「我有一个改变世界的 idea，只差一个程序员了」。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="招聘岗位"&gt;招聘岗位&lt;/h2&gt;&lt;h4 id="Ruby Developer （10-30k）"&gt;Ruby Developer (10-30k)&lt;/h4&gt;
&lt;p&gt;任职要求：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;熟悉 Ruby on Rails。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;熟悉右列语言中的任意 1 种（ECMAScript、Python、Swift、Go、Erlang、PHP）。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;使用*nix 系统作为开发及生产环境。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;熟悉至少一种关系型或非关系型数据库。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;了解 RESTful 规范，并有相关之实践经验。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;了解 SQL Injection、XSS、CSRF 等常见 web 攻击手段及其防御方法。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;基本了解 TDD、Scrum 及 DevOps 理念。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;加分项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;具 haskell、lisp 等函数式编程语言使用经验。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;具 Docker 或 Puppet 经验。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;了解 Growth Hacker。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="NLP自然语言处理工程师（10-40k）"&gt;NLP 自然语言处理工程师（10-40k）&lt;/h4&gt;
&lt;p&gt;任职要求：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;熟悉至少一种脚本语言。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;拥有扎实的数理基础，对数据结构拥有较深入之了解。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;熟悉进行文本数据挖掘所必须的各项手段，包括但不限于：中文分词、情感分析、自动摘要。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;对基于协同过滤的推荐算法具有一定的了解及实践经验。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;对算法复杂度及算法优化具有较为敏锐之感知。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;加分项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;在高影响因子的期刊或会议上发布过相关领域论文。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;具有 haskell、lisp 等函数式编程语言使用经验，或具有 GPU 编程经验。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;具有 Deep Learning、Knowledge Graph 等方向之实践经验。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;具有非结构数据的数据挖掘经验。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="如何勾搭"&gt;如何勾搭&lt;/h2&gt;
&lt;p&gt;请将您的简历 Email 至 borisding【at】huaxing.com（PDF 格式）。
如若能够随手附上 Github、Dribbble、StackOver、Ruby China、SegmentFault 或其他在线社区 ID，将帮助我们更深入的了解您。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's To The Crazy Ones. 
The misfits. The rebels. The trouble-makers. The round pegs in the square holes. 
The ones who see things differently.
They're not fond of rules, and they have no respect for the status-quo. 
You can quote them, disagree with them, glorify, or vilify them.
About the only thing you can't do is ignore them. 
Because they change things. They push the human race forward. 
And while some may see them as the crazy ones, we see genius.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>cn_boris</author>
      <pubDate>Mon, 24 Aug 2015 20:24:02 +0800</pubDate>
      <link>https://ruby-china.org/topics/27067</link>
      <guid>https://ruby-china.org/topics/27067</guid>
    </item>
    <item>
      <title>Rails4 + Carrierwave 下實現 JSON 形式的文件上傳 API</title>
      <description>&lt;p&gt;相較於傳統的 &lt;code&gt;multipart/form-data&lt;/code&gt; 表單上傳，在 API 中將文件以 Base64 形式編碼後置於 JSON 中會是更為妥貼的實現方式。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/carrierwaveuploader/carrierwave" rel="nofollow" target="_blank" title=""&gt;Carrierwave&lt;/a&gt; 是一個知名的文件上傳 GEM。下文將主要闡述在 Rails4 與 Carrierwave 中如何實現一個&lt;strong&gt;不依賴於具體數據庫&lt;/strong&gt;的獨立上傳 API。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/media.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Media&lt;/span&gt;  
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;

  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:original_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:content_type&lt;/span&gt;
  &lt;span class="nb"&gt;attr_writer&lt;/span&gt; &lt;span class="ss"&gt;:file&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:original_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:content_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;inclusion: &lt;/span&gt;&lt;span class="sx"&gt;%w(logo image video)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;file&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class_eval&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:original_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:content_type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# 確保與 Carrierwave 所預期的格式保持一致&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;original_filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;original_filename&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="c1"&gt;# 返回 data&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload!&lt;/span&gt;
    &lt;span class="n"&gt;uploader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;const_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Uploader"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="c1"&gt;# 允許按需上傳到不同的 Uploader&lt;/span&gt;
    &lt;span class="n"&gt;uploader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store!&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uploader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;為保證更好的閱讀體驗，全文請見我的博客： &lt;a href="https://hacker.design/design-json-upload-api-base-by-carrierwave/" rel="nofollow" target="_blank"&gt;https://hacker.design/design-json-upload-api-base-by-carrierwave/&lt;/a&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Sat, 15 Aug 2015 12:23:52 +0800</pubDate>
      <link>https://ruby-china.org/topics/26937</link>
      <guid>https://ruby-china.org/topics/26937</guid>
    </item>
    <item>
      <title>[北京国贸] 招聘全栈或 Ruby 开发者 华兴资本旗下内部创业项目</title>
      <description>&lt;h2 id="我们是谁"&gt;我们是谁&lt;/h2&gt;
&lt;p&gt;华兴资本是国内 TMT 领域最顶尖的投资银行（没有之一），亦是 &lt;strong&gt;滴滴快滴合并&lt;/strong&gt;、&lt;strong&gt;京东&amp;amp;微博&amp;amp;陌陌上市&lt;/strong&gt;、 &lt;strong&gt;豌豆荚&amp;amp;饿了吗融资&lt;/strong&gt; 等等一系列互联网圈大案要案的&lt;strong&gt;「幕后推手」&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/af2d3cfad0ea9a9954c797abb350228d.jpeg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;逐鹿 X 是华兴资本旗下的一个内部创业项目。我们期望通过技术的力量，为尚处于&lt;strong&gt;刀耕火种&lt;/strong&gt;时代的创投领域带来文明之光。&lt;/p&gt;
&lt;h2 id="招聘岗位"&gt;招聘岗位&lt;/h2&gt;&lt;h4 id="Web Developer (全栈/前端/ruby)"&gt;Web Developer (全栈/前端/ruby)&lt;/h4&gt;
&lt;p&gt;你将参与到后端 RESTful API 及前端 SPA 的开发（可以只参与到其中一种，亦可兼而有之。重点是你开心就好^_^）&lt;/p&gt;

&lt;p&gt;我们的主要技术栈是：Ruby on Rails、PostgreSQL、Redis、AngularJS &amp;amp; Ionic、RSpec &amp;amp; Karma、Sass&amp;amp;CoffeeScript。&lt;/p&gt;

&lt;p&gt;我们对于全栈的定义并不局限于技术领域。我们更期望你目前是或想要成为一名 Growth Hacker (&lt;a href="https://en.wikipedia.org/wiki/Growth_hacking" rel="nofollow" target="_blank"&gt;https://en.wikipedia.org/wiki/Growth_hacking&lt;/a&gt;) 。&lt;/p&gt;

&lt;p&gt;以及我们在团队内部实践 TDD、Scrum 和 DevOps。&lt;/p&gt;

&lt;p&gt;在面试过程中，我们会重点关注你之前做过的项目、踩过坑与总结的经验以及对技术的展望。&lt;/p&gt;
&lt;h2 id="为什么是我们"&gt;为什么是我们&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt; 我们提供最卓越的薪酬福利及工作环境。包括但不限于：&lt;strong&gt;高于 BAT&lt;/strong&gt;的顶尖薪资、补充商业保险、每年至少&lt;strong&gt;15 天年假&lt;/strong&gt;、（已经成为互联网公司标配的）人体工学座椅、顶配 MBP、大屏显示器、无限量零食供应。&lt;/li&gt;
&lt;li&gt; 作为&lt;strong&gt;大型企业的内部创业项目&lt;/strong&gt;，尽管我们从零开始但拥有最丰富的资源和行业经验。&lt;/li&gt;
&lt;li&gt; 我们真的在改变世界。我们真的在改变世界。我们真的在改变世界（重要的事情一定要说三遍）。你的每一行代码，都旨在让整个真实世界的创业融资更有效率、将数以万计的人们从繁琐机械的重复劳动中解救出来，并基于此&lt;strong&gt;让整个世界变得更加美好&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt; 我们是一支由心怀壮志的专业人士组成的团队——每个人都在自己的专业领域具备丰富经验及独到见解。我们通过&lt;strong&gt;知识&lt;/strong&gt;、&lt;strong&gt;经验&lt;/strong&gt;和&lt;strong&gt;思考&lt;/strong&gt;改变世界，而不是——「我有一个改变世界的 idea，只差一个程序员了」。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="如何勾搭"&gt;如何勾搭&lt;/h2&gt;
&lt;p&gt;请将您的简历 Email 至 borisding【at】huaxing.com（PDF 格式）。
如若能够随手附上 Github、Dribbble、StackOver、Ruby China、SegmentFault 或其他在线社区 ID，将帮助我们更深入的了解您。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's To The Crazy Ones. 
The misfits. The rebels. The trouble-makers. The round pegs in the square holes. 
The ones who see things differently.
They're not fond of rules, and they have no respect for the status-quo. 
You can quote them, disagree with them, glorify, or vilify them.
About the only thing you can't do is ignore them. 
Because they change things. They push the human race forward. 
And while some may see them as the crazy ones, we see genius.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>cn_boris</author>
      <pubDate>Sun, 19 Jul 2015 23:16:14 +0800</pubDate>
      <link>https://ruby-china.org/topics/26558</link>
      <guid>https://ruby-china.org/topics/26558</guid>
    </item>
    <item>
      <title>一个测试 JSON API 时用来对比 JSON 内容的 helper，以及请教是否有更优雅的写法</title>
      <description>&lt;h2 id="业务需求"&gt;业务需求&lt;/h2&gt;
&lt;p&gt;在测试 json api 时，测试返回的 json 是否与预期的一致。&lt;/p&gt;
&lt;h2 id="最初的解决方案　"&gt;最初的解决方案 &lt;/h2&gt;
&lt;p&gt;使用了一个叫做 &lt;a href="https://github.com/alto/assert_json" rel="nofollow" target="_blank" title=""&gt;assert_json&lt;/a&gt; 的 GEM。但感觉过于繁琐，特别是在测试用例多的时候。&lt;/p&gt;
&lt;h2 id="现时的解决方案"&gt;现时的解决方案&lt;/h2&gt;
&lt;p&gt;手写了几个 helper&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;response_json&lt;/span&gt;
   &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;end&lt;/span&gt;

 &lt;span class="c1"&gt;# data_map could look like {uid: :id,last_name: :last_name,full_name: '"#{exp['last_name']}#{exp['first_name']}"'}&lt;/span&gt;
 &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_json_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;data_map&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# convert array to hash (key == value)&lt;/span&gt;
   &lt;span class="n"&gt;data_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
     &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
     &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# support insets&lt;/span&gt;
       &lt;span class="n"&gt;assert_json_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
     &lt;span class="k"&gt;else&lt;/span&gt;
       &lt;span class="kp"&gt;false&lt;/span&gt;
     &lt;span class="k"&gt;end&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
 &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="使用示例"&gt;使用示例&lt;/h4&gt;
&lt;p&gt;假设我们有这样 1 个 model &lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User (id,last_name,first_name,password,updated_at,created_at)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 json 返回值为 User.first.to_json 时，直接使用 assert_json_equal User.first 即可，而无需指定 data_map。&lt;/p&gt;

&lt;p&gt;当 json 只返回 {"id":1,"last_name":'Jobs'} 时，则需使用数组作为 data_map，形如 assert_json_equal User.first,[:id,:last_name]&lt;/p&gt;

&lt;p&gt;而当 json 返回的数据结果较复杂时，则需要使用 hash 作为 data_map，形如：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;返回结果&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;full_name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Steve&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Jobs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;password:'&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 验证json返回&lt;/span&gt;
&lt;span class="vi"&gt;@data_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;uid: :id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;full_name: &lt;/span&gt;&lt;span class="s1"&gt;'"#{exp.first_name} #{exp.last_name}"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;password: :password&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="n"&gt;assert_json_equal&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="vi"&gt;@data_map&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="问题"&gt;问题&lt;/h2&gt;
&lt;p&gt;总觉得这样的实现似乎有点怪怪的，以及是否有更优雅的写法？&lt;/p&gt;

&lt;p&gt;Ruby 新手一枚，还望各位大牛不吝赐教。&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Tue, 21 Apr 2015 17:43:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/25235</link>
      <guid>https://ruby-china.org/topics/25235</guid>
    </item>
    <item>
      <title>如何权衡 DRY 与可读性</title>
      <description>&lt;p&gt;*** UPDATE ***&lt;/p&gt;

&lt;p&gt;抱歉之前精简了部分无关的业务代码，可能导致了一定的误导性。现在补上完整的应用场景&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;比如同一功能的下述两种实现方式，大家觉得使用那种会更恰当？&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 方案1&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ApiHelper&lt;/span&gt;
  &lt;span class="sx"&gt;%w(get post put patch delete)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="nb"&gt;module_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;__LINE__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
      def api_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;(params={},headers={}}
        headers.merge!('X-Token' =&amp;gt; CFG['api_token']['apollo'])
        &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;(path,params,headers)
      end
&lt;/span&gt;&lt;span class="no"&gt;    EOF&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 方案 2&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ApiHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;request_send&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;request_send&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;request_send&lt;/span&gt; &lt;span class="ss"&gt;:put&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;request_send&lt;/span&gt; &lt;span class="ss"&gt;:patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;request_send&lt;/span&gt; &lt;span class="ss"&gt;:delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;CFG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'api_token'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'apollo'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nb"&gt;send&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>cn_boris</author>
      <pubDate>Wed, 24 Dec 2014 16:34:37 +0800</pubDate>
      <link>https://ruby-china.org/topics/23383</link>
      <guid>https://ruby-china.org/topics/23383</guid>
    </item>
    <item>
      <title>Announcing RemoteIE: Test the latest IE on Mac OS X</title>
      <description>&lt;p&gt;Today, we’re excited to announce the preview availability of RemoteIE via Azure RemoteApp. &lt;/p&gt;

&lt;p&gt;This is a free service from Microsoft that allows you to run the latest version of Internet Explorer on the Windows 10 Technical Preview from your Windows, Mac OS X, iOS or Android devices, without the need to run a new OS or heavyweight virtual machine on your device. &lt;/p&gt;

&lt;p&gt;Going forward, this will be the recommended way for developers who are not running Windows 10 to test the latest IE preview versions.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://blogs.msdn.com/b/ie/archive/2014/11/02/announcing-remoteie-test-the-latest-ie-on-windows-mac-os-x-ios-and-android.aspx" rel="nofollow" target="_blank"&gt;http://blogs.msdn.com/b/ie/archive/2014/11/02/announcing-remoteie-test-the-latest-ie-on-windows-mac-os-x-ios-and-android.aspx&lt;/a&gt;&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Tue, 04 Nov 2014 11:49:33 +0800</pubDate>
      <link>https://ruby-china.org/topics/22457</link>
      <guid>https://ruby-china.org/topics/22457</guid>
    </item>
    <item>
      <title>[广州] 情感行业互联网创业公司 诚聘 Ruby 研发工程师（Senior/ 实习 / 应届皆宜）</title>
      <description>&lt;h4 id="关于公司"&gt;关于公司&lt;/h4&gt;
&lt;p&gt;泡学网（ &lt;a href="http://www.paoxue.com" rel="nofollow" target="_blank" title=""&gt;www.paoxue.com&lt;/a&gt; ）成立于 2008 年 3 月是目前国内最大的男性情感门户。&lt;/p&gt;

&lt;p&gt;迄今为止，我们已经成功帮助超过 50 万会员提升约会、恋爱、社交等技能。&lt;/p&gt;

&lt;p&gt;网站拥有原创文章超过 350 万，包含内容丰富的约会、恋爱、社交资讯。&lt;/p&gt;

&lt;p&gt;&lt;a href="http://v.youku.com/v_show/id_XNjg0MDkyMTA0.html?qq-pf-to=pcqq.c2c" rel="nofollow" target="_blank" title=""&gt;欢迎点击这里查看我们的简介视频&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="关于团队"&gt;关于团队&lt;/h4&gt;
&lt;p&gt;作为互联网公司，我们热衷于把玩 Ruby、ODPS（阿里云版 hadoop）、响应式设计、Scrum 管理 等业界最为前沿的技术；&lt;/p&gt;

&lt;p&gt;作为创业公司，我们没有各种笑里藏刀的「办公室政治」或者臃肿不堪的「办事流程」；&lt;/p&gt;

&lt;p&gt;作为情感行业的公司，我们能够为你提供系统的培训机会，以便你能够在人际关系领域得到极大的成长（确实，某种程度上这意味着你得到了一本《恋爱秘籍》，只要你愿意相信情感是一门可以被习得的艺术）。&lt;/p&gt;
&lt;h4 id="基本要求"&gt;基本要求&lt;/h4&gt;
&lt;p&gt;熟悉我们的技术栈（Linux + ROR + MySQL + Memcached + Nginx + Git）；&lt;/p&gt;

&lt;p&gt;真正热爱服务器端编程，而不仅仅将 coding 作为一种谋生工具；&lt;/p&gt;
&lt;h4 id="加分项"&gt;加分项&lt;/h4&gt;
&lt;p&gt;熟悉 JRuby；&lt;/p&gt;

&lt;p&gt;有自己的技术博客或在 Github 中参与多个开源项目;&lt;/p&gt;

&lt;p&gt;把玩过 PHP 或 JAVA;&lt;/p&gt;
&lt;h4 id="关于薪资"&gt;关于薪资&lt;/h4&gt;
&lt;p&gt;我们将根据能力确定薪资，总体而言高于业界平均水平且上不封顶。&lt;/p&gt;
&lt;h4 id="其他"&gt;其他&lt;/h4&gt;
&lt;p&gt;如有意向烦请将简历 email 至 &lt;code&gt;hr[at]pv.cc&lt;/code&gt; 。作为技术岗位的招聘，请务必在 email 中附上你的 Github 地址。如果可能，烦请尽量使用 PDF 格式投递简历。&lt;/p&gt;

&lt;p&gt;在面试过程中，我们会重点关注你之前做过的项目、踩过坑以及总结的经验。&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Wed, 10 Sep 2014 18:05:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/21459</link>
      <guid>https://ruby-china.org/topics/21459</guid>
    </item>
    <item>
      <title>[广州] 靠谱互联网团队招人</title>
      <description>&lt;p&gt;我们是一家立足于情感行业的互联网创业公司，坐标广州大学城。&lt;/p&gt;

&lt;p&gt;作为互联网公司，我们热衷于把玩 Ruby、ODPS（阿里云版 hadoop）、响应式设计、Scrum 管理 等业界最为前沿的技术；作为创业公司，我们没有各种笑里藏刀的「办公室政治」或者臃肿不堪的「办事流程」；作为情感行业的公司，我们能够为你提供系统的培训机会，以便你能够在人际关系领域得到极大的成长（确实，某种程度上这意味着你得到了一本《恋爱秘籍》，只要你愿意相信情感是一门可以被习得的艺术）。&lt;/p&gt;

&lt;p&gt;以下是正在招聘的岗位清单，如有意向烦请将简历 email 至 &lt;code&gt;hr[at]pv.cc&lt;/code&gt; 。作为技术岗位的招聘，请务必在 email 中附上你的 Github 地址。如果可能，烦请尽量使用 PDF 格式投递简历。&lt;/p&gt;
&lt;h2 id="Web研发工程师（Senior级）"&gt;Web 研发工程师（Senior 级）&lt;/h2&gt;
&lt;p&gt;我们主要使用 Ruby 来进行后端开发，偶尔也会基于业务需求混搭些 PHP、JAVA 神马的。当然，我们并不要求你同时具有上述几种语言的项目经验。因为，我们相信具有 Senior 级经验的你完全可以在相对短的时间里基本掌握一门新的语言。&lt;/p&gt;

&lt;p&gt;尽管我们有专职的前端工程师，但你可能偶尔也需要写一些简单的页面。对了，我们使用 Bitbucket 进行代码管理。所以，你必须要能够熟练的使用 Git 以及 Bootstrap。&lt;/p&gt;

&lt;p&gt;尽管我们不要求强制实施 TDD，但大多数情况下你可能被要求给自己的代码加上单元测试。&lt;/p&gt;

&lt;p&gt;在面试过程中，我们会重点关注你之前做过的项目、踩过坑以及总结的经验。&lt;/p&gt;
&lt;h2 id="Web研发工程师 （实习&amp;amp;应届）"&gt;Web 研发工程师（实习&amp;amp;应届）&lt;/h2&gt;
&lt;p&gt;理想情况下，我们期望你已经做过至少 2 个实现了完整功能的网站项目、了解诸如 MVC、ORM、设计模式、数据结构之类的基本概念。&lt;/p&gt;

&lt;p&gt;同样，作为加入团队的最低要求 你必须已经能够熟练的使用 Git 以及 Bootstrap。&lt;/p&gt;

&lt;p&gt;具有足够优秀的自学能力，也是你胜任这一岗位的基本要求。&lt;/p&gt;

&lt;p&gt;在面试过程中，我们将重点考察你的学习能力、沟通能力以及诸如算法、数据结构之类的基本功。&lt;/p&gt;
&lt;h2 id="前端开发工程师（Senior级）"&gt;前端开发工程师（Senior 级）&lt;/h2&gt;
&lt;p&gt;理想情况下，我们期望你已经具有多年的前端开发经验。无论高端大气上档次的 ECMAScript 或者 坑爹苦逼超屌丝的 IE6 兼容，对你而言都是小菜一碟。（当然，作为一家有节操的互联网公司实际上我们只要求兼容 IE8 及以上版本）&lt;/p&gt;

&lt;p&gt;作为 Senior 级别的 F2E，你将主要负责前端框架的维护以及前端组件的开发。我们使用支付宝前端团队的 Sea.js、Arale 以及 Alice ui 作为前端解决方案，如果你有这方面的经验便是再好不过了。&lt;/p&gt;

&lt;p&gt;要是你有使用 LESS/SASS 或者 CoffeeScript 的经验，那会成为占有极高权重的加分项。&lt;/p&gt;

&lt;p&gt;在面试过程中，我们将重点考察你的基本功底（JS&amp;amp;CSS）以及 架构规划能力。&lt;/p&gt;
&lt;h2 id="测试工程师（实习&amp;amp;应届）"&gt;测试工程师（实习&amp;amp;应届）&lt;/h2&gt;
&lt;p&gt;你将主要负责各个项目的集成测试工作。所以，你必须要心细如丝到令人发指以至于走火入魔的境界。（摁，某种意义上这意味着你可能要有类似于处女座的那种性格。）&lt;/p&gt;

&lt;p&gt;我们在尽可能的实行测试自动化，以帮助你摆脱那些枯燥且繁复的「脏活」。所以，你必须要具有足够牛逼的学习能力以及基本的脚本语言编程功底以驾驭那些自动化工具。&lt;/p&gt;

&lt;p&gt;当然，受限于工具的局限性。偶尔你可能还不得不去人肉完成那些枯燥且繁复的「脏活」，所以你必须要有足够的耐心才能够胜任这一工作。&lt;/p&gt;

&lt;p&gt;你也会负责整理一些基本的技术文档，所以你必须具有流畅且无歧义的文字表达能力。&lt;/p&gt;

&lt;p&gt;在面试过程中，我们将重点考察你的学习能力、程序语言基础（C、Java、pascal 或你学过的其他语言）以及语言表达能力。&lt;/p&gt;</description>
      <author>cn_boris</author>
      <pubDate>Wed, 16 Jul 2014 22:03:01 +0800</pubDate>
      <link>https://ruby-china.org/topics/20527</link>
      <guid>https://ruby-china.org/topics/20527</guid>
    </item>
  </channel>
</rss>
