提交文章后会发生什么?dev.to 底层原理(第一部分)简介 应用概览 显示新文章视图 保存文章 总结

2025-06-08

提交文章后会发生什么?

dev.to 底层原理(第一部分)

介绍

应用程序概述

显示新文章视图

保存文章

结论

dev.to 底层原理(第一部分)

本系列文章将揭秘dev.to源代码的奥秘,帮助大家理解和改进这款应用。

源代码可在GitHub上获取,贡献代码即可获得炫酷徽章

免责声明:我不懂 Ruby,也不懂 Ruby on Rails,所以这篇文章可能存在一些错误或缺失的地方。欢迎指出,我会尽力改正!

介绍

提交文章很容易,对吧?

您只需按下SAVE POST按钮,就可以了!

其实它的复杂性要大得多,在这篇文章中,我将揭开幕后发生的神奇事件!

应用程序概述

Dev.to 的后端使用Ruby On Rails ,前端使用Preact

。 后端托管 REST API,前端使用这些 API 来访问和发布数据。

前端是单页应用程序,但也是服务器端渲染的

这意味着,如果您dev.to/new直接访问,服务器将为您生成所有 HTML,以供浏览器显示。
然后,每当捆绑的 preact 脚本加载时,我们就获得了 SPA 功能:当尝试访问新页面时,JavaScript 会抓取该页面,然后 preact 将使用接收到的 HTML 更新页面内容。

显示新文章视图

好吧,你想写一篇文章。

首先,你前往dev.to/new

Ruby on rails使用 GET 协议检查/config/routes中的路由以找到/new 。

该路由告诉它加载articles控制器和new方法。

get "/new" => "articles#new"
get "/new/:template" => "articles#new"

get "/pod" => "podcast_episodes#index"
get "/readinglist" => "reading_list_items#index"

Enter fullscreen mode Exit fullscreen mode

该控制器可以在/app/controllers/articles_controller.rb下找到

在加载该new方法之前,会执行一些权限检查。
这些检查在控制器顶部声明,包括确保您已登录以及阻止被禁用户创建文章等方法。


class ArticlesController < ApplicationController
  include ApplicationHelper
  before_action :authenticate_user!, except: %i[feed new]
  before_action :set_article, only: %i[edit update destroy]
  before_action :raise_banned, only: %i[new create update]
  before_action :set_cache_control_headers, only: %i[feed]
  after_action :verify_authorized
// ...
Enter fullscreen mode Exit fullscreen mode

完成后,该new方法被调用:

  def new
    @user = current_user
    @tag = Tag.find_by_name(params[:template])
    @article = if @tag&.submission_template.present? && @user
                 authorize Article
                 Article.new(body_markdown: @tag.submission_template_customized(@user.name),
                             processed_html: "")
               else
                 skip_authorization
                 if params[:state] == "v2" || Rails.env.development?
                   Article.new
                 else
                   Article.new(
                     body_markdown: "---\ntitle: \npublished: false\ndescription: \ntags: \n---\n\n",
                     processed_html: "",
                   )
                 end
               end
end
Enter fullscreen mode Exit fullscreen mode

它非常简单:它会检查您是否正在使用模板(又名使用路径/new/:template),并加载此模板,或创建通用的Front Matter主体。

Article.new 代表New Article View,可在/app/views/articles/new.html.erb下找到

<% title "New Article - DEV" %>

<% if user_signed_in? %>
  <% if params[:state] == "v2" || Rails.env.development? %>
    <%= javascript_pack_tag 'articleForm', defer: true %>
    <%= render 'articles/v2_form' %>
  <% else %>
    <%= render 'articles/markdown_form' %>
  <% end %>
<% else %>
  <%= render "devise/registrations/registration_form" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

这将根据我们的条件加载正确的视图,通常是articles/markdown_form

<%= form_for(@article, html: {id:"article_markdown_form"}) do |f| %>
  <% if @article.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>

      <ul>
      <% @article.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
<% end %>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

这个表单渲染出来的就是你访问时通常看到的 HTML dev.to/new,我们终于成功了!
生成的 HTML 会在 Ruby On Rails 的某个时刻被用作/app/views/layouts/application.html.erb中的 body。

保存文章

好吧,您已经撰写了一篇关于Ben Halpern 的网站有多好的精彩文章,现在您希望将其发布出来供所有人查看!

你把published值设置为了true,然后按下这个蓝色的大SAVE POST按钮。接下来会发生什么?

您的 HTML 已加载,Preact 已加载,并且它会监听 SAVE 按钮的点击事件。

前端

我们现在位于前端代码中,位于/app/javascript/article-form/articleForm.jsx下。

按钮本身位于elements/publishToggle.jsx下,我们articleForm.jsx为点击添加了一个事件监听器。

publishToggle.jsx:

<button onClick={onPublish}>
  {published ? 'SAVE CHANGES' : 'PUBLISH' }
</button>
Enter fullscreen mode Exit fullscreen mode

articleForm.jsx:

<PublishToggle
  published={published}
  onPublish={this.onPublish}
  onSaveDraft={this.onSaveDraft}
  onChange={linkState(this, 'published')}
  // ...
