<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>yetrun (Yet Run)</title>
    <link>https://ruby-china.org/yetrun</link>
    <description/>
    <language>en-us</language>
    <item>
      <title>Rails 中实现自定义卡片视图帮助方法的心路历程</title>
      <description>&lt;h2 id="实现一个基本的 cards_for 效果"&gt;实现一个基本的 &lt;code&gt;cards_for&lt;/code&gt; 效果&lt;/h2&gt;
&lt;p&gt;我需要用 BootStrap Card 组件实现一个卡片列表，单个的 BootStrap Card 组件的使用方式如下：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 18rem;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h5&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Card title&lt;span class="nt"&gt;&amp;lt;/h5&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Some quick example text to build on the card title and make up the bulk of the card's content.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Go somewhere&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是它的效果：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/yetrun/03bd3677-389a-46f1-ba5a-cbc30f900988.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;现在我需要展示一堆这样的卡片，卡片之间要有间距，大致是这样的效果（这是一行的效果，实际上应当支持多行，这里不再展示）：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/yetrun/aed19775-4ab8-41b4-833c-0558adcf417c.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;就此机会，我的第一步是实现一个名为 card 的局部视图，像这样：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- _card.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 18rem;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h5&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/h5&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= link %&amp;gt;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Go somewhere&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;card 局部视图接受三个变量：&lt;code&gt;name&lt;/code&gt;、&lt;code&gt;description&lt;/code&gt;、&lt;code&gt;link&lt;/code&gt;，分别显示卡片的标题，正文和指向的链接。&lt;/p&gt;

&lt;p&gt;如果需要显示一堆卡片，我需要这样做：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"d-flex gap-3 p-3 flex-wrap"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;models.each&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;name:&lt;/span&gt; &lt;span class="na"&gt;model.name&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; 
                       &lt;span class="na"&gt;description:&lt;/span&gt; &lt;span class="na"&gt;model.description&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; 
                       &lt;span class="na"&gt;link:&lt;/span&gt; &lt;span class="na"&gt;model.link&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我想到我的项目中有一堆这样模型，它也是满足 &lt;code&gt;name&lt;/code&gt;、&lt;code&gt;description&lt;/code&gt;、&lt;code&gt;link&lt;/code&gt; 这三个属性的。为了减少重复代码，我决定实现一个自定义的 helper 方法，来简化这些卡片列表的渲染过程。这不仅能提高代码的可读性，还能确保一致的样式和结构。&lt;/p&gt;

&lt;p&gt;我将这个方法命名为 &lt;code&gt;cards_for&lt;/code&gt;，就像 Rails 自带的视图帮助方法 &lt;code&gt;form_for&lt;/code&gt; 等一样，&lt;code&gt;cards_for&lt;/code&gt;方法只接受一个满足要求的 &lt;code&gt;models&lt;/code&gt; 属性即可：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;cards_for&lt;/span&gt; &lt;span class="na"&gt;models:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;models&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Helper 中我只需要实现成这样：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CardsHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cards_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;cards_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                     &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                     &lt;span class="ss"&gt;link: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;    
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt;
    &lt;span class="n"&gt;content_tag&lt;/span&gt; &lt;span class="ss"&gt;:div&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cards_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"d-flex gap-3 p-3 flex-wrap"&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;在 Helper 中写代码，简直就像在视图中写代码那样，只要你找到对应的方法。&lt;/p&gt;
