提交文章后会发生什么?
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"
该控制器可以在/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
// ...
完成后,该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
它非常简单:它会检查您是否正在使用模板(又名使用路径/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 %>
这将根据我们的条件加载正确的视图,通常是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 %>
<!-- ... -->
这个表单渲染出来的就是你访问时通常看到的 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>
articleForm.jsx:
<PublishToggle
published={published}
onPublish={this.onPublish}
onSaveDraft={this.onSaveDraft}
onChange={linkState(this, 'published')}
// ...
/>
articleForm.jsx:
onPublish = e => {
e.preventDefault();
this.setState({submitting: true, published: true})
let state = this.state;
state['published'] = true;
submitArticle(state, this.handleArticleError);
};
该函数从./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);
}
因此,一旦您单击该SAVE ARTICLE
按钮,就会发生以下情况:
state
根据当前变量创建一篇文章- 文章发送至
/api/articles
- 保存完成后,我们将重定向到其新的 URL。
我们现在可以开始深入研究后端!
后端
/api/articles
我们现在通过 POST 方式从前端接收 JSON 文件形式的文章。
路由
再次,在/config/routes.rb文件中,我们需要搜索我们的端点。
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
// ...
控制器
我们现在位于/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
服务
此方法调用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
该服务创建了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
before_validation
在确保对象有效之前将调用这些方法。
- evaluate_markdown将把我们的 markdown 转换为 HTML
- create_slug将为 URL 创建最有可能唯一的 slug
- create_password将创建一个唯一的预览密码值
其余方法的名称应该非常明确。
该模型还将对其属性执行许多验证。
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 }
结论
呼,这篇文章终于保存了!这么简单的操作,竟然费了这么大劲。
简单回顾一下,为了查看一篇文章,我们加载了正确的Controller,它会加载View并将其呈现到页面上。
当尝试执行 CRUD 操作时,我们会根据API 资源找到正确的路由,该路由会加载一个控制器。该控制器可以使用 服务 与数据交互,而服务本身又使用模型与数据库交互。
现在已经涵盖了技术方面的内容,我希望获得有关这篇文章的一些反馈。
我对这个系列有几个目标:
- 帮助人们浏览大型代码库并了解其架构
- 降低本网站等开源项目的贡献门槛。
这就是为什么反馈很重要。
它是否帮助你理解了来源?
或者你希望看到一些具体的东西?
请在下面的评论中告诉我,我会尽力改进这个系列!
鏂囩珷鏉ユ簮锛�https://dev.to/antogarand/what-happens-when-you-submit-an-article-3f8a