/>
Enter fullscreen mode Exit fullscreen mode

articleForm.jsx:

onPublish = e => {
  e.preventDefault();
  this.setState({submitting: true, published: true})
  let state = this.state;
  state['published'] = true;
  submitArticle(state, this.handleArticleError);
};
Enter fullscreen mode Exit fullscreen mode

该函数从./actionssubmitArticle导入

actions.js - 提交文章

export function submitArticle(payload, errorCb, failureCb) {
  const method = payload.id ? 'PUT' : 'POST'
  const url = payload.id ? '/api/articles/'+ payload.id : '/api/articles'
  fetch(url, {
    // ...
    body: JSON.stringify({
      article: payload,
    })
  })
  .then(response => response.json())
  .then(response => {
    if (response.current_state_path) {
      window.location.replace(response.current_state_path);
    } else {
      errorCb(response)
    }
  })
  .catch(failureCb);
}

Enter fullscreen mode Exit fullscreen mode

因此,一旦您单击该SAVE ARTICLE按钮,就会发生以下情况:

  • state根据当前变量创建一篇文章
  • 文章发送至/api/articles
  • 保存完成后,我们将重定向到其新的 URL。

我们现在可以开始深入研究后端!

后端

/api/articles我们现在通过 POST 方式从前端接收 JSON 文件形式的文章。

路由

再次,在/config/routes.rb文件中,我们需要搜索我们的端点。

有一个api命名空间,其中包含我们的文章资源。

Ruby on Rails 资源将一些默认 CRUD 动词映射到它们各自的方法,因此在我们的例子中该POST方法将调用该articles#create方法。

路线.rb

namespace :api, defaults: { format: "json" } do
  scope module: :v0,
        constraints: ApiConstraints.new(version: 0, default: true) do
    resources :articles, only: %i[index show create update] do
      collection do
        get "/onboarding", to: "articles#onboarding"
      end
    end
    resources :comments
// ...
Enter fullscreen mode Exit fullscreen mode

控制器

我们现在位于/app/controllers/articles_controller中,方法如下create

def create
  authorize Article
  @user = current_user
  @article = ArticleCreationService.
    new(@user, article_params, job_opportunity_params).
    create!
  redirect_after_creation
end
Enter fullscreen mode Exit fullscreen mode

服务

此方法调用ArticleCreationService来创建我们的文章!

def create!
  raise if RateLimitChecker.new(user).limit_by_situation("published_article_creation")
  article = Article.new(article_params)
  article.user_id = user.id
  article.show_comments = true
  if user.organization_id.present? && article_params[:publish_under_org].to_i == 1
    article.organization_id = user.organization_id
  end
  create_job_opportunity(article)
  if article.save
    if article.published
      Notification.send_all(article, "Published")
    end
  end
  article.decorate
end
Enter fullscreen mode Exit fullscreen mode

该服务创建了Article模型的新实例,并保存它。

模型

使用 Ruby on Rails,我们的模型是Active Records,并且附带一些魔力。

虽然我不会深入研究对象的数据库映射部分,但我发现有趣的是在创建或保存对象时调用的before方法。

before_validation :evaluate_markdown
before_validation :create_slug
before_create     :create_password
before_save       :set_all_dates
before_save       :calculate_base_scores
before_save       :set_caches
after_save :async_score_calc, if: :published
Enter fullscreen mode Exit fullscreen mode

before_validation在确保对象有效之前将调用这些方法。

其余方法的名称应该非常明确。

该模型还将对其属性执行许多验证。

  validates :slug, presence: { if: :published? }, format: /\A[0-9a-z-]*\z/,
                   uniqueness: { scope: :user_id }
  validates :title, presence: true,
                    length: { maximum: 128 }
  validates :user_id, presence: true
  validates :feed_source_url, uniqueness: { allow_blank: true }
  validates :canonical_url,
            url: { allow_blank: true, no_local: true, schemes: ["https", "http"] },
uniqueness: { allow_blank: true }
Enter fullscreen mode Exit fullscreen mode

结论

呼,这篇文章终于保存了!这么简单的操作,竟然费了这么大劲。

简单回顾一下,为了查看一篇文章,我们加载了正确的Controller,它会加载View并将其呈现到页面上。

当尝试执行 CRUD 操作时,我们会根据API 资源找到正确的路由,该路由会加载一个控制器。该控制器可以使用 服务 与数据交互而服务本身又使用模型与数据库交互。


现在已经涵盖了技术方面的内容,我希望获得有关这篇文章的一些反馈。

我对这个系列有几个目标:

  1. 帮助人们浏览大型代码库并了解其架构
  2. 降低本网站等开源项目的贡献门槛。

这就是为什么反馈很重要。
它是否帮助你理解了来源?
或者你希望看到一些具体的东西?

请在下面的评论中告诉我,我会尽力改进这个系列!

鏂囩珷鏉ユ簮锛�https://dev.to/antogarand/what-happens-when-you-submit-an-article-3f8a
PREV
👨‍💻[FirstDevRole #1] 成为一名程序员的真正要求是什么?
NEXT
路线图 - 高级 Java 开发人员 2024