本文概述
- 用例
- 测试驱动开发和RSpec
- 入门
- 用户登录用例和规格
- 实现登录API端点
- 配对编程会话审阅API端点:入门
- 配对编程会话审阅API端点:验证
- 配对编程会话审阅API端点:返回结果
- 生成测试数据
- 测试API
- 本文总结
在本教程中, 我将演示如何使用Grape(一种用于Ruby的类似REST的API微框架)在Rails中为JSON API建立后端支持。 Grape旨在作为可安装的机架引擎运行, 以补充我们的Web应用程序, 而不会干扰它们。
文章图片
用例 在本教程中, 我们将重点关注的用例是一个能够捕获和审查结对编程会话的应用程序。该应用程序本身将在ObjectiveC中为iOS编写, 并将需要与后端服务进行通信以存储和检索数据。在本教程中, 我们的重点是创建支持JSON API的健壮且安全的后端服务。
该API将支持以下方法:
- 登录系统
- 查询配对编程会话评论
关键技术要求包括:
- 每个API调用都必须返回有效的JSON
- 每个失败的API调用都必须记录有足够的上下文和信息, 以便随后可重现, 并在必要时进行调试
- 每个请求应仅限于我们跟踪的一小部分开发人员
- 所有请求(登录/注册除外)都需要进行身份验证
为了进行测试, 我们将使用RSpec, 它是RubyOnRails社区中众所周知的测试框架。因此, 在本文中, 我将提及” 规格” 而不是” 测试” 。
全面的测试方法包括” 阳性” 和” 阴性” 测试。负规范将指定, 例如, 某些参数丢失或不正确时, API端点的行为。积极规范涵盖了正确调用API的情况。
入门 让我们为后端API奠定基础。首先, 我们需要创建一个新的rails应用程序:
rails new srcmini_grape_blog
接下来, 我们将rspec-rails添加到我们的gemfile中, 以安装RSpec:
group :development, :test do
gem 'rspec-rails', '~>
3.2'
end
然后, 从命令行中运行:
rails generate rspec:install
我们还可以将一些现有的开源软件用于我们的测试框架。特别:
- Devise-基于Warden的Rails灵活的身份验证解决方案
- factory_girl_rails-为factory_girl提供Rails集成, 该库用于将Ruby对象设置为测试数据
...
gem 'devise'
...group :development, :test do
...gem 'factory_girl_rails', '~>
4.5'
...
end
步骤2:生成用户模型, 初始化devise gem, 并将其添加到用户模型(这使用户类可用于身份验证)。
rails g model user
rails generate devise:install
rails generate devise user
步骤3:在我们的rails_helper.rb文件中包括factory_girl语法方法, 以便在我们的规范中使用用户创建的缩写版本:
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
步骤4:将葡萄宝石添加到我们的DSL并安装它:
gem 'grape'
bundle
用户登录用例和规格 我们的后端将需要支持基本的登录功能。假设有效的登录请求包含一个注册的电子邮件地址和密码对, 让我们为login_spec创建框架:
require 'rails_helper'describe '/api/login' do
context 'negative tests' do
context 'missing params' do
context 'password' do
end
context 'email' do
end
end
context 'invalid params' do
context 'incorrect password' do
end
context 'with a non-existent login' do
end
end
end
context 'positive tests' do
context 'valid params' do
end
end
end
如果缺少任何一个参数, 则客户端应收到HTTP返回状态代码400(即错误请求), 以及错误消息” 电子邮件丢失” 或” 密码丢失” 。
对于我们的测试, 我们将创建一个有效的用户, 并将该用户的电子邮件和密码设置为此测试套件的原始参数。然后, 我们将通过省略密码/电子邮件或覆盖它来为每个特定的规范自定义此参数哈希。
【Grape Gem教程(如何在Ruby中构建类似REST的API)】让我们在规范的开头创建用户和参数哈希。我们将这段代码放在describe块之后:
describe '/api/login' do
let(:email) { user.email }
let(:password) { user.password }
let!(:user) { create :user }
let(:original_params) { { email: email, password: password } }
let(:params) { original_params }
...
然后, 我们可以如下扩展” 缺少参数” /” 密码” 的上下文:
let(:params) { original_params.except(:password) }
it_behaves_like '400'
it_behaves_like 'json result'
it_behaves_like 'contains error msg', 'password is missing'
但是, 我们无需在” 电子邮件” 和” 密码” 上下文中重复期望, 而是可以使用与期望相同的共享示例。为此, 我们需要在rails_helper.rb文件中取消注释此行:
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
然后, 我们需要将3个RSpec共享示例添加到spec / support / shared.rb中:
RSpec.shared_examples 'json result' do
specify 'returns JSON' do
api_call params
expect { JSON.parse(response.body) }.not_to raise_error
end
endRSpec.shared_examples '400' do
specify 'returns 400' do
api_call params
expect(response.status).to eq(400)
end
endRSpec.shared_examples 'contains error msg' do |msg|
specify "error msg is #{msg}" do
api_call params
json = JSON.parse(response.body)
expect(json['error_msg']).to eq(msg)
end
end
这些共享的示例正在调用api_call方法, 该方法使我们能够在规范中仅一次定义API端点(与DRY??原理保持一致)。我们将这种方法定义如下:
describe '/api/login' do
...
def api_call *params
post "/api/login", *params
end
...
我们还需要为我们的用户定制工厂:
FactoryGirl.define do
factory :user do
password "Passw0rd"
password_confirmation { |u| u.password }sequence(:email) { |n| "test#{n}@example.com" }
end
end
最后, 在运行规格之前, 我们需要运行迁移:
rake db:migrate
不过请记住, 由于我们尚未实现API端点, 因此规范仍会失败。接下来。
实现登录API端点 首先, 我们将为登录API(app / api / login.rb)写一个空框架:
class Login <
Grape::API
format :json
desc 'End-points for the Login'
namespace :login do
desc 'Login via email and password'
params do
requires :email, type: String, desc: 'email'
requires :password, type: String, desc: 'password'
end
post do
end
end
end
接下来, 我们将编写一个聚合器类, 用于聚合API端点(app / api / api.rb):
class API <
Grape::API
prefix 'api'
mount Login
end
好的, 现在我们可以在以下路线中安装我们的API了:
Rails.application.routes.draw do
...
mount API =>
'/'
...
end
现在, 让我们添加代码以检查缺少的参数。我们可以通过从Grape :: Exceptions :: ValidationErrors抢救将该代码添加到api.rb中。
rescue_from Grape::Exceptions::ValidationErrors do |e|
rack_response({
status: e.status, error_msg: e.message, }.to_json, 400)
end
对于无效的密码, 我们将检查http响应代码是否为401, 这表示未经授权的访问。让我们将其添加到” 密码错误” 上下文中:
let(:params) { original_params.merge(password: 'invalid') }
it_behaves_like '401'
it_behaves_like 'json result'
it_behaves_like 'contains error msg', 'Bad Authentication Parameters'
然后, 将相同的逻辑也添加到” 不存在登录名” 上下文中。
然后, 我们将处理无效身份验证尝试的逻辑实现到我们的login.rb中, 如下所示:
post do
user = User.find_by_email params[:email]
if user.present? &
&
user.valid_password?(params[:password])
else
error_msg = 'Bad Authentication Parameters'
error!({ 'error_msg' =>
error_msg }, 401)
end
end
此时, 登录api的所有负面规格都可以正常运行, 但是我们仍然需要支持登录api的正面规格。我们的积极规范将期望端点返回带有有效JSON和有效令牌的HTTP响应代码200(即成功):
it_behaves_like '200'
it_behaves_like 'json result'specify 'returns the token as part of the response' do
api_call params
expect(JSON.parse(response.body)['token']).to be_present
end
让我们还将对响应代码200的期望添加到spec / support / shared.rb:
RSpec.shared_examples '200' do
specify 'returns 200' do
api_call params
expect(response.status).to eq(200)
end
end
如果成功登录, 我们将以以下格式返回第一个有效的authentication_token和用户的电子邮件:
{‘email’:<
the_email_of_the_user>
, ‘token’:<
the users first valid token>
}
如果还没有这样的令牌, 那么我们将为当前用户创建一个:
...
if user.present? &
&
user.valid_password?(params[:password])
token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user)
status 200
else
...
为了使它起作用, 我们将需要一个属于用户的AuthenticationToken类。我们将生成此模型, 然后运行相应的迁移:
rails g model authentication_token token user:references expires_at:datetime
rake db:migrate
我们还需要将相应的关联添加到我们的用户模型中:
class User <
ActiveRecord::Base
has_many :authentication_tokens
end
然后, 我们将有效范围添加到AuthenticationToken类:
class AuthenticationToken <
ActiveRecord::Base
belongs_to :user
validates :token, presence: true
scope :valid, ->
{ where{ (expires_at == nil) | (expires_at >
Time.zone.now) } }
end
请注意, 我们在where语句中使用了Ruby语法。这是通过使用squeel gem启用的, 它在activerecord查询中支持Ruby语法。
对于经过验证的用户, 我们将利用葡萄实体gem的功能创建一个我们称为” 具有令牌实体的用户” 的实体。
让我们为我们的实体编写规范并将其放入user_with_token_entity_spec.rb文件中:
require 'rails_helper'describe Entities::UserWithTokenEntity do
describe 'fields' do
subject(:subject) { Entities::UserWithTokenEntity }
specify { expect(subject).to represent(:email)}let!(:token) { create :authentication_token }
specify 'presents the first available token' do
json = Entities::UserWithTokenEntity.new(token.user).as_json
expect(json[:token]).to be_present
end
end
end
接下来, 将实体添加到user_entity.rb:
module Entities
class UserEntity <
Grape::Entity
expose :email
end
end
最后, 向user_with_token_entity.rb添加另一个类:
module Entities
class UserWithTokenEntity <
UserEntity
expose :token do |user, options|
user.authentication_tokens.valid.first.token
end
end
end
由于我们不希望令牌无限期保持有效, 因此我们希望令牌在一天后过期:
FactoryGirl.define do
factory :authentication_token do
token "MyString"
expires_at 1.day.from_now
user
end
end
完成所有这些之后, 我们现在可以使用新编写的UserWithTokenEntity返回所需的JSON格式:
...
user = User.find_by_email params[:email]
if user.present? &
&
user.valid_password?(params[:password])
token = user.authentication_tokens.valid.first || AuthenticationToken.generate(user)
status 200
present token.user, with: Entities::UserWithTokenEntity
else
...
凉。现在, 我们所有的规范都已通过, 并且支持基本登录api端点的功能要求。
配对编程会话审阅API端点:入门 我们的后端将需要允许已登录的授权开发人员查询对编程会话评论。
我们的新API端点将安装到/ api / pair_programming_session并将返回属于项目的评论。首先, 为该规范编写一个基本框架:
require 'rails_helper'describe '/api' do
describe '/pair_programming_session' do
def api_call *params
get '/api/pair_programming_sessions', *params
endcontext 'invalid params' do
endcontext 'valid params' do
end
end
end
我们还将编写一个相应的空API端点(app / api / pair_programming_sessions.rb):
class PairProgrammingSessions <
Grape::API
format :jsondesc 'End-points for the PairProgrammingSessions'
namespace :pair_programming_sessions do
desc 'Retrieve the pairprogramming sessions'
params do
requires :token, type: String, desc: 'email'
end
get do
end
end
end
然后, 安装新的api(app / api / api.rb):
...
mount Login
mount PairProgrammingSessions
end
让我们根据需求扩展规范和API端点。
配对编程会话审阅API端点:验证 我们最重要的非功能性安全要求之一是将API访问权限限制在我们跟踪的一小部分开发人员中, 因此我们要指定:
...
def api_call *params
get '/api/pair_programming_sessions', *params
endlet(:token) { create :authentication_token }
let(:original_params) { { token: token.token} }
let(:params) { original_params }it_behaves_like 'restricted for developers'context 'invalid params' do
...
然后, 我们将在shared.rb中创建一个shared_example, 以确认请求来自我们的注册开发人员之一:
RSpec.shared_examples 'restricted for developers' do
context 'without developer key' do
specify 'should be an unauthorized call' do
api_call params
expect(response.status).to eq(401)
end
specify 'error code is 1001' do
api_call params
json = JSON.parse(response.body)
expect(json['error_code']).to eq(ErrorCodes::DEVELOPER_KEY_MISSING)
end
end
end
我们还需要创建一个ErrorCodes类(在app / models / error_codes.rb中):
module ErrorCodes
DEVELOPER_KEY_MISSING = 1001
end
由于我们希望API将来会扩展, 因此我们将实现一个authorization_helper, 可以在应用程序中的所有API端点之间重用它们, 以仅限制对注册开发人员的访问:
class PairProgrammingSessions <
Grape::API
helpers ApiHelpers::AuthenticationHelper
before { restrict_access_to_developers }
我们将在ApiHelpers :: AuthenticationHerlper模块(app / api / api_helpers / authentication_helper.rb)中定义方法strict_access_to_developers。此方法将仅检查标题下的密钥授权是否包含有效的ApiKey。 (每个想要访问该API的开发人员都需要一个有效的ApiKey。这可以由系统管理员提供, 也可以通过一些自动注册过程来提供, 但是该机制不在本文的讨论范围之内。)
module ApiHelpers
module AuthenticationHelperdef restrict_access_to_developers
header_token = headers['Authorization']
key = ApiKey.where{ token == my{ header_token } }
Rails.logger.info "API call: #{headers}\tWith params: #{params.inspect}" if ENV['DEBUG']
if key.blank?
error_code = ErrorCodes::DEVELOPER_KEY_MISSING
error_msg = 'please aquire a developer key'
error!({ :error_msg =>
error_msg, :error_code =>
error_code }, 401)
# LogAudit.new({env:env}).execute
end
end
end
end
然后, 我们需要生成ApiKey模型并运行迁移:rails g模型api_key令牌rake db:migrate
完成此操作后, 在我们的spec / api / pair_programming_spec.rb中, 我们可以检查用户是否已通过身份验证:
...
it_behaves_like 'restricted for developers'
it_behaves_like 'unauthenticated'
...
我们还要定义一个未经身份验证的共享示例, 该示例可以在所有规范(spec / support / shared.rb)中重复使用:
RSpec.shared_examples 'unauthenticated' do
context 'unauthenticated' do
specify 'returns 401 without token' do
api_call params.except(:token), developer_header
expect(response.status).to eq(401)
end
specify 'returns JSON' do
api_call params.except(:token), developer_header
json = JSON.parse(response.body)
end
end
end
这个共享的示例需要开发人员标头中的令牌, 因此我们将其添加到我们的规范(spec / api / pair_programming_spec.rb)中:
...
describe '/api' do
let(:api_key) { create :api_key }
let(:developer_header) { {'Authorization' =>
api_key.token} }
...
现在, 在我们的app / api / pair_programming_session.rb中, 让我们尝试验证用户身份:
...
class PairProgrammingSessions <
Grape::API
helpers ApiHelpers::AuthenticationHelper
before { restrict_access_to_developers }
before { authenticate! }
...
让我们实施身份验证! AuthenticationHelper(app / api / api_helpers / authentication_helper.rb)中的方法:
...
module ApiHelpers
module AuthenticationHelper
TOKEN_PARAM_NAME = :tokendef token_value_from_request(token_param = TOKEN_PARAM_NAME)
params[token_param]
enddef current_user
token = AuthenticationToken.find_by_token(token_value_from_request)
return nil unless token.present?
@current_user ||= token.user
enddef signed_in?
!!current_user
enddef authenticate!
unless signed_in?
AuditLog.create data: 'unauthenticated user access'
error!({ :error_msg =>
"authentication_error", :error_code =>
ErrorCodes::BAD_AUTHENTICATION_PARAMS }, 401)
end
end
...
(请注意, 我们需要将错误代码BAD_AUTHENTICATION_PARAMS添加到我们的ErrorCodes类中。)
接下来, 让我们说明如果开发人员使用无效令牌调用API会发生什么情况。在这种情况下, 返回码将为401, 表示” 未经授权的访问” 。结果应为JSON并应创建可审核的。因此, 我们将其添加到spec / api / pair_programming_spec.rb:
...
context 'invalid params' do
context 'incorrect token' do
let(:params) { original_params.merge(token: 'invalid') }it_behaves_like '401'
it_behaves_like 'json result'
it_behaves_like 'auditable created'it_behaves_like 'contains error msg', 'authentication_error'
it_behaves_like 'contains error code', ErrorCodes::BAD_AUTHENTICATION_PARAMS
end
end
...
我们将在spec / support / shared.rb中添加” auditable created” , “ 包含错误代码” 和” 包含错误msg” 共享的示例:
...
RSpec.shared_examples 'contains error code' do |code|
specify "error code is #{code}" do
api_call params, developer_header
json = JSON.parse(response.body)
expect(json['error_code']).to eq(code)
end
endRSpec.shared_examples 'contains error msg' do |msg|
specify "error msg is #{msg}" do
api_call params, developer_header
json = JSON.parse(response.body)
expect(json['error_msg']).to eq(msg)
end
endRSpec.shared_examples 'auditable created' do
specify 'creates an api call audit' do
expect do
api_call params, developer_header
end.to change{ AuditLog.count }.by(1)
end
end
...
我们还需要创建一个audit_log模型:
rails g model audit_log backtrace data user:references
rake db:migrate
配对编程会话审阅API端点:返回结果 对于经过身份验证和授权的用户, 对此API端点的调用应返回按项目分组的一组成对编程会话评论。让我们相应地修改spec / api / pair_programming_spec.rb:
...
context 'valid params' do
it_behaves_like '200'
it_behaves_like 'json result'
end
...
这指定使用有效api_key和有效参数提交的请求返回的HTTP代码为200(即成功), 并且结果以有效JSON的形式返回。
我们将查询然后以JSON格式返回那些参与编程会话, 其中任何参与者都是当前用户(app / api / pair_programming_sessions.rb):
...
get do
sessions = PairProgrammingSession.where{(host_user == my{current_user}) | (visitor_user == my{current_user})}
sessions = sessions.includes(:project, :host_user, :visitor_user, reviews: [:code_samples, :user] )
present sessions, with: Entities::PairProgrammingSessionsEntity
end
...
配对编程会话在数据库中的建模如下:
- 项目与结对编程会话之间的一对多关系
- 配对编程会话与评论之间的一对多关系
- 评论和代码示例之间的一对多关系
rails g model project name
rails g model pair_programming_session project:references host_user:references visitor_user:references
rails g model review pair_programming_session:references user:references comment
rails g model code_sample review:references code:text
rake db:migrate
然后, 我们需要修改PairProgrammingSession和Review类以包含has_many关联:
class Review <
ActiveRecord::Base
belongs_to :pair_programming_session
belongs_to :userhas_many :code_samples
endclass PairProgrammingSession <
ActiveRecord::Base
belongs_to :project
belongs_to :host_user, class_name: :User
belongs_to :visitor_user, class_name: 'User'has_many :reviews
end
注意:在通常情况下, 我会通过首先为它们编写规范来生成这些类, 但是由于这超出了本文的范围, 因此我将跳过该步骤。
现在, 我们需要编写那些类, 这些类会将模型转换为JSON表示形式(在葡萄术语中称为” 葡萄实体” )。为简单起见, 我们将在模型和葡萄实体之间使用一对一映射。
我们首先从CodeSampleEntity(在api / entities / code_sample_entity.rb)中公开代码字段:
module Entities
class CodeSampleEntity <
Grape::Entity
expose :code
end
end
然后, 通过重用已经定义的UserEntity和CodeSampleEntity, 公开用户和相关的code_samples:
module Entities
class ReviewEntity <
Grape::Entity
expose :user, using: UserEntity
expose :code_samples, using: CodeSampleEntity
end
end
我们还公开了ProjectEntity的名称字段:
module Entities
class ProjectEntity <
Grape::Entity
expose :name
end
end
最后, 我们将实体组装到一个新的PairProgrammingSessionsEntity中, 在其中我们暴露项目, host_user, visitor_user和评论:
module Entities
class PairProgrammingSessionsEntity <
Grape::Entity
expose :project, using: ProjectEntity
expose :host_user, using: UserEntity
expose :visitor_user, using: UserEntity
expose :reviews, using: ReviewEntity
end
end
至此, 我们的API已完全实现!
生成测试数据 为了进行测试, 我们将在db / seeds.rb中创建一些示例数据。该文件应包含使用默认值播种数据库所需的所有记录创建。然后可以使用rake db:seed加载数据(或在调用db:setup时使用db创建数据)。这是其中可能包含的示例:
user_1 = User.create email: '[email
protected]', password: 'password', password_confirmation: 'password'
user_2 = User.create email: '[email
protected]', password: 'password', password_confirmation: 'password'
user_3 = User.create email: '[email
protected]', password: 'password', password_confirmation: 'password'
ApiKey.create token: '12345654321'project_1 = Project.create name: 'Time Sheets'
project_2 = Project.create name: 'srcmini Blog'
project_3 = Project.create name: 'Hobby Project'session_1 = PairProgrammingSession.create project: project_1, host_user: user_1, visitor_user: user_2
session_2 = PairProgrammingSession.create project: project_2, host_user: user_1, visitor_user: user_3
session_3 = PairProgrammingSession.create project: project_3, host_user: user_2, visitor_user: user_3review_1 = session_1.reviews.create user: user_1, comment: 'Please DRY a bit your code'
review_2 = session_1.reviews.create user: user_1, comment: 'Please DRY a bit your specs'review_3 = session_2.reviews.create user: user_1, comment: 'Please DRY your view templates'
review_4 = session_2.reviews.create user: user_1, comment: 'Please clean your N+1 queries'review_1.code_samples.create code: 'Lorem Ipsum'
review_1.code_samples.create code: 'Do not abuse the single responsibility principle'review_2.code_samples.create code: 'Use some shared examples'
review_2.code_samples.create code: 'Use at the beginning of specs'
现在我们的应用程序可以使用了, 我们可以启动我们的rails服务器。
测试API 我们将使用Swagger对API进行一些基于浏览器的手动测试。不过, 我们需要一些设置步骤才能使用Swagger。
首先, 我们需要在gemfile中添加几个gem:
...
gem 'grape-swagger'
gem 'grape-swagger-ui'
...
然后, 我们运行bundle来安装这些gem。
我们还需要将这些添加到资产到资产管道中(在config / initializers / assets.rb中):
Rails.application.config.assets.precompile += %w( swagger_ui.js )
Rails.application.config.assets.precompile += %w( swagger_ui.css )
最后, 在app / api / api.rb中, 我们需要安装swagger生成器:
...
add_swagger_documentation
end
...
现在, 我们只需访问http:// localhost:3000 / api / swagger, 即可利用Swagger的漂亮用户界面来探索我们的API。
Swagger以一种很好的探索方式展示了我们的API端点。如果单击端点, 则Swagger会列出其操作。如果单击某个操作, Swagger将显示其必需和可选参数及其数据类型。
在进行下一步之前, 还有一个细节:由于我们使用有效的api_key限制了API开发人员的使用, 因此我们将无法直接从浏览器访问API端点, 因为服务器在HTTP标头中需要有效的api_key。我们可以通过使用” 修改Google Chrome浏览器标题” 插件在Google Chrome浏览器中完成此操作, 以进行测试。这个插件将使我们能够编辑HTTP标头并添加有效的api_key(我们将使用数据库种子文件中包含的虚拟api_key 12345654321)。
好的, 现在我们可以测试了!
为了调用pair_programming_sessions API端点, 我们首先需要登录。我们将仅使用数据库种子文件中的电子邮件和密码组合, 然后通过Swagger将其提交给登录API端点, 如下所示。
文章图片
正如你在上面看到的, 将返回属于该用户的令牌, 这表明登录API可以按预期正常工作。现在, 我们可以使用该令牌成功执行GET /api/pair_programming_sessions.json操作。
文章图片
如图所示, 结果将作为格式正确的分层JSON对象返回。请注意, JSON项目反映了两个嵌套的一对多关联, 因为该项目具有多个评论, 并且一个评论具有多个代码示例。如果我们不以这种方式返回结构, 那么我们的API调用者将需要分别请求每个项目的审核, 这需要向我们的API端点提交N个查询。因此, 通过这种结构, 我们解决了N + 1查询性能问题。
本文总结 如此处所示, 你的API的全面规范有助于确保已实现的API正确, 充分地解决了预期的(和非预期的)用例。
尽管本教程中介绍的示例API是相当基本的, 但是我们展示的方法和技术可以作为使用Grape gem的任意复杂度更复杂的API的基础。本教程有望证明, Grape是一个有用且灵活的工具, 可以帮助在Rails应用程序中实现JSON API。请享用!
推荐阅读
- 开发人员的十大前端设计规则
- 通过Amazon Web Services提升生产力
- 在Satellizer中将Facebook登录集成到AngularJS App中
- 声明式编程(这是真的吗())
- 拥抱Sass(为什么应该停止使用Vanilla CSS)
- 如何引导和创建.NET项目
- tomcat 403 forbidden - 无法访问我在webapps中部署的项目
- 我们可以配置要在Tomcat的webapp目录中的相应位置部署的文件夹吗()
- JPA OneToMany,如何在控制台或webapp中打印时忽略字段ManyToOne