&lt;h2 id="为 cards_for 添砖加瓦所受的伤"&gt;为 &lt;code&gt;cards_for&lt;/code&gt; 添砖加瓦所受的伤&lt;/h2&gt;
&lt;p&gt;接下来，我的任务是继续为 &lt;code&gt;cards_for&lt;/code&gt; 方法添砖加瓦。因为我们总不能保证传递进来的模型满足 &lt;code&gt;name&lt;/code&gt;、&lt;code&gt;description&lt;/code&gt;、&lt;code&gt;link&lt;/code&gt; 这样的命名关系，为了适应这种情况，我的初步想法是，为 cards_for 方法添加一个块，将块的返回值作为局部变量传递给 card 视图：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CardsHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cards_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cards_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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;model&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="ss"&gt;link: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="c1"&gt;# 保险起见，我写成这样&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt;
    &lt;span class="n"&gt;content_tag&lt;/span&gt; &lt;span class="ss"&gt;:div&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cards_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"d-flex gap-3 p-3 flex-wrap"&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;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;cards_for&lt;/span&gt; &lt;span class="na"&gt;models:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;models&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {
    name: "T-" + model.name,
    description: "T-" + model.description,
    link: "https://www.sina.com"
  }
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;  &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后给我报了这样一个错误：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;undefined method `keys' for "\n  {\n    name: \"T-\" + model.name,\n    description: \"T-\" + model.description,\n    link: \"https://www.sina.com\"\n  }\n":String
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是我调用的写法写错了，要改成这样才正确：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;cards_for&lt;/span&gt; &lt;span class="na"&gt;models:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;models&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;
  &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;T-&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="na"&gt;model.name&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;T-&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="na"&gt;model.description&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;link:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;https:&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="na"&gt;www.sina.com&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意区别很有限，只比上一个少了一个 &lt;code&gt;%&amp;gt;&lt;/code&gt; 结束标签和 &lt;code&gt;&amp;lt;%&lt;/code&gt; 打开标签，但也是我们经常犯的错误。原因是因为 ERB 对于不在 &lt;code&gt;&amp;lt;% ... %&amp;gt;&lt;/code&gt; 标签内的代码，它会视为纯文本。请细细品味，方能知晓。&lt;/p&gt;

&lt;p&gt;至此，一个 &lt;code&gt;cards_for&lt;/code&gt; 帮助方法已经实现了。还可以继续为其添砖加瓦，利用 Ruby 语言的灵活性。&lt;/p&gt;</description>
      <author>yetrun</author>
      <pubDate>Fri, 26 Jul 2024 11:12:00 +0800</pubDate>
      <link>https://ruby-china.org/topics/43825</link>
      <guid>https://ruby-china.org/topics/43825</guid>
    </item>
    <item>
      <title>meta-api: 版本 0.2.0 小结</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;项目地址：&lt;a href="https://github.com/yetrun/web-frame" rel="nofollow" target="_blank"&gt;https://github.com/yetrun/web-frame&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/yetrun/web-frame" rel="nofollow" target="_blank" title=""&gt;meta-api&lt;/a&gt; 已经拼拼凑凑地到了 0.2.0 版本了，这次的更新算是又一次的升华和梳理。可以说 meta-api 是我对于 API 开发的所有经验总结，也是我对于 API 开发的所有期望。只不过 API 开发是一个见仁见智的水平，所以 meta-api 也一直处于不温不火的状态。&lt;/p&gt;

&lt;p&gt;编写框架是个非常劳累的活计，基本属于吃力不讨好吧。支撑我干下去的初衷只是对我当初的想法的坚持，我想让它实现，看看效果。这次 0.2.0 版本有可能是最终形态的雏形了，以后只做一些小修小补的工作，实在是力不从心了。&lt;/p&gt;

&lt;p&gt;meta-api 它有两大特点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;它的核心思想是：设计即是实现。这样，我们就可以避免实现与文档不一致的问题。&lt;/li&gt;
&lt;li&gt;它提供了静态场景化的方式，用于定义不同的场景。这样，我们就可以在不同的接口中声明不同的场景。更为要紧的事，这样的声明体现在文档中，不会出错，不会造成歧义。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="主要的核心问题：设计即是实现"&gt;主要的核心问题：设计即是实现&lt;/h2&gt;
&lt;p&gt;在 API 开发领域，我所遇到的一个主要问题是，要写两茬罪：1. 实现一套 API；2. 针对实现的 API 编写文档给前端看（或者反过来）。总之，遇到一些修改的时候，比如一个接口多返回一个字段，我们总要去改两个地方。如果忘记修改（这往往会出现），就会遇到实现与文档不一致的地方，徒增交流成本。&lt;/p&gt;

&lt;p&gt;meta-api 的核心思想是：设计即是实现。也就是说，我们只需要设计一次，然后就可以直接生成实现和文档。这样，我们就可以避免实现与文档不一致的问题。&lt;/p&gt;
&lt;h3 id="Ruby Grape 框架"&gt;Ruby Grape 框架&lt;/h3&gt;
&lt;p&gt;Grape 框架是 Ruby 语言里专注于 API 开发的，它其实可以生成对应的文档。我在 2022 年的时候逐渐放弃它，主要是它做到了“设计即实现”，但又好像只做到了一半。所以，我就想自己写一个。&lt;/p&gt;

&lt;p&gt;首先，它支持参数定义，然后使用 grape-entity 可以渲染实体给前端。但是它的参数定义和实体渲染是分开的，这造成了前面提到的两茬罪最终还是两茬罪，只不过添加字段的点变了。&lt;/p&gt;

&lt;p&gt;在我写稿的日子里，Grape 框架似乎有所升级，支持通过实体的 &lt;code&gt;.documentation&lt;/code&gt; 方法生成字段用于 &lt;code&gt;params&lt;/code&gt; 宏，我是从官方文档的前面小节里看到的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BasicAPI&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Grape&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;
  &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s1"&gt;'Statuses index'&lt;/span&gt; &lt;span class="k"&gt;do&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;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entities&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;documentation&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="ss"&gt;:all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entities&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;documentation&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/statuses'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;statuses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="ss"&gt;:full&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;
    &lt;span class="n"&gt;present&lt;/span&gt; &lt;span class="n"&gt;statuses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entities&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="n"&gt;type&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;blockquote&gt;
&lt;p&gt;这里有很多别扭的点，不知道你发现没发现。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这个功能在我 2022 年的时候就已经有了，那时非常地不好用。实体里面的 &lt;code&gt;type&lt;/code&gt; 格式与 &lt;code&gt;params&lt;/code&gt; 宏里的不一样，另外还不支持嵌套。不知现在这些问题解决了没。我想总归是解决了，否则不会就此放出来。（关于这一点，希望懂行的朋友能给个指正）&lt;/p&gt;

&lt;p&gt;那么对比一下，如果用 meta-api 如何实现呢？是不是会更好：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BasicAPI&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;MetaAPI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/statuses'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="s1"&gt;'Statuses index'&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entities&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Status&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entities&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Status&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;statuses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
      &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'full'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;statuses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="n"&gt;scope&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;不是有意要比较的意思，但总归消除别扭的。比如 Grape 写法里那个奇怪的 &lt;code&gt;requires :all&lt;/code&gt; 参数。而且正因为如此，它要多写一句 &lt;code&gt;desc&lt;/code&gt; 宏在里面声明 &lt;code&gt;params&lt;/code&gt; 的正确文档。&lt;/p&gt;

&lt;p&gt;Grape 是把它的所有的优秀才华都放在参数的定义上面了，而将实体渲染的部分留给第三方插件去完成。归根结底，它还是将自己看作一个 API 实现框架，而不是一个 API 设计框架。&lt;/p&gt;

&lt;p&gt;但是，如果仅仅说实现，Rails API 又哪里有问题呢？&lt;/p&gt;
&lt;h3 id="Rails API"&gt;Rails API&lt;/h3&gt;
&lt;p&gt;在更早期的时候，我一直用的是 Rails API only. Rails API 能够集成 Rails 所能提供的几乎所有组件，这是它的最大优势。它唯一的缺点就是没有文档。当然，你可以通过插件生成文档，但这依然是两茬罪的事情。&lt;/p&gt;

&lt;p&gt;其实用 Rails API 挺自由的。如果不考虑文档的约束，只管实现的话，任何框架都挺自由的，Rails API 更甚。但是，我的理念是，开发 API 就得提供文档。不提供只是太麻烦了而已。但如果生成文档不麻烦了呢？比如 meta-api.&lt;/p&gt;

&lt;p&gt;使用 Rails API 时，一定要理解 MVC 思想。很多人会觉得 Rails API 没有参数验证，没有实体渲染。其实它是有的，你要按照框架的思维去思考。比如，参数验证，你可以在 model 里面定义。实体渲染，你要用到 JBuilder.&lt;/p&gt;

&lt;p&gt;就拿一个登录接口为例，它接受参数 &lt;code&gt;email&lt;/code&gt; 和 &lt;code&gt;password&lt;/code&gt;，返回 &lt;code&gt;token&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Login&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;login_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&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;class&lt;/span&gt; &lt;span class="nc"&gt;Login&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&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;API&lt;/span&gt;

  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;

  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:email&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;:password&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;valid?&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;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&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="c1"&gt;# JBuilder&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token&lt;/span&gt; &lt;span class="vi"&gt;@token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就好像 &lt;code&gt;ActiveRecord&lt;/code&gt; 一样，你可以在 &lt;code&gt;ActiveModel&lt;/code&gt; 里面做各种验证。而编程式的 JBuilder 渲染，就像 erb 一样灵活。&lt;/p&gt;

&lt;p&gt;只不过是没有文档而已，如果只是自产自销的项目，没有文档无可厚非（只不过这个时候，你用 Rails 全栈似乎是更好的选择，彻底避免了两茬罪的问题）。&lt;/p&gt;
&lt;h3 id="meta-api"&gt;meta-api&lt;/h3&gt;
&lt;p&gt;meta-api 的核心思想是：设计即实现。截止如今 0.2.0 版本，我估摸着实现了 80% 以上了吧。它也从根本上解决了两茬罪的问题，因为参数和返回值里用到的实体是相通的。也就是说，如何定义参数的，你也就用同样的方法定义返回值的字段。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;有关 meta-api 的文档，主要看两个：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/yetrun/web-frame/blob/26ca24e8426d3a4c43eda1e165c0e412f4443659/docs/%E6%95%99%E7%A8%8B.md" rel="nofollow" target="_blank" title=""&gt;教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/yetrun/web-frame/blob/26ca24e8426d3a4c43eda1e165c0e412f4443659/docs/%E5%AD%97%E6%AE%B5%E5%9C%BA%E6%99%AF%E5%8C%96.md" rel="nofollow" target="_blank" title=""&gt;字段场景化&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id="静态场景化"&gt;静态场景化&lt;/h2&gt;
&lt;p&gt;meta-api 与其他的框架最不同的地方：静态场景化。因为最考虑文档的优先性，因此才会有这种考虑，用静态的语法声明字段的场景，这样才最可能生成对应的文档。&lt;/p&gt;

&lt;p&gt;场景是个通用的说法。这其中包括哪些字段用于参数，哪些字段用于返回值；哪些字段用于这个接口，哪些字段用于这个接口。meta-api 提供了静态声明这类场景的方式，这也就意味着这些差异文档中可以体现出来。&lt;/p&gt;

&lt;p&gt;一个接口，不应该接受一个实体的所有字段，同样也不应该返回所有字段。这是为了安全性。以 User 字段为例：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:profile&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，&lt;code&gt;id&lt;/code&gt; 字段不应该作为参数，主键不能被修改；&lt;code&gt;password&lt;/code&gt; 字段不应该返回，密码不能被泄露。&lt;/p&gt;

&lt;p&gt;更复杂的情况下，针对不同的接口应返回不同的字段。&lt;code&gt;profile&lt;/code&gt; 字段，只有当 &lt;code&gt;/users/:id&lt;/code&gt; 这样的详情类的接口才返回，而不应该在 &lt;code&gt;/users&lt;/code&gt; 这样的列表类的接口返回。&lt;/p&gt;

&lt;p&gt;meta-api 提供了 &lt;code&gt;scope&lt;/code&gt; 方法，用于定义不同的场景。这样，我们就可以在不同的接口中声明不同的场景。更为要紧的事，这样的声明体现在文档中，不会出错，不会造成歧义。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;
    &lt;span class="c1"&gt;# 可以在这里定义更多的参数字段&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 仅用于参数&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;
    &lt;span class="c1"&gt;# 可以在这里定义更多的返回字段&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
  &lt;span class="c1"&gt;# 可以在这里定义更多的基本字段，这些字段在任何情况下都会用到&lt;/span&gt;

  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="no"&gt;Detail&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:profile&lt;/span&gt;
    &lt;span class="c1"&gt;# 可以在这里定义更多的 Detail 场景字段&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="no"&gt;Admin&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;
    &lt;span class="c1"&gt;# 可以在这里定义更多的 Admin 场景字段&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;blockquote&gt;
&lt;p&gt;我写了这么一个文档：&lt;a href="https://github.com/yetrun/web-frame/blob/26ca24e8426d3a4c43eda1e165c0e412f4443659/docs/%E5%AD%97%E6%AE%B5%E5%9C%BA%E6%99%AF%E5%8C%96.md" rel="nofollow" target="_blank" title=""&gt;字段场景化&lt;/a&gt;，这里面举出了常用的场景示例。这里仅给出一个小小的窥探吧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="最后想说的话"&gt;最后想说的话&lt;/h2&gt;
&lt;p&gt;我真心感受到一点，国内的短平快开发环境下大多数开发者不认真去做文档。文档这玩意，有就行，有时候语义和实现不一致也懒得改，反正微信聊天记录有。这种情况下，meta-api 似乎有点多余。它所提供的自动一致性文档功能，也并被大家所认可。&lt;/p&gt;

&lt;p&gt;然后，国内的 Ruby 论坛本身也快变成了招聘和瞎扯淡的地方，技术交流的地方越来越少。&lt;/p&gt;

&lt;p&gt;只不过，用过的人应该会感受到，meta-api 的确是一个好东西。它的设计思想是对的，只不过它的实现方式目前可能不对，但没有反馈我也不清楚。只不过，它的应用场景太小了，太小了。就像双拼输入法的感觉那样，没用之前不知所云，用了之后真是舒服。&lt;/p&gt;</description>
      <author>yetrun</author>
      <pubDate>Fri, 28 Jun 2024 11:04:40 +0800</pubDate>
      <link>https://ruby-china.org/topics/43779</link>
      <guid>https://ruby-china.org/topics/43779</guid>
    </item>
    <item>
      <title>meta-api: 让后端开发者不用再写文档</title>
      <description>&lt;p&gt;meta-api 框架历经长时间的迭代，终于在今天发布了第一个稳定的版本 &lt;code&gt;0.1&lt;/code&gt; 版了。借此机会，我再次向 Ruby 社区发表下这个项目，如有打扰之处还请见谅。这次的文章比较长，不知道 Ruby China 论坛支不支持发表长文章，会不会产生字数限制导致截断的情况。也许长文章会劝退某些 Ruby 伙伴，没事那就跳着看到最后吧。这次我将介绍下我对后端 API 开发的看法，介绍 meta-api 框架文档化支持的能力和它的与众不同之处。这次我还将它与 Rails、Grape 框架做个简单的比较。当然，欢迎大家来交流，我在文末列出交流的途径，希望能与有志同道合的同志一起讨论，当然更希望大家一起参与建设。&lt;/p&gt;
&lt;h2 id="目录"&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="#%E7%9B%AE%E5%BD%95" title=""&gt;meta-api: 让后端开发者不用再写文档&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E8%83%8C%E6%99%AF%E7%AE%80%E4%BB%8B" title=""&gt;背景简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="#meta-api-%E5%B7%A5%E5%85%B7%E7%AE%80%E4%BB%8B" title=""&gt;meta-api 工具简介&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E5%86%99%E4%BB%A3%E7%A0%81%E6%97%B6%E5%90%8C%E6%AD%A5%E4%BA%A7%E5%87%BA%E6%96%87%E6%A1%A3" title=""&gt;写代码时同步产出文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%96%87%E6%A1%A3%E4%B8%8E%E6%8E%A5%E5%8F%A3%E7%9A%84%E9%80%BB%E8%BE%91%E6%98%AF%E4%B8%80%E8%87%B4%E7%9A%84" title=""&gt;文档与接口的逻辑是一致的&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%B8%80%E5%A4%84%E7%BC%96%E5%86%99%E5%88%B0%E5%A4%84%E4%BD%BF%E7%94%A8" title=""&gt;一处编写，到处使用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%A1%86%E6%9E%B6%E4%B8%8E-rails-%E5%92%8C-grape-%E7%9A%84%E6%AF%94%E8%BE%83" title=""&gt;框架与 Rails 和 Grape 的比较&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="#%E6%A1%86%E6%9E%B6%E5%BC%80%E5%8F%91%E8%BF%9B%E5%B1%95%E6%83%85%E5%86%B5" title=""&gt;框架开发进展情况&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E5%A6%82%E4%BD%95%E8%AF%95%E7%94%A8%E5%92%8C%E4%B8%8A%E6%89%8B" title=""&gt;如何试用和上手&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%B8%8A%E7%BA%BF%E7%9A%84%E9%A1%B9%E7%9B%AE%E6%83%85%E5%86%B5" title=""&gt;上线的项目情况&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="#%E5%9C%BA%E6%99%AF%E4%B8%BE%E4%BE%8B" title=""&gt;场景举例&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E6%8E%A5%E5%8F%A3%E7%9A%84%E8%AF%AD%E4%B9%89%E5%92%8C%E8%81%8C%E8%B4%A3" title=""&gt;接口的语义和职责&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%8C%BA%E5%88%86%E5%8F%82%E6%95%B0%E5%92%8C%E8%BF%94%E5%9B%9E%E5%80%BC" title=""&gt;区分参数和返回值&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E8%AF%A6%E6%83%85%E6%8E%A5%E5%8F%A3%E8%BF%94%E5%9B%9E%E5%AE%8C%E6%95%B4%E5%AD%97%E6%AE%B5%E5%88%97%E8%A1%A8%E6%8E%A5%E5%8F%A3%E8%BF%94%E5%9B%9E%E6%A6%82%E8%A6%81%E5%AD%97%E6%AE%B5" title=""&gt;详情接口返回完整字段，列表接口返回概要字段&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%88%9B%E5%BB%BA%E5%92%8C%E6%9B%B4%E6%96%B0%E5%8A%A8%E4%BD%9C%E5%BA%94%E7%94%A8%E4%B8%8D%E5%90%8C%E7%9A%84%E5%8F%82%E6%95%B0" title=""&gt;创建和更新动作应用不同的参数&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%94%A8%E5%B1%80%E9%83%A8%E6%9B%B4%E6%96%B0%E8%BF%98%E6%98%AF%E5%85%A8%E5%B1%80%E6%9B%BF%E6%8D%A2" title=""&gt;用局部更新还是全局替换&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%9C%BA%E6%99%AF%E6%98%AF%E5%8F%AF%E4%BB%A5%E5%90%91%E4%B8%8B%E4%BC%A0%E9%80%92%E7%9A%84" title=""&gt;场景是可以向下传递的&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%8A%A8%E6%80%81-if" title=""&gt;动态 if:&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%9B%B4%E5%A4%9A%E7%9A%84%E4%BE%8B%E5%AD%90" title=""&gt;更多的例子&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="#%E7%AF%87%E5%B0%BE" title=""&gt;篇尾&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80" title=""&gt;项目地址&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%BD%9C%E4%B8%BA-rails-%E7%9A%84%E6%8F%92%E4%BB%B6" title=""&gt;作为 Rails 的插件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%B8%8C%E6%9C%9B%E8%83%BD%E5%BE%97%E5%88%B0%E6%9D%A5%E8%87%AA%E6%9B%B4%E5%A4%9A%E4%BC%99%E4%BC%B4%E7%9A%84%E6%94%AF%E6%8C%81" title=""&gt;希望能得到来自更多伙伴的支持&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%AC%A2%E8%BF%8E%E5%A4%A7%E5%AE%B6%E8%BF%9B%E7%BE%A4%E5%94%A0%E5%97%91" title=""&gt;欢迎大家进群唠嗑&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="背景简介"&gt;背景简介&lt;/h2&gt;
&lt;p&gt;现在前后端分离已经成为了 Web 项目开发的主要模式。一个项目组内分为前端开发人员和后端开发人员，后端开发者负责产出 Restful 接口，前端开发者通过调用接口获取数据或操作。在前后端交流接口的过程中，在写 API 文档上，后端开发者一般持有三种态度：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;不想写文档，压根不打算产出 API 文档；&lt;/li&gt;
&lt;li&gt;愿意写文档，但产出的文档很粗糙，经常跟接口的实现对不上；&lt;/li&gt;
&lt;li&gt;很细致地写文档，产出的文档也很好，但写文档的过程非常地繁琐，也有少部分与接口的实现对不上的情况发生。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我没有完全调研大家编写文档的方式，据我所知有单独写一个 Markdown 文件的，有用 ApiFox 之类可视化工具的，有用代码注释或注解产生 API 文档的，不一一列举了。但这些工具都有一个共同点：&lt;strong&gt;分心&lt;/strong&gt;。程序员在百忙之中不得不抽出相当一部份时间专门处理文档的事情，让自己的工作在写文档和写代码之间不断地跳跃。&lt;/p&gt;

&lt;p&gt;除了写文档的繁琐之外，国内开发者对待文档的浮躁也是一个方面。项目组内往往是铺天盖地的讨论声，前端一会儿问问后端这个接口怎么用，那个接口怎么会报错？后端也乐于解答这些问题。他们在开发中往往不去参考文档。有些项目组可能会生产文档，但是如果文档的表达能力不够好，开发过程中经常地弃之不用，那么即使产出了文档也起不到一丁点作用。&lt;/p&gt;

&lt;p&gt;项目组到底需不需要写文档呢？如果你的回答是不需要，那也许你找到了适合你们项目组开发的最佳途径。但总体上，生产 API 文档至少有几个好处：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;减少前后端联调和沟通的成本，当然前提是文档的质量要足够好。&lt;/li&gt;
&lt;li&gt;从 API 文档了解项目的设计结构，文档可作为项目组沟通的媒介。&lt;/li&gt;
&lt;li&gt;如果你开发的是一个开放 API 项目，API 文档不可或缺。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我一直在想，让人们讨论要不要产出文档这个话题，是不是因为生产文档的额外工作让人厌烦。所以，如果文档的工作足够简单，成为了开发代码自然产生的附属品，那么纠结它重不重要的话题还有意义吗？&lt;/p&gt;
&lt;h2 id="meta-api 工具简介"&gt;meta-api 工具简介&lt;/h2&gt;
&lt;p&gt;这篇文章阐述了一个想法，介绍了一种工具，或者是一个框架，目的是为了让后端开发者简化写文档的工作。甚至达到一个终点，后端开发者不用再写文档了，因为文档就在那里，就在你的代码里。&lt;/p&gt;

&lt;p&gt;这里介绍的一个新的框架，是我历时一年半左右的大大小小的时间里积累而完成的。现在我认为它可以达到发布一个较为稳定的版本时机了，至少从实践上我探索出了代码同步文档的可行性。总结起来，它最大的特点是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;写代码时同步产生文档（结合配套的 OpenAPI 工具基本达到实时的效果），开发者不用再单独考虑文档的生成问题；&lt;/li&gt;
&lt;li&gt;文档与接口的逻辑是一致的，包括文档中定义了几个字段，接口就接受几个字段，不多也不少；&lt;/li&gt;
&lt;li&gt;只需要在一个地方定义实体，就可以到处使用，包括参数中、返回中、列表接口、详情接口，其中提供了一个简单的机制在以上不同的场景下产生不同的字段。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="写代码时同步产出文档"&gt;写代码时同步产出文档&lt;/h3&gt;
&lt;p&gt;我录了一个视频，我在左边写代码，等我把代码写完之后，右边就实时出现更新后的文档了：&lt;/p&gt;


  


&lt;p&gt;发布后发现 Ruby China 不能直接显示视频，大家伙可以&lt;strong&gt;点击下面的链接访问视频&lt;/strong&gt;：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="http://assets.blog.yet.run/ruby-china/web-frame-0.1/%E5%90%8C%E6%AD%A5%E7%94%9F%E6%88%90%E6%96%87%E6%A1%A3.mp4" rel="nofollow" target="_blank" title=""&gt;同步生成文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;左边是一个 Vim 编辑器，右边同步产生文档。当我们在代码中将接口的语义用各种宏命令定义好后，保存文件，右边的 API 文档预览同步生成更新后的效果。所以，你完全可以一边写代码一边检查文档；或者当你写完代码后统一检查文档，哪里需要调整的再回过头去调整即可。&lt;/p&gt;
&lt;h3 id="文档与接口的逻辑是一致的"&gt;文档与接口的逻辑是一致的&lt;/h3&gt;
&lt;p&gt;&lt;img src="http://assets.blog.yet.run/ruby-china/web-frame-0.1/%E8%A7%86%E9%A2%91%E6%88%AA%E5%9B%BE.png" alt="创建用户" width="80%"&gt;&lt;/p&gt;

&lt;p&gt;其实当你看到视频的最后，再截一张图时，就可以发现端倪了。编辑器里的代码，都能对应上 OpenAPI 文档里的一部份。甚至是字段都能对应上。仔细看右边的文档，同样引用的 &lt;code&gt;ArticleEntity&lt;/code&gt; 这个实体，但在文档显示时却能正确地说明参数包括 &lt;code&gt;title&lt;/code&gt;、&lt;code&gt;content&lt;/code&gt; 字段，渲染返回时包括 &lt;code&gt;id&lt;/code&gt;、 &lt;code&gt;title&lt;/code&gt;、&lt;code&gt;content&lt;/code&gt; 三个字段。&lt;/p&gt;
&lt;h3 id="一处编写，到处使用"&gt;一处编写，到处使用&lt;/h3&gt;
&lt;p&gt;还是这个例子，你通篇看到对 &lt;code&gt;ArticleEntity&lt;/code&gt; 的引用。其实，对于文章这个实体的参数和响应值，我就写在了这一个类里，然后就几乎用在所有的地方，包括参数、返回值，也包括列表接口、详情接口。所谓&lt;em&gt;一处编写，到处使用&lt;/em&gt;，你将享受到统一管理的便利。&lt;/p&gt;
&lt;h3 id="框架与 Rails 和 Grape 的比较"&gt;框架与 Rails 和 Grape 的比较&lt;/h3&gt;
&lt;p&gt;首先，meta-api 是纯 API 框架，接受 JSON 参数，生产 JSON 实体；Rails 是全栈框架。它们的应用场景不一样。全栈开发（也就是通常所说的一个人单撸整个项目），适合用 Rails 开发，中间没必要生产 API 文档。前后端分离的开发，中间需要生产 API 文档，适合 meta-api 开发。&lt;/p&gt;

&lt;p&gt;Rails 有纯 API 模式，但仍是全栈的思维（MVC），我个人认为开发起来还是有诸多不便。而且，它不能生产文档。通过 rswag 生产的文档，没有一致性可言。你完全可以在文档中写是一套，在实际逻辑中运行时又是一套。&lt;/p&gt;

&lt;p&gt;Grape 是纯 API 框架，也能生产 Swagger 文档。但它不是面向文档优先的框架，一致性做得不足。它在参数方面的文档一致性做得很好，但在渲染方面却欠佳。而且，它无法做到一处编写到处使用，参数定义和返回值定义用了两套不同的语法。&lt;/p&gt;

&lt;p&gt;meta-api 框架天生地就是面向文档优先的，所以目前来说，一致性方面做得最足。这是它与其他框架目前的最大区别。如果你是 Rails 全栈开发者，暂时用不到 meta-api，保持原有技术栈就好。如果你是 Grape 框架使用者，我真心建议你试用下 meta-api 框架，它已经有了 Grape 框架大约 80% 的同等表达能力；而且，它还是一个文档友好的独一无二的框架。&lt;/p&gt;
&lt;h2 id="框架开发进展情况"&gt;框架开发进展情况&lt;/h2&gt;&lt;h3 id="如何试用和上手"&gt;如何试用和上手&lt;/h3&gt;
&lt;p&gt;首先，它的上手难度不见得比 Grape 高，因为二者都是啥也不依赖的微型框架。最简单也是最好的上手方案，直接使用我提供的脚手架：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/yetrun/web-frame-example" rel="nofollow" target="_blank"&gt;https://github.com/yetrun/web-frame-example&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;请按照它的说明文档一步一步去做就好。如果需要访问 API 文档，可使用我的 OpenAPI 工具作实时预览。在浏览器地址栏输入：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="http://openapi.yet.run/playground" rel="nofollow" target="_blank"&gt;http://openapi.yet.run/playground&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;然后内里还有一个地址栏，输入 &lt;code&gt;ws://localhost:9292/api_spec&lt;/code&gt; 就好。（前提是项目先用 Rackup 启动了）&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关于脚手架的说明：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这是一个纯 Ruby 脚手架，没有用到 Rails. 在使用微型框架时，没有必要引入 Rails，只需要基于 Rack 即可。这个脚手架除了可用于 meta-api 之外，也可以用于 Grape、Sinatra 等，只需要将 meta-api 的部分对应地替换掉即可。&lt;/p&gt;

&lt;p&gt;我么摒弃 Rails 的 MVC，不是说要完全摒弃 Rails. 像 ActiveRecord，放在哪里都能用的，而且本身也可作为组件拆分出来单独使用。脚手架集成了几个好用的组件，像 ActiveRecord、Pundit、FactoryBot 等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="上线的项目情况"&gt;上线的项目情况&lt;/h3&gt;
&lt;p&gt;由于知名度真的很低，再加上我很少宣传，现在项目的使用情况很少。主要是包括我公司的项目，和我业余开发的两个项目：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://openapi.yet.run" rel="nofollow" target="_blank"&gt;http://openapi.yet.run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://stock-app.yet.run" rel="nofollow" target="_blank"&gt;http://stock-app.yet.run&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="场景举例"&gt;场景举例&lt;/h2&gt;&lt;h3 id="接口的语义和职责"&gt;接口的语义和职责&lt;/h3&gt;
&lt;p&gt;可以这么说，meta-api 这个框架是我这么多年接口开发的理念的总结，其中蕴涵了一些我对接口职责的看法。有些东西通过产品是看不出来的，但通过接口可能暴露出来，从而给系统带来隐患。如果我们一直以一种不负责任、能用即用的态度开发接口，那么系统的安全问题迟早会暴露出来。&lt;/p&gt;

&lt;p&gt;举个简单的例子，对于返回用户信息这样的接口（&lt;code&gt;GET /user&lt;/code&gt;），密码这个字段不应该在返回实体内列出来的。如果我们只是简单地使用类似&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;render&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;to_json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的代码，就会暴露出 &lt;code&gt;user&lt;/code&gt; 对象实例能够暴露的所有字段，这样是不安全的。因而，对返回字段的控制是一个框架应该考虑的事情。&lt;/p&gt;

&lt;p&gt;但仅仅这样也不够，如果能很好地和文档结合就让事情更加友好了。&lt;strong&gt;如果一个接口返回这个字段，我们就在文档中包含它；如果一个接口不返回这个字段，我们就在文档中去掉它。&lt;/strong&gt;这样，我们只要扫一眼接口文档，就可以知道接口的参数和返回值定义得对或不对，是否合理，有没有隐患等等。无论是后端开发者、前端开发者、系统安全员、架构师都可以通过用与实际代码逻辑一致的接口文档进行交流。&lt;/p&gt;

&lt;p&gt;meta-api 框架就是天生为这些使用场景而考虑的。具体来讲，是框架内提供的 &lt;code&gt;JsonSchema&lt;/code&gt; 模块来定义接口的实体，包括参数和响应值。例如，对于上面的 &lt;code&gt;GET /user&lt;/code&gt; 接口，只需要在字段定义处简单地加上标记 &lt;code&gt;render: false&lt;/code&gt; 就可以在返回时去掉 &lt;code&gt;password&lt;/code&gt; 字段了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;param: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'用户名'&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'密码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;render: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;meta-api 框架的 &lt;code&gt;JsonSchmea&lt;/code&gt; 模块在设计之初，就秉承了两个理念：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;参数和返回值统一定义；&lt;/li&gt;
&lt;li&gt;提供了场景的概念，实体统一定义，使用时通过场景予以区分。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样做只为了一个效果，不作重复性的劳动。就是这样，meta-api 不仅让开发者避免写文档，也要避免开发过程中的无谓的重复劳动。做到了这样，就更有效地保证了接口逻辑与文档的一致性。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;篇幅有限，说不了太多。JsonSchema 其实是一个很完备的模式定义工具，它支持各种 JSON 类型以及深层次的嵌套，还包括类型转换、数据验证、当前环境访问等特性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="区分参数和返回值"&gt;区分参数和返回值&lt;/h3&gt;
&lt;p&gt;上面的 &lt;code&gt;UserEntity&lt;/code&gt; 实体已经给出了这个示例了。我们在实体定义中，注意到 &lt;code&gt;id&lt;/code&gt; 字段，我们不让它作为参数；&lt;code&gt;password&lt;/code&gt; 字段，我们不让它作为返回值。这样，无论是参数定义还是实体定义，我们都能引用它了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/users'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&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;img src="http://assets.blog.yet.run/ruby-china/web-frame-0.1/a.%E5%88%9B%E5%BB%BA%E7%94%A8%E6%88%B7%E7%9A%84%E5%8F%82%E6%95%B0%E5%92%8C%E8%BF%94%E5%9B%9E%E5%80%BC.png" alt="创建用户" width="50%"&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这么做保护了你的数据。将 &lt;code&gt;id&lt;/code&gt; 字段从参数里去除，不会因为偶然因素导致 &lt;code&gt;id&lt;/code&gt; 被莫名地更改。将 &lt;code&gt;password&lt;/code&gt; 字段从返回字段里去除，防止别人通过接口窥探到隐私数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="详情接口返回完整字段，列表接口返回概要字段"&gt;详情接口返回完整字段，列表接口返回概要字段&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;详情接口和列表接口返回的字段应该是不一样的。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;例如查看文章列表 &lt;code&gt;GET /articles&lt;/code&gt;，它的返回消息体可能是：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"articles"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Article One"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Article Two"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Article Three"&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="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;GET /articles/1&lt;/code&gt;，它的返回消息体应包括富文本内容，也就是 &lt;code&gt;content&lt;/code&gt; 字段：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"article"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Article One"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A long article content..."&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;做到这一点我们也只需要定义一种实体，但要借助场景（&lt;code&gt;scope&lt;/code&gt;）辅助实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'details'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scope&lt;/code&gt; 选项定义需要外部提供 &lt;code&gt;details&lt;/code&gt; 场景才能用到这个字段。外部提供场景？额，是的……&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/articles'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="s1"&gt;'返回文章列表'&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:articles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ArticleEntity&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;code&gt;GET /articles&lt;/code&gt;）啥都没干，因此按照默认条件它是无法得到 &lt;code&gt;content&lt;/code&gt; 字段的。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://assets.blog.yet.run/ruby-china/web-frame-0.1/b.%E8%BF%94%E5%9B%9E%E6%96%87%E7%AB%A0%E5%88%97%E8%A1%A8.png" alt="返回文章列表" style="width:50%;"&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/articles/:id'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="s1"&gt;'details'&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="s1"&gt;'返回文章详情'&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ArticleEntity&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;code&gt;GET /articles/:id&lt;/code&gt;）定义了 &lt;code&gt;scope&lt;/code&gt; 为 &lt;code&gt;details&lt;/code&gt;，则与 &lt;code&gt;ArticleEntity&lt;/code&gt; 内部定义不谋而合，这种场景下它会返回 &lt;code&gt;content&lt;/code&gt; 字段：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://assets.blog.yet.run/ruby-china/web-frame-0.1/b.%E8%BF%94%E5%9B%9E%E6%96%87%E7%AB%A0%E8%AF%A6%E6%83%85.png" alt="返回文章详情" style="width:50%;"&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这样做的考虑是保证接口限制消息体的大小。如果一个消息体过大，它会过度地占用带宽从而影响到服务器的响应能力。我亲自遇到过一个接口包含了富文本的详情后导致系统卡顿不能响应的真实情况。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;场景是什么？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;场景（&lt;code&gt;scope&lt;/code&gt;）是 &lt;code&gt;JsonSchema&lt;/code&gt; 模块的核心理念，它是静态生成文档能力的最主要工具。因此，它的功能是很强大的。你可以在接口层定义 &lt;code&gt;scope&lt;/code&gt;，它同时应用到参数和返回值；你也可以在参数和返回值上单独应用 &lt;code&gt;scope&lt;/code&gt;；上层的 &lt;code&gt;scope&lt;/code&gt; 会继承到下层（试想一下在 &lt;code&gt;/admin&lt;/code&gt; 空间的顶层定义一个 &lt;code&gt;scope: 'admin'&lt;/code&gt; 之后会有什么妙用吧）；你甚至可以在运行时传递 &lt;code&gt;scope&lt;/code&gt;（但我不建议这么做，这会让它失去文档正确的表达能力）……总之，有什么问题，来问我吧。&lt;/p&gt;
&lt;h3 id="创建和更新动作应用不同的参数"&gt;创建和更新动作应用不同的参数&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;你知道吗？你的接口在不同的场景下请求参数的字段应该是不一样的。因此，无脑接受一样的参数类型，而要求前端来区分和传递真的是正确的习惯吗？&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;现在我说一下场景，还是 &lt;code&gt;UserEntity&lt;/code&gt;，我们要求创建用户时包含 &lt;code&gt;password&lt;/code&gt; 这个字段，而在更新时不要包含这个字段。现实是很合理的，我们一般把修改密码放在另一个接口去做。&lt;/p&gt;

&lt;p&gt;这次我反过来，先定义接口并列举出场景，再来写 &lt;code&gt;UserEntity&lt;/code&gt;. 我们定义两个接口，创建用户和更新用户：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/users'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="s1"&gt;'create'&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="s1"&gt;'创建用户'&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;put&lt;/span&gt; &lt;span class="s1"&gt;'/user'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="s1"&gt;'update'&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="s1"&gt;'更新用户'&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&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;code&gt;password&lt;/code&gt; 参数，更新用户接口没有 &lt;code&gt;password&lt;/code&gt; 参数。&lt;/p&gt;


  &lt;img src="http://assets.blog.yet.run/ruby-china/web-frame-0.1/c.%E5%88%9B%E5%BB%BA%E7%94%A8%E6%88%B7.png" alt="创建用户" width="50%"&gt;
  &lt;img src="http://assets.blog.yet.run/ruby-china/web-frame-0.1/c.%E6%9B%B4%E6%96%B0%E7%94%A8%E6%88%B7.png" alt="更新用户" width="50%"&gt;


&lt;p&gt;定义 &lt;code&gt;UserEntity&lt;/code&gt; 实体时，我们只需要根据 &lt;code&gt;scope&lt;/code&gt; 来规定字段即可。为了避免干扰，我们只关注 &lt;code&gt;password&lt;/code&gt; 字段怎么定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;param: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'create'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;render: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scope: 'create'&lt;/code&gt; 我们已经熟悉了，它只接受创建接口。我们还将这一切包装在 &lt;code&gt;param&lt;/code&gt; 选项下，因为只有作为参数时我们才做这样的区分。请记住，你可以为参数和返回两种情况定义不同的 &lt;code&gt;scope&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;meta-api 框架为每个路由的方法自动创建了 scope，可以直接在实体中引用。例如，&lt;code&gt;UserEntity&lt;/code&gt; 中的 &lt;code&gt;password&lt;/code&gt; 可以直接改为：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'$post'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，因为创建用户的接口是 &lt;code&gt;POST&lt;/code&gt;，其自动传递 &lt;code&gt;scope: '$post'&lt;/code&gt;，因此，不需要在路由定义 &lt;code&gt;scope 'create'&lt;/code&gt; 也能让用例跑得通。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="用局部更新还是全局替换"&gt;用局部更新还是全局替换&lt;/h3&gt;
&lt;p&gt;如果你了解 &lt;code&gt;PUT&lt;/code&gt; 和 &lt;code&gt;PATCH&lt;/code&gt; 的语义区别，你就知道什么是局部更新，什么是全局替换。&lt;code&gt;meta-api&lt;/code&gt; 可以在声明时定义解析参数时使用局部更新还是全局替换的，默认情况是全局替换。&lt;/p&gt;

&lt;p&gt;为实体调用 &lt;code&gt;locked(discard_missing: true)&lt;/code&gt;，就将行为调整为局部更新：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;put&lt;/span&gt; &lt;span class="s1"&gt;'/articles/:id'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ArticleEntity&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="s1"&gt;'/articles/:id'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ArticleEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;discard_missing: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&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;在实际调用中，如果传递的参数是（JSON）&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"article"&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="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"New Article"&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;那么 &lt;code&gt;PUT&lt;/code&gt; 方法获取的参数是（Ruby）&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;article:&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;title:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"New Article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;content:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nil&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;&lt;code&gt;PUT&lt;/code&gt; 方法获取的参数是（Ruby）&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;article:&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;title:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"New Article"&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;blockquote&gt;
&lt;p&gt;Meta 框架支持为全局情况使用 &lt;code&gt;discard_missing: true&lt;/code&gt; 的配置，这样就可以避免在每个路由参数引用实体底下设置 &lt;code&gt;locked(discard_missing: true)&lt;/code&gt; 的调用了。不过我还是喜欢做下区分，即创建类的接口用 &lt;code&gt;discard_missing: false&lt;/code&gt;，更新类的接口用 &lt;code&gt;discard_missing: true&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="场景是可以向下传递的"&gt;场景是可以向下传递的&lt;/h3&gt;
&lt;p&gt;这里举出一个例子，将 &lt;code&gt;/admin&lt;/code&gt; 锚点下的所有接口都添加一个共同的 scope，这样挂载在它下面的所有接口都能应用这个 scope，实现一些共同的目的。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="s1"&gt;'/admin'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/admin/dashboard'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# 将直接应用 'admin' 场景&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 通过模块引入的接口也能继承场景 'admin'&lt;/span&gt;
  &lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Users&lt;/span&gt;
  &lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Articles&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们可以实现一些特殊的需求，比如只有 &lt;code&gt;admin&lt;/code&gt; 情况下才能操作和看到的字段：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'是否发布上线'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="动态 if:"&gt;动态 &lt;code&gt;if:&lt;/code&gt;
&lt;/h3&gt;
&lt;p&gt;拥有和 Grape Entity 提供的类似的 &lt;code&gt;if:&lt;/code&gt; 选项，用于动态渲染字段。&lt;/p&gt;

&lt;p&gt;接着上面的例子，如果你将管理员的接口和普通群众的接口放在同一个锚点下（比如都在 &lt;code&gt;/articles&lt;/code&gt; 下），希望根据身份决定是否渲染字段，这个时候 &lt;code&gt;if:&lt;/code&gt; 选项将派上用场了。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'是否发布上线'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，虽然渲染（包括参数解析）是动态的，但文档只能保持静态的能力，其字段将会在 API 文档中一直显示出来。你的文档描述中最好加上一段解释：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                       &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'是否发布上线，仅管理员可操作'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                       &lt;span class="ss"&gt;if: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="更多的例子"&gt;更多的例子&lt;/h3&gt;
&lt;p&gt;我其实想举出更多的例子，但我知道一篇文章能够表达得有限。我其实想说的是，无论你用什么语言，用什么框架，都要小心细致地定义你的接口。让接口正确地表达，永远应该成为你的中心任务。&lt;/p&gt;

&lt;p&gt;我经常在网络上听到什么诸如“能用就行”，“这也不用管，那也不用管”，“Restful 是垃圾”之类的论断。但是，拒绝不需要的参数，隐藏需要保护的信息，和生成与行为一致的文档，真的只是几句潦草的“能用就行”之类的论句就能盖之的吗？我很疑惑。&lt;/p&gt;

&lt;p&gt;如果只是因为做到这些很繁杂，让你觉得时间成本上不划算，我认为那是情有可原。但如果打心眼里排斥正确、精准的 API 表达，我就实在想不通。&lt;/p&gt;

&lt;p&gt;meta-api 框架就是致力于用最简单的方式生产表达一致的 API. 虽然它现在可能不够好，但我相信，随着它的发展和大家的支持，它的表达方式会越来越简单，它的表达结果会越来越一致。&lt;/p&gt;
&lt;h2 id="篇尾"&gt;篇尾&lt;/h2&gt;&lt;h3 id="项目地址"&gt;项目地址&lt;/h3&gt;
&lt;p&gt;说了这么多，我还没有说 meta-api 框架的基本情况。&lt;/p&gt;

&lt;p&gt;它的 GitHub 地址是，希望大家能投上一票（Star）：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/yetrun/web-frame" rel="nofollow" target="_blank"&gt;https://github.com/yetrun/web-frame&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这篇文章我只介绍了 meta-api 框架与文档相关的能力，但这并不代表它仅限于此。作为一个框架，它的基本素养是齐全的，包括路由组织和定义、异常拦截、数据验证等。它还包含了一个非常友好的错误栈提示，让开发者在遇到问题时容易对应到出错的代码上。&lt;/p&gt;

&lt;p&gt;我目前只在国内推广，用中文文档，对国人友好。我希望能补充一下国内 Ruby 框架的空白吧。&lt;/p&gt;
&lt;h3 id="作为 Rails 的插件"&gt;作为 Rails 的插件&lt;/h3&gt;
&lt;p&gt;框架还打算提供一个 Rails 插件的方案，让 Rails 纯 API 开发者享受到 JsonSchema 文档化的便利。使用 Rails 插件的方案，可以复用现有的框架和代码，可作为一个渐进的兼容方案，显得没那么跳脱。&lt;/p&gt;

&lt;p&gt;由于开发初期没有作为主要目标，所支持的配套可能尚不完善。我之前也发布过有关 Rails 插件的方案，没有得到响应和回复。也许 Rails 开发者用的还是全栈的较多吧。有需要的同志可以艾特我，我将加速这方面的进程，后续也可能作为一个主要主题予以开发。&lt;/p&gt;
&lt;h3 id="希望能得到来自更多伙伴的支持"&gt;希望能得到来自更多伙伴的支持&lt;/h3&gt;
&lt;p&gt;现如今的框架开发着实没什么市场了，开发这样的一个项目也是因为我当初有需求，正好当时项目组也需要。因为我们是完全的前后端分离的开发组，而我个人比较讨厌就具体字段情况回答太多的问题，我就这样开发了一个新的框架。&lt;/p&gt;

&lt;p&gt;目前框架的发展受到了限制，而我个人开发确实耗时耗力。如果能得到大家的帮助，那就更好了，也能作为我继续坚持下去的动力。当然，&lt;strong&gt;我更希望有人能和我一起开发，共同维护一个项目。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;加入的途径不限，你可以提出 Issue，对&lt;a href="https://github.com/yetrun/web-frame/blob/master/docs/%E6%95%99%E7%A8%8B.md" rel="nofollow" target="_blank" title=""&gt;教程&lt;/a&gt;或代码提出改进意见；或提出 Pull Request，或加群一起讨论提问。&lt;/p&gt;

&lt;p&gt;如果你能使用它并正式投产，像我公司那样，我将不胜感激，并倾尽全力为你解决使用上的任何麻烦。&lt;/p&gt;

&lt;p&gt;我坐标位于上海，如果有机会，可以组织一次线下的讨论和交流。我可以就框架的实现思路，以及框架的适用场景作出分享。&lt;/p&gt;
&lt;h3 id="欢迎大家进群唠嗑"&gt;欢迎大家进群唠嗑&lt;/h3&gt;
&lt;p&gt;针对项目的前景和发展的方向，我热忱地欢迎大家伙一起来聊聊，因为我本人也是迷茫的。我也担心我的路走偏，听听大伙的心声有助于我避免重复的、无用的工作。（群号是 489579810）&lt;/p&gt;

&lt;p&gt;或者使用 GitHub Discussion？看大家觉得哪个地方更好了，可在帖子下留言通知我。&lt;/p&gt;

&lt;p&gt;另外，我很乐意接受应用场景的提供。你可以进群来，提出一些场景，我来答复我的框架是不是能够很好地支持。这样既是帮助了你，也是帮助了我，我将不胜感激。&lt;/p&gt;</description>
      <author>yetrun</author>
      <pubDate>Sat, 05 Aug 2023 17:04:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/43256</link>
      <guid>https://ruby-china.org/topics/43256</guid>
    </item>
    <item>
      <title>开源一个 Gem，为 Rails 生成 Swagger 文档</title>
      <description>&lt;p&gt;现实中为 Rails 添加参数验证的 Gems 很多，添加 Swagger 文档生成的也不少，但能够同时支持参数验证、返回值渲染和 Swagger 文档生成的少之又少，而且将这些能力紧密结合在一起的就几乎没有了。&lt;/p&gt;

&lt;p&gt;大家伙在团队开发中，是否会有过这些困惑：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;缺少一个比较详细的 API 文档，有些甚至没有文档。&lt;/li&gt;
&lt;li&gt;文档和实现往往不一致，明明文档中说有这个字段，结果在实际调用时发现没有这个字段，或者反之。&lt;/li&gt;
&lt;li&gt;文档很难实现规约，比如参数集的字段与返回值的字段需要区分开来，以及不同场景下返回值的字段也应有所区分。所以，现实中往往只能抛出一个大而全的字段表放在 API 文档中。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果有，你就真应该看看我的这个 gem：&lt;a href="https://github.com/yetrun/web-frame" rel="nofollow" target="_blank" title=""&gt;meta-api&lt;/a&gt;，它天生就是为了解决这些问题的。我们把 API 文档看成是前后端开发者都需要遵守的契约，不仅是前端调用错误需要报错，后端使用错误也需要报错。&lt;/p&gt;
&lt;h2 id="初窥一览"&gt;初窥一览&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;温馨提示：&lt;/em&gt;访问仓库 &lt;a href="https://github.com/yetrun/web-frame-rails-example" rel="nofollow" target="_blank" title=""&gt;web-frame-rails-example&lt;/a&gt; 可查看示例项目。你可以现在就去体验，或者任何时候回过头来体验均可。&lt;/p&gt;
&lt;h3 id="安装"&gt;安装&lt;/h3&gt;
&lt;p&gt;要在 Rails 中使用 &lt;code&gt;meta-api&lt;/code&gt;，请遵循以下步骤。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第一步，&lt;/strong&gt;在 Gemfile 中添加：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'meta-api'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后执行 &lt;code&gt;bundle install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第二步，&lt;/strong&gt;在 &lt;code&gt;config/initializers&lt;/code&gt; 目录下创建一个文件，例如 &lt;code&gt;meta_rails_plugin.rb&lt;/code&gt;，并写入：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'meta/rails'&lt;/span&gt;

&lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第三步，&lt;/strong&gt;在 &lt;code&gt;application_controller.rb&lt;/code&gt; 下添加：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;
  &lt;span class="c1"&gt;# 引入宏命令&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Plugin&lt;/span&gt;

  &lt;span class="c1"&gt;# 处理参数验证错误，以下仅是个示例，请根据实际需要编写&lt;/span&gt;
  &lt;span class="n"&gt;rescue_from&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ParameterInvalid&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;e&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :bad_request&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;以上步骤完成后，就可以在 Rails 中编写宏命令定义参数和返回值了。&lt;/p&gt;
&lt;h3 id="编写示例"&gt;编写示例&lt;/h3&gt;
&lt;p&gt;下面编写一个 &lt;code&gt;UsersController&lt;/code&gt;，作为使用宏命令的示例：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="s1"&gt;'创建用户'&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'姓名'&lt;/span&gt;
        &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&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="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'ID'&lt;/span&gt;
        &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'姓名'&lt;/span&gt;
        &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&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;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&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;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params_on_schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json_on_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="mi"&gt;201&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;code&gt;route&lt;/code&gt; 命令定义一个 &lt;code&gt;POST /users&lt;/code&gt; 的路由，并提供了一个代码块，所有的参数定义和返回值定义都在这个代码块内。当这一切定义就绪之后，实际执行时会带来两个效果：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;params_on_schema&lt;/code&gt;：它返回根据定义解析后的参数，比如你如果传递了这么一个参数：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"foo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"foo"&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;它会帮你过滤掉不必要的字段，并且做适度地类型转换，从而让应用内真正获取的参数变成（通过 &lt;code&gt;params_on_schema&lt;/code&gt;）：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&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;这样你就可以放心无虞地将其传递给 &lt;code&gt;User.create!&lt;/code&gt; 方法，不用担心任何错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;json_on_schema&lt;/code&gt;：通过它传递的对象会经由返回值定义解析，因为有字段的过滤、类型的转换和数据的验证，后端可以控制哪些字段返回给前端、如何返回以及在字段出错时得到提醒等。比如你的 users 表包括以下字段：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id, name, age, password, created, updated
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据定义，它只会返回如下字段：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id, name, age
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="生成 Swagger 文档"&gt;生成 Swagger 文档&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;一切皆是以生成 Swagger 文档为核心。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;如果你想要生成文档，请按照我的步骤执行。&lt;/p&gt;

&lt;p&gt;第一步，想好你的 Swagger 文档放哪？比如我新建一个接口用于返回 Swagger 文档：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SwaggerController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eager_load!&lt;/span&gt; &lt;span class="c1"&gt;# 需要提前加载所有控制器常量&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_swagger_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;ApplicationController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;info: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s1"&gt;'Web API 示例项目'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;version: &lt;/span&gt;&lt;span class="s1"&gt;'current'&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;servers: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="s1"&gt;'http://localhost:9292'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'Web API 示例项目'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;doc&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;

&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/swagger_doc'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'swagger#index'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三步，启动应用即可查看到 Swagger 文档的效果，在浏览器内输入：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000/swagger_doc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON 格式的 Swagger 文档映入眼帘。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果你是使用 Swagger 文档的老人了，应该知道接下来怎么做。如果你不知道，请按照如下步骤渲染 Swagger 文档：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;为应用添加跨域支持：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rack-cors'&lt;/span&gt;

&lt;span class="c1"&gt;# config/initializers/cors.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_before&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cors&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;origins&lt;/span&gt; &lt;span class="s1"&gt;'openapi.yet.run'&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;headers: :any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;methods: :all&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;/li&gt;
&lt;li&gt;&lt;p&gt;如果你用的是 Google Chrome 浏览器，需要做一些额外的设置才能支持 localhost 域名的跨域。或者你需要将其挂在一个正常的域名下。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;打开 &lt;a href="http://openapi.yet.run/playground" rel="nofollow" target="_blank"&gt;http://openapi.yet.run/playground&lt;/a&gt;，在输入框内输入 &lt;code&gt;http://localhost:3000/swagger_doc&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;至此，一切初试体验完毕，前端可以获得一份定义明确的文档，后端也是严格按照这个文档执行的。后端开发者不用做额外的工作，它只是定义参数、定义返回值，然后，一切就完备了。&lt;/p&gt;
&lt;h2 id="用不同的场合响应不同的字段"&gt;用不同的场合响应不同的字段&lt;/h2&gt;
&lt;p&gt;这一章节提供了一个比较充分的主题，&lt;code&gt;meta-api&lt;/code&gt; 如何处理不同场合下的字段区分。不同的场合，比如：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;某些字段只用作参数，而某些字段只作为返回值。&lt;/li&gt;
&lt;li&gt;用作返回值时，某些接口返回字段 &lt;code&gt;a, b, c&lt;/code&gt;，而某些接口返回字段 &lt;code&gt;a, b, c, d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;用作参数时同 2，某些接口用到字段 &lt;code&gt;a, b, c&lt;/code&gt;，而某些接口用到字段 &lt;code&gt;a, b, c, d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;如何处理 HTTP 中对 &lt;code&gt;PUT&lt;/code&gt; 和 &lt;code&gt;PATCH&lt;/code&gt; 请求的语义区分。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这里 &lt;code&gt;meta-api&lt;/code&gt; 插件提供了实体的概念，我们可以将字段放到实体里，然后在接口中引用它。实体只用定义一遍，不需要任何的重复定义，并且最重要的是，文档和你的定义能够始终保持一致。请接下去看！&lt;/p&gt;
&lt;h3 id="使用实体简化参数和返回值的定义"&gt;使用实体简化参数和返回值的定义&lt;/h3&gt;
&lt;p&gt;我们将上例中的参数和返回值归纳为为一个实体：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;param: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'姓名'&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'年龄'&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&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;code&gt;id&lt;/code&gt; 属性有一个 &lt;code&gt;param: false&lt;/code&gt; 选项，它表示这个字段只能被用作返回值而不能用作参数。这与之前的定义是一致的。&lt;/p&gt;

&lt;p&gt;除了限定字段只能用作返回值外，也可以限定它只能用作参数。在 &lt;code&gt;UserEntity&lt;/code&gt; 内添加一个 &lt;code&gt;password&lt;/code&gt; 字段作为参数，可以如下定义：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="c1"&gt;# 省略其他字段&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;'密码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;render: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;总之，只用写一遍实体，同时用作参数和返回值。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="如何根据不同场景返回不同的字段"&gt;如何根据不同场景返回不同的字段&lt;/h3&gt;
&lt;p&gt;实际接口运用中，不同的场景返回的字段范畴往往是不同的，那些不分场合一股脑返回同样字段的接口实现是错误的。这里提到的不同的出场景，比方说：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;列表页下返回基本字段，详情页返回更多字段。&lt;/li&gt;
&lt;li&gt;普通用户查看时只能看到一部分字段，管理员查看时可查看全部字段。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些都可以用 &lt;code&gt;meta-api&lt;/code&gt; 提供的 scope 机制加以区分。我仅以列表页接口举例。&lt;/p&gt;

&lt;p&gt;假设 &lt;code&gt;/articles&lt;/code&gt; 接口返回文章列表，每一篇文章只用返回：&lt;code&gt;title&lt;/code&gt; 字段；&lt;code&gt;/articles/:id&lt;/code&gt;  接口需要返回文章详情，每一篇文章需要返回 &lt;code&gt;title&lt;/code&gt;、&lt;code&gt;content&lt;/code&gt; 字段。定义两个实体会显得繁琐加混乱，只用定义一个实体并用 &lt;code&gt;scope:&lt;/code&gt; 选项加以区分：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'full'&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/articles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:articles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ArticleEntity&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;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json_on_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'articles'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/articles/:id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ArticleEntity&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;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json_on_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'article'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'full'&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;code&gt;show&lt;/code&gt; 方法内渲染数据时用到的这一行代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json_on_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'article'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'full'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它传递了一个选项 &lt;code&gt;scope: 'full'&lt;/code&gt;，告诉实体要同时渲染出 &lt;code&gt;scope&lt;/code&gt; 标注为 &lt;code&gt;full&lt;/code&gt; 的字段（也就是 &lt;code&gt;content&lt;/code&gt; 字段）。&lt;/p&gt;

&lt;p&gt;除了在调用时传递特定的选项外，这里还有另外一个技巧，使用 &lt;code&gt;meta-api&lt;/code&gt; 提供的&lt;strong&gt;锁定&lt;/strong&gt;技术将引用的实体锁定在特定的选项上。&lt;/p&gt;

&lt;p&gt;我们修改一下上例 &lt;code&gt;show&lt;/code&gt; 方法的定义和实现，如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/articles/:id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ArticleEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'full'&lt;/span&gt;&lt;span class="p"&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;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json_on_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'article'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="p"&gt;}&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;ol&gt;
&lt;li&gt;在引用实体时，我们对实体作了 &lt;code&gt;ArticleEntity.locked(scope: 'full')&lt;/code&gt;，它返回新的被锁定的实体。&lt;/li&gt;
&lt;li&gt;在渲染数据时，选项 &lt;code&gt;scope: 'full'&lt;/code&gt; 不用再传递了。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不要小看这点小小的变化，我们在定义时锁定实体，实际上是在接口设计阶段就将实体的作用范畴定义好了。我的观点是，设计优先于实现，因此我更推荐第二种方案。此外，锁定还会影响到文档的生成，文档中的实体只会渲染出锁定 &lt;code&gt;scope&lt;/code&gt; 的字段，不会生成不会返回的其他的字段。这对前端查看文档时更友好。&lt;/p&gt;

&lt;p&gt;我们在 &lt;code&gt;ArticleEntity&lt;/code&gt; 内添加一个新的用不上的字段：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'full'&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:foo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'foo'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则 &lt;code&gt;ArticleEntity.locked(scope: 'full')&lt;/code&gt; 不仅实现时只会返回 &lt;code&gt;title&lt;/code&gt;、&lt;code&gt;content&lt;/code&gt; 字段，文档渲染时也只会出现 &lt;code&gt;title&lt;/code&gt;、&lt;code&gt;content&lt;/code&gt; 这两个字段。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这一节的思想是：只用写一遍实体，在不同的场合下使用。&lt;/strong&gt;这条思想同时也是下一节的思想。&lt;/p&gt;
&lt;h3 id="参数上的锁定：在参数上区分不同的场合"&gt;参数上的锁定：在参数上区分不同的场合&lt;/h3&gt;
&lt;p&gt;与返回值一样，参数也需要根据不同的场合修改不同的字段。我们定义一个参数实体：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExampleEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'master'&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它设定有两个 &lt;code&gt;scope&lt;/code&gt;，在两种场合下能够修改的字段不同。我们在实现时，可以在参数定义时使用锁定技术：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admin::UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/admin/users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:put&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ExampleEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&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;def&lt;/span&gt; &lt;span class="nf"&gt;update&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:put&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;ExampleEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'master'&lt;/span&gt;&lt;span class="p"&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;def&lt;/span&gt; &lt;span class="nf"&gt;example&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;h3 id="参数上的锁定：处理缺失参数"&gt;参数上的锁定：处理缺失参数&lt;/h3&gt;
&lt;p&gt;先声明一个 HTTP 相关的背景。对于缺失的参数，HTTP 提供了两种语义的方法：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;PUT&lt;/code&gt; 请求需要提供完整的实体，包括所有字段。如果某个字段缺失，将按照该字段传递了一个 &lt;code&gt;nil&lt;/code&gt; 值处理。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATCH&lt;/code&gt; 请求只用提供需要更新的字段。如果某个字段缺失，将表示这个字段不需要被更新。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这里面涉及到的是对于参数中缺失值如何处理。换言之，对于实体：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用户请求只传递了 &lt;code&gt;{ "name": "Jim" }&lt;/code&gt;，调用 &lt;code&gt;params_on_schema&lt;/code&gt; 会返回什么呢？是&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jim"&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;p&gt;两者的区分在于前者将缺失值设为 &lt;code&gt;nil&lt;/code&gt;，后者丢弃了缺失值。注意这两种数据作为参数传递到 &lt;code&gt;user.update!&lt;/code&gt; 方法中，其效果是不同的。&lt;/p&gt;

&lt;p&gt;控制这种效果的方式也是通过锁定设置 &lt;code&gt;discard_missing:&lt;/code&gt; 选项。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;# PUT 效果，默认情况，参数始终返回完整实体&lt;/span&gt;
  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/users/:id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:put&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ref: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt; &lt;span class="c1"&gt;# 或 UserEntity.locked(discard_missing: 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;def&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# PATCH 效果，参数丢弃未传递的字段&lt;/span&gt;
  &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="s1"&gt;'/users/:id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:patch&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;UserEntity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;discard_missing: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&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;def&lt;/span&gt; &lt;span class="nf"&gt;update&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;strong&gt;总结：只用写一遍实体，在不同的场合下使用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="深入细节"&gt;深入细节&lt;/h2&gt;
&lt;p&gt;最后，我们讲讲 &lt;code&gt;meta-api&lt;/code&gt; 有关细节的一些东西吧。它具备基本的参数验证、默认值和转化等逻辑，详细的内容可参考 &lt;a href="https://github.com/yetrun/web-frame/blob/master/docs/%E6%95%99%E7%A8%8B.md" rel="nofollow" target="_blank" title=""&gt;教程&lt;/a&gt; 相关部分。如果文档中出现不友好的部分，欢迎提 ISSUE 改进。&lt;/p&gt;
&lt;h3 id="默认值"&gt;默认值&lt;/h3&gt;
&lt;p&gt;你可以为参数（或实体内的字段）提供默认值，如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="数据验证"&gt;数据验证&lt;/h3&gt;
&lt;p&gt;有一些内建的数据验证器，比如：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:birth_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="sr"&gt;/\d\d\d\d-\d\d-\d\d/&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:birthday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="ss"&gt;validate: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JsonSchema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'日期格式不对'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=~&lt;/span&gt; &lt;span class="sr"&gt;/\d\d\d\d-\d\d-\d\d/&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="枚举"&gt;枚举&lt;/h3&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;allowable: &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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# 只允许 6 岁以下儿童&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="多态"&gt;多态&lt;/h3&gt;
&lt;p&gt;真的，它有处理多态的能力，就像 Swagger 文档中提供的那样。我不想在这里讲解了，但请相信，它真的有处理多态的能力。&lt;/p&gt;
&lt;h2 id="最后的说辞"&gt;最后的说辞&lt;/h2&gt;
&lt;p&gt;啥也不说了，我的项目地址目前是在：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="http://github.com/yetrun/web-frame" rel="nofollow" target="_blank"&gt;http://github.com/yetrun/web-frame&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所有的意见和建议对我都是有用的。&lt;/p&gt;

&lt;p&gt;如果你遇到 Bug，或者想要新的特性，请提 &lt;a href="https://github.com/yetrun/web-frame/issues" rel="nofollow" target="_blank" title=""&gt;ISSUE&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;如果你遇到任何使用上的问题，希望快速得到解决，请加群：489579810.&lt;/p&gt;

&lt;p&gt;如果这个项目对你有用，请为它添加一个 star，不胜感激。&lt;/p&gt;

&lt;p&gt;如果你有兴趣为这个项目贡献，欢迎提供 PR.&lt;/p&gt;

&lt;p&gt;我希望为 Rails 提供插件的方式没有对你现在的项目带来任何干扰，并为生成文档方面提供助力。&lt;/p&gt;</description>
      <author>yetrun</author>
      <pubDate>Tue, 04 Apr 2023 11:51:34 +0800</pubDate>
      <link>https://ruby-china.org/topics/42981</link>
      <guid>https://ruby-china.org/topics/42981</guid>
    </item>
    <item>
      <title>新写的 Web API 框架今天升级了一波</title>
      <description>&lt;p&gt;之前就这个框架发过一遍帖子。近期由于新冠疫情，公司业务停摆了，全员停工放假，故而集中精力将先前的框架整理了一波。框架地址：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/yetrun/web-frame" rel="nofollow" target="_blank"&gt;https://github.com/yetrun/web-frame&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;现在框架的适用性和稳定性比我第一次发帖时有了大大的提升，已经在我的一个个人项目应用上去（openapi.yet.run），并正在应用在我公司的一个新项目上。希望感兴趣的朋友可以试用之，并提出改进意见。&lt;/p&gt;
&lt;h2 id="框架有了名字"&gt;框架有了名字&lt;/h2&gt;
&lt;p&gt;框架有了自己的名字：&lt;code&gt;meta-api&lt;/code&gt;. 有关取名的思路，可阅读：&lt;a href="https://github.com/yetrun/web-frame/blob/master/docs/%E5%90%8D%E7%A7%B0%E7%94%B1%E6%9D%A5.md" rel="nofollow" target="_blank" title=""&gt;名称由来&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="如何试用"&gt;如何试用&lt;/h2&gt;
&lt;p&gt;我为此提供了一个脚手架：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/yetrun/web-frame-example" rel="nofollow" target="_blank"&gt;https://github.com/yetrun/web-frame-example&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这个脚手架没有使用到 Rails 全家桶，而是将 Rails 能用到 ActiveRecord 组件单独抽出来了。如果你想要用的是别的框架如 Sinatra、Grape 等，同时也不想引入 Rails 路由那一套，也可以参考这个脚手架的思路改造。&lt;/p&gt;
&lt;h2 id="支持"&gt;支持&lt;/h2&gt;
&lt;p&gt;由于框架尚未成熟，可能会遇到各种问题，我特地建个 QQ 群在线实时答疑。（QQ：489579810）&lt;/p&gt;
&lt;h2 id="最后的心声"&gt;最后的心声&lt;/h2&gt;
&lt;p&gt;目前框架已经初步达到可生产应用的程度，但实际效果和稳定性还需更多的项目测试才行。我的个人项目上在运行，公司的项目仍在开发中。最近由于奥密克戎的折磨苦不堪言，对人生更少了点期待，也更愿意沉下心来做点自己的事情。&lt;/p&gt;

&lt;p&gt;希望 未来更好，且行且珍惜吧！&lt;/p&gt;</description>
      <author>yetrun</author>
      <pubDate>Tue, 03 Jan 2023 11:27:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/42818</link>
      <guid>https://ruby-china.org/topics/42818</guid>
    </item>
    <item>
      <title>分享一下最近新写的 Web API 框架</title>
      <description>&lt;p&gt;11 月 22 日更新：今天上传了新的版本，主要调整了路由编写的语法。以前是链式语法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'创建一个新用户'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;do_any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# 这里实现你的业务逻辑 ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;if_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在改成块状语法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/users'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="s1"&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;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# 这里实现你的业务逻辑 ...&lt;/span&gt;
  &lt;span class="p"&gt;}&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://github.com/yetrun/web-frame/blob/master/docs/%E6%95%99%E7%A8%8B.md" rel="nofollow" target="_blank" title=""&gt;教程&lt;/a&gt;。比较重要的章节是有关实体定义的，请看&lt;a href="https://github.com/yetrun/web-frame/blob/master/docs/%E6%95%99%E7%A8%8B.md#%E5%AE%9E%E4%BD%93%E5%AE%9A%E4%B9%89" rel="nofollow" target="_blank" title=""&gt;实体定义&lt;/a&gt;。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;零零总总写了有半年了，虽然现在并不是一个发布的好时机，但是想要先分享出来和大家伙交流一下想法。本来打算先在公司用起来的，不过最近公司没有什么新项目，老项目我也不想换框架了，人力物力呀…… 所以只在自己的个人项目下应用了，目前运转良好。&lt;/p&gt;

&lt;p&gt;又是一个轮子，只不过有些不一样的地方。&lt;/p&gt;

&lt;p&gt;首先，思想上它是完全面向 API 文档的。也许好多团队写文档会借助于 YAPI 这样的第三方工具，或者直接用 Postman 这样用用例来说话，再不济手写 Markdown. 我在框架里直接生成了 OpenAPI 的规格文档，在开发者实现接口的同时，文档就生成了。例如像下面这样定义一个接口，同时文档就能生成了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'创建一个新用户'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;do_any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# 这里实现你的业务逻辑 ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;if_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;expose&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;using: &lt;/span&gt;&lt;span class="no"&gt;UserEntity&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其次，这次将参数和返回值的写法统一起来了。像上面，我们只用了一个 &lt;code&gt;UserEntity&lt;/code&gt;，就可以同时定义参数和返回值了，再也不用写两遍了。你的 &lt;code&gt;UserEntity&lt;/code&gt; 内部可能是这样写的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserEntity&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;param: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# 只返回&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;render: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# 只用作参数&lt;/span&gt;
  &lt;span class="n"&gt;property&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'string,
  property :age, type: '&lt;/span&gt;&lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他的功能还有很多，就不想一一列举了。想进一步了解的，可以去看 GitHub 仓库：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/yetrun/web-frame" rel="nofollow" target="_blank"&gt;https://github.com/yetrun/web-frame&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这个框架适合纯 API 后端的场景，即前后端分离。也比较适合团队，基本用一种固定的写法构建 API. 重点是生成对应的文档，前后端不再扯皮（你的文档是这么定义的，怎么你返回的数据是长这样啊之类的云云）。&lt;/p&gt;

&lt;p&gt;说实话最近整体的工作节奏都有些慢，受大环境的影响吧。本来打算等到我稳定运行后再发布了，但是这可能还要一阵子，就先预先发布一下看看反馈再说。现在大家运行起来可能有点吃力，因为还缺一个项目脚手架，后面会补上。&lt;/p&gt;

&lt;p&gt;如果觉得对你有用，还望支持一下，对我回复个 +1. 如果大家热情足够的话，因为涉及到文档，我感觉上手难度还是有的，到时会建个 QQ 群或微信群做全面的答疑。如果大家热情不高，我就当成一个小众项目在我的团队里使用吧。视反馈而定。&lt;/p&gt;</description>
      <author>yetrun</author>
      <pubDate>Fri, 19 Aug 2022 18:22:40 +0800</pubDate>
      <link>https://ruby-china.org/topics/42602</link>
      <guid>https://ruby-china.org/topics/42602</guid>
    </item>
  </channel>
</rss>
