Alex

有的故事值得一直说下去.
Home » Latest Posts

我是来鼓吹使用 Rails 写 API 的。
原文在此:https://labs.kollegorna.se/blog/2015/04/build-an-api-now/
原文有一个很大的缺陷就是读者无法按照它的步骤一步一步的去实现一个可以运行的 demo, 这对经验丰富的开发 者可能不算是一个问题,但是对刚刚接触这方面知识的新手来说却是一个很大的遗憾,软件开发方面的知识重在 实践,只有动手做了,才能对知识掌握地更加牢靠,才能更好地在工作中去使用这些知识,所以我对原文的内容 做了一些补充修整,以期能够让读者边读边思考边实践。
原文的 demo 是一个类微博应用,为简单起见我们只使用 User 和 Micropost 模型,并且我们不使用 ActiveModel::Serializer, 而是使用 Jbuilder 作为 json 模版。
首先建立一个项目:build-an-api-rails-demo

$rails new build-an-api-rails-demo

加入第一个 API resource

BaseController

生成控制器:

# 我们不需要生成资源文件
$bundle exe rails g controller api/v1/base --no-assets

app/controllers/api/v1/base_controller.rb,

class Api::V1::BaseController < ApplicationController
# disable the CSRF token
protect_from_forgery with: :null_session
# disable cookies (no set-cookies header in response)
before_action :destroy_session
# disable the CSRF token
skip_before_action :verify_authenticity_token
def destroy_session
request.session_options[:skip] = true
end
end

在 BaseController 里我们禁止了 CSRF token 和 cookies

配置路由:

config/routes.rb,

namespace :api do
namespace :v1 do
resources :users, only:[:index, :create, :show, :update, :destroy]
# 原文有 microposts, 我们现在把它注释掉
# resources :microposts, only: [:index, :create, :show, :update, :destroy]
end
end

Api::V1::UsersController

生成控制器:

# 我们不需要生成资源文件
$bundle exe rails g controller api/v1/users --no-assets

app/controllers/api/v1/users_controller.rb,

class Api::V1::UsersController < Api::V1::BaseController
def show
@user = User.find(params[:id])
# 原文使用 Api::V1::UserSerializer
# 我们现在使用 app/views/api/v1/users/show.json.jbuilder
# render(json: Api::V1::UserSerializer.new(user).to_json)
end
end

app/views/api/v1/users/show.json.jbuilder,

json.user do
json.(@user, :id, :email, :name,  :activated, :admin, :created_at, :updated_at)
end

User 模型和 users 表

$bundle exe rails g model User

app/models/user.rb,

class User < ActiveRecord::Base
end

db/migrate/20150502072954_create_users.rb,

class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
t.string :name
t.datetime :activated
t.boolean :admin, default:false
t.timestamps null:false
end
end
end

数据迁移:

$bundle exe rake db:migrate

种子数据:
db/seeds.rb,

users = User.create([
{
email:'[email protected]',
name:'test-user-00',
activated:DateTime.now,
admin:false
},
{
email:'[email protected]',
name:'test-user-01',
activated:DateTime.now,
admin:false
}
])

创建种子数据:

$ bundle exe rake db:seed

现在我们可以测试一下 api 是否正常工作,我们可以先查看下相关 api 的路由,

$bundle exe rake routes

输出:

      Prefix Verb   URI Pattern                      Controller#Action
api_v1_users GET    /api/v1/users(.:format)          api/v1/users#index
POST   /api/v1/users(.:format)          api/v1/users#create
api_v1_user GET    /api/v1/users/:id(.:format)      api/v1/users#show
PATCH  /api/v1/users/:id(.:format)      api/v1/users#update
PUT    /api/v1/users/:id(.:format)      api/v1/users#update
DELETE /api/v1/users/:id(.:format)      api/v1/users#destroy

启动 rails 服务,

$bundle exe rails s

使用 curl 请求 api,

$curl -i http://localhost:3000/api/v1/users/1.json
{"user":{"id":1,"email":"[email protected]","name":"test-user-00","activated":"2015-05-02T07:47:14.697Z","admin":false,"created_at":"2015-05-02T07:47:14.708Z","updated_at":"2015-05-02T07:47:14.708Z"}}

恭喜,我们的 api 工作正常!

增加认证 (Authentication)

认证的过程是这样的:用户把她的用户名和密码通过 HTTP POST 请求发送到我们的 API (在这里我们使用 sessions 端点来处理这个请求), 如果用户名和密码匹配,我们 会把 token 发送给用户。这个 token 就是用来证明用户身份的凭证。然后在以后的每个请求中,我们都通过这个 token 来查找用户,如果没有找到用户则返回 401 错误。

给 User 模型增加 authentication_token 属性

$bundle exe rails g migration add_authentication_token_to_users

db/migrate/20150502123451_add_authentication_token_to_users.rb

class AddAuthenticationTokenToUsers < ActiveRecord::Migration
def change
add_column :users, :authentication_token, :string
end
end
$bundle exe rake db:migrate

生成 authentication_token

app/models/user.rb,

class User < ActiveRecord::Base
+ before_create :generate_authentication_token
+ def generate_authentication_token
+   loop do
+     self.authentication_token = SecureRandom.base64(64)
+     break if !User.find_by(authentication_token:authentication_token)
+   end
+ end
+ def reset_auth_token!
+   generate_authentication_token
+   save
+ end
end

和原文相比,我给 User 模型增加了一个 reset_auth_token! 方法,我这样做的理由主要有以下几点:

  1. 我觉得需要有一个方法帮助用户重置 authentication token, 而不仅仅是在创建用户时生成 authenticeation token;
  2. 如果用户的 token 被泄漏了,我们可以通过 reset_auth_token! 方法方便地重置用户 token;

sessions endpoint

生成 sessions 控制器,

# 我们不需要生成资源文件
$bundle exe rails g controller api/v1/sessions --no-assets
create  app/controllers/api/v1/sessions_controller.rb
invoke  erb
create    app/views/api/v1/sessions
invoke  test_unit
create    test/controllers/api/v1/sessions_controller_test.rb
invoke  helper
create    app/helpers/api/v1/sessions_helper.rb
invoke    test_unit

app/controllers/api/v1/sessions_controller.rb,

class Api::V1::SessionsController < Api::V1::BaseController
def create
@user = User.find_by(email:create_params[:email])
if @user && @user.authenticate(create_params[:password])
self.current_user = @user
# 我们使用 jbuilder
# render(
#   json: Api::V1::SessionSerializer.new(user, root: false).to_json,
#   status: 201
# )
else
return api_error(status:401)
end
end
private
def create_params
params.require(:user).permit(:email, :password)
end
end

现在我们还需要做一些原文没有提到的工作:

  1. 给 User 模型增加和 password 相关的属性;
  2. 给数据库中已存在的测试用户增加密码和 authentication token;
  3. 实现和 current_user 相关的方法;
  4. 实现 app/views/api/v1/sessions/create.json.jbuilder;
  5. 配置和 sessions 相关的路由;

给 User 模型增加和 password 相关的属性

在 Gemfile 里将 gem 'bcrypt' 这一行的注释取消

# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

app/models/user.rb,

class User < ActiveRecord::Base
+ has_secure_password
end

给 User 模型增加 password_digest 属性,

$bundle exe rails g migration add_password_digest_to_users

db/migrate/20150502134614_add_password_digest_to_users.rb,

class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
$bundle install
$bundle exe rake db:migrate

给数据库中已存在的测试用户增加密码和 authentication token

这个任务可以在 rails console 下完成,
首先启动 rails console,

$bundle exe rails c

然后在 rails console 里执行,

User.all.each {|user|
user.password = '123123'
user.reset_auth_token!
}

实现和 current_user 相关的方法

app/controllers/api/v1/base_controller.rb,

class Api::V1::BaseController < ApplicationController
+ attr_accessor :current_user
end

实现 app/views/api/v1/sessions/create.json.jbuilder

app/views/api/v1/sessions/create.json.jbuilder,

json.session do
json.(@user, :id, :name, :admin)
json.token @user.authentication_token
end

配置和 sessions 相关的路由

Rails.application.routes.draw do
namespace :api do
namespace :v1 do
+ resources :sessions, only:[:create]
end
end
end

现在我们做一个测试看是否能够顺利地拿到用户的 token, 我们使用下面的用户作为测试用户:

{
email:'[email protected]',
name:'test-user-00'
}
$curl -i -X POST -d "user[email][email protected]&user[password]=123123" http://localhost:3000/api/v1/sessions.json
{"session":{"id":1,"name":"test-user-00","admin":false,"token":"izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w=="}}

我们顺利地拿到了 token。
我们再做一个验证失败的测试。
我们使用一个错误的密码:fakepwd

curl -i -X POST -d "user[email][email protected]&user[password]=fakepwd" http://localhost:3000/api/v1/sessions.json

糟糕系统出错了:

NoMethodError (undefined method `api_error' for #<Api::V1::SessionsController:0x007fead422c178>):
app/controllers/api/v1/sessions_controller.rb:14:in `create'

原来我们没有实现 api_error 这个方法,那我们现在就实现 api_error 这个方法。
app/controllers/api/v1/base_controller.rb,

class Api::V1::BaseController < ApplicationController
+ def api_error(opts = {})
+   render nothing:true, status:opts[:status]
+ end
end

继续测试:

curl -i -X POST -d "user[email][email protected]&user[password]=fakepwd" http://localhost:3000/api/v1/sessions
HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/plain; charset=utf-8
Cache-Control: no-cache
X-Request-Id: a5349b47-d756-4830-84f8-0653577f936d
X-Runtime: 0.319768
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Sat, 02 May 2015 14:41:55 GMT
Content-Length: 0
Connection: Keep-Alive

此时服务器返回了 401 Unauthorized

Authenticate User

在前面的测试中,我们已经成功地拿到了用户的 token, 那么现在我们把 token 和 email 发给 API
看能否成功识别出用户。
首先在 Api::V1::BaseController 里实现 authenticate_user! 方法:
app/controllers/api/v1/base_controller.rb,

class Api::V1::BaseController < ApplicationController
+  def authenticate_user!
+    token, options = ActionController::HttpAuthentication::Token.token_and_options(request)
+    user_email = options.blank?? nil : options[:email]
+    user = user_email && User.find_by(email:user_email)
+    if user && ActiveSupport::SecurityUtils.secure_compare(user.authentication_token, token)
+      self.current_user = user
+    else
+      return unauthenticated!
+    end
+  end
end

ActionController::HttpAuthentication::Token 是 rails 自带的方法,可以参考 rails 文档 了解其详情。
当我们通过 user_email 拿到 user 后,通过 ActiveSupport::SecurityUtils.secure_compare
对 user.authentication_token 和从请求头里取到的 token 进行比较,如果匹配则认证成功,否则返回
unauthenticated!。这里使用了 secure_compare 对字符串进行比较,是为了防止时序攻击 (timing attack)
我们构造一个测试用例,这个测试用例包括以下一些步骤:

  1. 用户登录成功,服务端返回其 email, token 等数据
  2. 用户请求 API 更新其 name, 用户发送的 token 合法,更新成功
  3. 用户请求 API 更新其 name, 用户发送的 token 非法,更新失败

为了让用户能够更新其 name, 我们需要实现 user update API, 并且加入 before_action :authenticate_user!, only: [:update]
app/controllers/api/v1/users_controller.rb,

class Api::V1::UsersController < Api::V1::BaseController
+ before_action :authenticate_user!, only:[:update]
+ def update
+   @user = User.find(params[:id])
+   @user.update_attributes(update_params)
+ end
+ private
+ def update_params
+   params.require(:user).permit(:name)
+ end
end

app/views/api/v1/users/update.json.jbuilder,

json.user do
json.(@user, :id, :name)
end

现在我们进行测试,测试用户是:

{
id:1,
email:'[email protected]',
name:'test-user-00',
authentication_token:'izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w=='
}
$curl -i -X PUT -d "user[name]=gg-user" \
--header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==,\[email protected]" \
http://localhost:3000//api/v1/users/1
{"user":{"id":1,"name":"gg-user"}}

我们看到 user name 已经成功更新为 gg-user。
读者们请注意:你们自己测试时需要将 token 换为你们自己生成的 token。
我们使用一个非法的 token 去请求 API, 看看会发生什么状况。

curl -i -X PUT -d "user[name]=bb-user" \
--header "Authorization: Token token=invalid token,\[email protected]" \
http://localhost:3000//api/v1/users/1

服务器出现错误:

NoMethodError (undefined method `unauthenticated!' for #<Api::V1::UsersController:0x007fead6108d80>)

接下来我们实现 unauthenticated! 方法。
app/controllers/api/v1/base_controller.rb,

class Api::V1::BaseController < ApplicationController
+ def unauthenticated!
+   api_error(status:401)
+ end
end

继续上面的测试:

curl -i -X PUT -d "user[name]=bb-user" \
--header "Authorization: Token token=invalid token,\[email protected]" \
http://localhost:3000//api/v1/users/1
HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/plain; charset=utf-8
Cache-Control: no-cache
X-Request-Id: 8cf07968-1fd0-4041-866a-ddea49af11d3
X-Runtime: 0.005578
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Sun, 03 May 2015 05:51:52 GMT
Content-Length: 0
Connection: Keep-Alive

服务器返回 401 Unauthorized, 并且 user name 没有被更新。

增加授权 (Authorization)

上面的测试有个问题,就是当前登录的用户可以把其他用户的 name 更新,这个应该是不被允许的,所以我们 还需要增加一个权限认证的机制。在这里我们使用 Pundit 来 实现权限认证。

安装 pundit

Gemfile,

+ gem 'pundit'
$bundle install

app/controllers/api/v1/base_controller.rb,

class Api::V1::BaseController < ApplicationController
+ include Pundit
end
$bundle exe rails g pundit:install
create  app/policies/application_policy.rb

将 policies 目录放到 rails 的自动加载路径中:
config/application.rb,

module BuildAnApiRailsDemo
class Application < Rails::Application
+    config.autoload_paths << Rails.root.join('app/policies')
end
end

创建和 user 相关的权限机制

app/policies/user_policy.rb,


class UserPolicy < ApplicationPolicy
def show?
return true
end
def create?
return true
end
def update?
return true if user.admin?
return true if record.id == user.id
end
def destroy?
return true if user.admin?
return true if record.id == user.id
end
class Scope < ApplicationPolicy::Scope
def resolve
scope.all
end
end
end

使用 UserPolicy

app/controllers/api/v1/users_controller.rb,

class Api::V1::UsersController < Api::V1::BaseController
def update
@user = User.find(params[:id])
+   return api_error(status:403) if !UserPolicy.new(current_user, @user).update?
@user.update_attributes(update_params)
end
end

测试:

$curl -i -X PUT -d "user[name]=gg-user" \
--header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==,\[email protected]" \
http://localhost:3000//api/v1/users/2.json
HTTP/1.1 403 Forbidden
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block

注意我们测试的 url 地址是 http://localhost:3000//api/v1/users/2, 也就是说我们在更新 id 为 2 的那个用户的 name。此时服务器返回的是 403 Forbidden。
pundit 提供了更简便的 authorize 方法为我们做权限认证的工作。


class Api::V1::UsersController < Api::V1::BaseController
def update
@user = User.find(params[:id])
# return api_error(status: 403) if !UserPolicy.new(current_user, @user).update?
+   authorize @user, :update?
@user.update_attributes(update_params)
end
end

测试:


$curl -i -X PUT -d "user[name]=gg-user" \
--header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==,\[email protected]" \
http://localhost:3000//api/v1/users/2.json

此时服务器报 Pundit::NotAuthorizedError 错误,

Pundit::NotAuthorizedError (not allowed to update?

我们可以使用 rescue_from 捕捉 Pundit::NotAuthorizedError 这类异常。

class Api::V1::BaseController < ApplicationController
include Pundit
+  rescue_from Pundit::NotAuthorizedError, with: :deny_access
+  def deny_access
+    api_error(status:403)
+  end
end

测试:

$curl -i -X PUT -d "user[name]=gg-user" \
--header "Authorization: Token token=izrFiion7xEe2ccTj0v0mOcuNoT3FvpPqI31WLSCEBLvuz4xSr0d9+VI2+xVvAJjECIoju5MaoytEcg6Md773w==,\[email protected]" \
http://localhost:3000//api/v1/users/2
HTTP/1.1 403 Forbidden
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/plain; charset=utf-8

这次服务器直接返回 403 Forbidden

分页

我们现在要实现一个展示用户发的微博的 API, 如果用户的微博数量很多,那么我们应该用上分页。

建立 Micropost 模型

$bundle exe rails g model Micropost

db/migrate/20150503131743_create_microposts.rb,

class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.string :title
t.text :content
t.integer :user_id
t.timestamps null:false
end
end
end

执行:

$bundle exe rake db:migrate

为 id 为 1 的用户创建 100 条微博纪录:
lib/tasks/data.rake,

namespace :data do
task :create_microposts => [:environment] do
user = User.find(1)
100.times do |i|
Micropost.create(user_id:user.id, title:"title-#{i}", content:"content-#{i}")
end
end
end

执行:

$bundle exe rake data:create_microposts

Api::V1::MicropostsController

执行:

$bundle exe rails g controller api/v1/microposts --no-assets

配置路由:
config/routes.rb,

Rails.application.routes.draw do
namespace :api do
namespace :v1 do
+    scope path:'/user/:user_id' do
+      resources :microposts, only:[:index]
+    end
end
end
end

此时和 microposts 相关的路由如下:

api_v1_microposts GET    /api/v1/user/:user_id/microposts(.:format) api/v1/microposts#index

我们使用 kaminari 这个 gem 进行分页。
安装 kaminari,
Gemfile

+ gem 'kaminari'

执行:

$ bundle install

app/models/user.rb

class User < ActiveRecord::Base
+ has_many :microposts
end

app/controllers/api/v1/microposts_controller.rb

class Api::V1::MicropostsController < Api::V1::BaseController
+  def index
+    user = User.find(params[:user_id])
+    @microposts = paginate(user.microposts)
+  end
end

app/controllers/api/v1/base_controller.rb

class Api::V1::BaseController < ApplicationController
def paginate(resource)
resource = resource.page(params[:page] || 1)
if params[:per_page]
resource = resource.per(params[:per_page])
end
return resource
end
end

app/helpers/application_helper.rb

module ApplicationHelper
+  def paginate_meta_attributes(json, object)
+    json.(object,
+          :current_page,
+          :next_page,
+          :prev_page,
+          :total_pages,
+          :total_count)
+  end
end

app/views/api/v1/microposts/index.json.jbuilder,

json.paginate_meta do
paginate_meta_attributes(json, @microposts)
end
json.microposts do
json.array! @microposts do |micropost|
json.(micropost, :id, :title, :content)
end
end

测试:

$curl -i -X GET http://localhost:3000/api/v1/user/1/microposts.json?per_page=3
{
"paginate_meta": {
"current_page":1,
"next_page":2,
"prev_page":null,
"total_pages":34,
"total_count":100
},
"microposts":[
{"id":1,"title":"title-0","content":"content-0"},
{"id":2,"title":"title-1","content":"content-1"},
{"id":3,"title":"title-2","content":"content-2"}
]
}

API 调用频率限制 (Rate Limit)

我们使用 redis-throttle 来实现这个功能。
Gemfile,

gem 'redis-throttle', git:'git://github.com/andreareginato/redis-throttle.git'

执行,

$bundle install

集成到 Rails 中:
config/application.rb,

# At the top of config/application.rb
+ require 'rack/redis_throttle'
class Application < Rails::Application
# Limit the daily number of requests to 3
# 为了测试我们把 limit 设置为 3
+ config.middleware.use Rack::RedisThrottle::Daily, max:3
end

我们开始测试,请求 http://localhost:3000/api/v1/users/1 4 次看会出现什么结果。
前面 3 次请求一切正常,

curl -i http://localhost:3000/api/v1/users/1
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
X-Ratelimit-Limit: 3
X-Ratelimit-Remaining: 0
Etag: W/"eb58510a43ebc583cf61de35b6d20093"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: bbe7437b-ba6e-4cfd-a4ef-49eec4c611fd
X-Runtime: 0.014384
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Thu, 07 May 2015 13:03:31 GMT
Content-Length: 199
Connection: Keep-Alive
{"user":{"id":1,"email":"[email protected]","name":"gg-user","activated":"2015-05-02T07:47:14.697Z","admin":false,"created_at":"2015-05-02T07:47:14.708Z","updated_at":"2015-05-03T05:40:24.931Z"}}

我们注意服务器返回的两个响应头:X-Ratelimit-Limit 和 X-Ratelimit-Remaining,
X-Ratelimit-Limit 的值一直为 3,表示请求的限制值,
而 X-Ratelimit-Remaining 每请求一次,其值会减 1,直到为 0。
第 4 次请求出现 403 Forbidden, 这说明 redis-throttle 起到了其应有的作用。

curl -i http://localhost:3000/api/v1/users/1
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
Cache-Control: no-cache
X-Request-Id: fd646f00-a6a8-411d-b5e4-24856c63b078
X-Runtime: 0.002375
Server: WEBrick/1.3.1 (Ruby/2.1.2/2014-05-08)
Date: Thu, 07 May 2015 13:03:33 GMT
Content-Length: 35
Connection: Keep-Alive
403 Forbidden (Rate Limit Exceeded)

redis-throttle 的 redis 连接默认是 redis://localhost:6379/0, 你也可以通过设置环境变量
ENV['REDIS_RATE_LIMIT_URL'] 来改变 redis-throttle 的 redis 连接。

CORS

CORS 是 Cross Origin Resource Sharing 的缩写。简单地说 CORS 可以允许其他域名的网页通过 AJAX 请求你的 API。
我们可以使用 rack-cors gem 来帮助我们的 API 实现 CORS。
Gemfile,

+ gem 'rack-cors'

config/application.rb,

module BuildAnApiRailsDemo
class Application < Rails::Application
+    config.middleware.insert_before 0, "Rack::Cors" do
+      allow do
+        origins '*'
+        resource '*', :headers => :any, :methods => [:get, :post, :put, :patch, :delete, :options, :head]
+      end
+    end
end
end

Version 2 API

随着我们的业务发展,我们的 API 需要做较大的改变,同时我们需要保持 Version 1 API, 所以我们 开始开发 Version 2 API。

routes

config/routes.rb,

Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only:[:index, :create, :show, :update, :destroy]
# resources :microposts, only: [:index, :create, :show, :update, :destroy]
resources :sessions, only:[:create]
scope path:'/user/:user_id' do
resources :microposts, only:[:index]
end
end
+    namespace :v2 do
+      resources :users, only:[:index, :create, :show, :update, :destroy]
+      resources :sessions, only:[:create]
+      scope path:'/user/:user_id' do
+        resources :microposts, only:[:index]
+      end
+    end
end
end

controller

生成 API::V2::UsersController, 其他控制器的生成类似

$bundle exe rails g controller api/v2/users --no-assets

app/controllers/api/v2/users_controller.rb,


class Api::V2::UsersController < Api::V1::UsersController
def show
@user = User.find(params[:id])
end
end

app/vies/api/v2/users/show.json.jbuilder,

json.user do
json.(@user, :id, :email, :name,  :activated, :admin, :created_at, :updated_at)
end

测试:

$curl -i http://localhost:3000/api/v2/users/1.json
{"user":{"id":1,"email":"[email protected]","name":"gg-user","activated":"2015-05-02T07:47:14.697Z","admin":false,"created_at":"2015-05-02T07:47:14.708Z","updated_at":"2015-05-03T05:40:24.931Z"}}%

文档

原文提到了下面的几种文档工具:

  1. swagger-rails 和 swagger-docs
  2. apipie-rails
  3. slate

和原文一样,我也喜欢使用 slate 作为文档工具。

将 slate 集成到项目中

创建 docs 目录,

$mkdirapp/docs

集成 slate,

$cdapp/docs
$git clone [email protected]:tripit/slate.git
$rm -rf slate/.git
$cdslate
$bundle install

配置构建目录,app/docs/slate/config.rb


+ set :build_dir, '../../../public/docs/'

现在我们开始编写获取用户信息这个 API 的文档。
app/docs/slate/source/index.md,

---
title: API Reference
language_tabs:
- ruby
toc_footers:
- <a href='http://github.com/tripit/slate'>Documentation Powered by Slate</a>
includes:
- errors
search: true
---
# 介绍
API 文档
# 获取用户信息
## V1
## HTTP 请求
`GET http://my-site/api/v1/users/<id>`
## 请求参数
参数名 | 是否必需 | 描述
-----| --------| -------
id   |  是      | 用户 id|
## 响应
\```json
{
"user":
{
"id":1,
"email":"[email protected]",
"name":"test-user-00",
"activated":"2015-05-02T07:47:14.697Z",
"admin":false,
"created_at":"2015-05-02T07:47:14.708Z",
"updated_at":"2015-05-02T07:47:14.708Z"
}
}
\```

注意:index.md 范例里的 json 代码语法高亮部分有转义字符,直接复制可能没法看到语法高亮效果,在实际使用时需要将 ``` 前面的 '\' 符号去掉。
build 脚本
docs_build.sh,

#!/bin/bash
cdapp/docs/slate
bundle execmiddleman build --clean

build docs,

$chmod +x docs_build.sh
$./docs_build.sh

可以通过 http://localhost:3000/docs/index.html 访问文档

f0e5d0e6e4ebd54562103e7163b5343d

给 API 文档添加访问控制

配置路由:
routes.rb,

+ get '/docs/index', to:'docs#index'

建立相关控制器:

$bundle exe rails g controller docs

app/controllers/docs_controller.rb,

class DocsController < ApplicationController
USER_NAME, PASSWORD = 'doc_reader', '123123'
before_filter :basic_authenticate
layout false
def index
end
private
def basic_authenticate
authenticate_or_request_with_http_basic do |user_name, password|
user_name == USER_NAME && password == PASSWORD
end
end
end

同时我们需要把 public/docs/index.html 文件转移到 app/views/docs/ 目录下面,我们
可以更改 docs_build.sh 脚本,注意 docs_build.sh 应该放在项目的根目录下,比如:/path/to/build-an-api-rails-demo/docs_build.sh,

#!/bin/bash
app_dir=`pwd`
cd $app_dir/app/docs/slate
bundle execmiddleman build --clean
cd $app_dir
mv $app_dir/public/docs/index.html $app_dir/app/views/docs

重新 build 文档,

$./docs_build.sh

浏览器访问 http://localhost:3000/docs/index.html,

a783135457f2bfb28ff002c43a2f8d10

提示需要输入用户名和密码,我们输入正确的用户名 (doc_reader) 和密码 (123123) 后就可以正常访问文档了,

项目代码

build-an-api-rails-demo

1、在运行 npm install electron 时,有些用户会偶尔遇到安装问题。
在大多数情况下,这些错误都是由网络问题导致,而不是因为 electron npm 包的问题。 如 ELIFECYCLE、EAI_AGAIN、ECONNRESET 和 ETIMEDOUT 等错误都是此类网络问题的标志。 最佳的解决方法是尝试切换网络,或是稍后再尝试安装。
如果通过 npm 安装失败,你也可以尝试通过从 https://github.com/electron/electron/releases 直接下载 Electron
npm下载electron失败,可以使用yarn来下载electron。先npm install -g yarn,下载yarn,再yarn config set ELECTRON_MIRROR https://npmmirror.com/mirrors/electron/指定electron的镜像,最后再yarn add electron。就大功告成了!
2、electron-builder打包electron-v13.6.9-win32-x64.zip下载失败
按照错误提示,是 electron-v13.6.9-win32-x64.zip这个文件下载失败,可以利用报错信息中的https://github.com/electron/electron/releases/download/v13.6.9/electron-v13.6.9-win32-x64.zip这个地址进行下载,或者到GitHub上下载。
https://npm.taobao.org/mirrors/electron/26.4.1/electron-v26.4.1-win32-x64.zip
下载后找到以下位置,将压缩包解压到electron的cache文件夹中。
C:\Users\admin\AppData\Local\electron\Cache
3、electron-builder打包winCodeSign-2.6.0.7z下载失败
https://registry.npmmirror.com/binary.html?path=electron-builder-binaries/winCodeSign-2.6.0/winCodeSign-2.6.0.7z
下载后找到以下位置,将压缩包解压到electron-builder的cache文件夹中。
C:\Users\admin\AppData\Local\electron-builder\Cache
 
4、nsis或nsis-resources下载失败也是一样的处理
https://registry.npmmirror.com/binary.html?path=electron-builder-binaries/
C:\Users\admin\AppData\Local\electron-builder\Cache\nsis
 
五、依赖下载
这里有你需要的依赖文件可以手动下载
web 站点
https://npmmirror.com/
二进制文件镜像
https://npm.taobao.org/mirrors/
https://registry.npmmirror.com/binary.html
六、淘宝镜像
1、安装 cnpm 淘宝镜像:
npm install -g cnpm --registry=https://registry.npm.taobao.org
npm install -g cnpm --registry=https://registry.npmmirror.com
1
2
3
2、将npm设置为淘宝镜像:
npm config set registry https://registry.npm.taobao.org
npm config set registry https://registry.npmmirror.com
1
2
3
3、通过npm查看cnpm镜像设置:
npm config get registry
1
4、通过cnpm查看cnpm镜像设置:(相当于使用cnpm)
cnpm config get registry
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/shanghai597/article/details/134122377

  • 在终端某个群组添加机器人之后,创建者可以在机器人详情页看的该机器人特有的webhookurl。开发者可以按以下说明a向这个地址发起HTTP POST 请求,即可实现给该群组发送消息。下面举个简单的例子.
    假设webhook是:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
特别特别要注意:一定要保护好机器人的webhook地址,避免泄漏!不要分享到github、博客等可被公开查阅的地方,否则坏人就可以用你的机器人来发垃圾消息了。

以下是用curl工具往群组推送文本消息的示例(注意要将url替换成你的机器人webhook地址,content必须是utf8编码):

  1. curl'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=693axxx6-7aoc-4bc4-97a0-0ec2sifa5aaa'\
  2. -H'Content-Type: application/json'\
  3. -d'
  4. {
  5. "msgtype": "text",
  6. "text": {
  7. "content": "hello world"
  8. }
  9. }'
  • 当前自定义机器人支持文本(text)、markdown(markdown)、图片(image)、图文(news)四种消息类型。
  • 机器人的text/markdown类型消息支持在content中使用<@userid>扩展语法来@群成员

消息类型及数据格式

文本类型

  1. {
  2. "msgtype": "text",
  3. "text": {
  4. "content": "广州今日天气:29度,大部分多云,降雨概率:60%",
  5. "mentioned_list":["wangqing","@all"],
  6. "mentioned_mobile_list":["13800001111","@all"]
  7. }
  8. }
参数 是否必填 说明
msgtype 消息类型,此时固定为text
content 文本内容,最长不超过2048个字节,必须是utf8编码
mentioned_list userid的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userid,可以使用mentioned_mobile_list
mentioned_mobile_list 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人

0-2

markdown类型

  1. {
  2. "msgtype": "markdown",
  3. "markdown": {
  4. "content": "实时新增用户反馈<font color=\"warning\">132例</font>,请相关同事注意。\n
  5. >类型:<font color=\"comment\">用户反馈</font>
  6. >普通用户反馈:<font color=\"comment\">117例</font>
  7. >VIP用户反馈:<font color=\"comment\">15例</font>"
  8. }
  9. }
参数 是否必填 说明
msgtype 消息类型,此时固定为markdown
content markdown内容,最长不超过4096个字节,必须是utf8编码

0-1
目前支持的markdown语法是如下的子集:

  1. 标题 (支持1至6级标题,注意#与文字中间要有空格)
    1. # 标题一
    2. ## 标题二
    3. ### 标题三
    4. #### 标题四
    5. ##### 标题五
    6. ###### 标题六

  2. 加粗
    1. **bold**

  3. 链接
    1. [这是一个链接](http://work.weixin.qq.com/api/doc)

  4. 行内代码段(暂不支持跨行)
    1. `code`

  5. 引用
    1. > 引用文字

  6. 字体颜色(只支持3种内置颜色)
    1. <font color="info">绿色</font>
    2. <font color="comment">灰色</font>
    3. <font color="warning">橙红色</font>

图片类型

  1. {
  2. "msgtype": "image",
  3. "image": {
  4. "base64": "DATA",
  5. "md5": "MD5"
  6. }
  7. }
参数 是否必填 说明
msgtype 消息类型,此时固定为image
base64 图片内容的base64编码
md5 图片内容(base64编码前)的md5值
注:图片(base64编码前)最大不能超过2M,支持JPG,PNG格式

0

图文类型

  1. {
  2. "msgtype": "news",
  3. "news": {
  4. "articles" : [
  5. {
  6. "title" : "中秋节礼品领取",
  7. "description" : "今年中秋节公司有豪礼相送",
  8. "url" : "www.qq.com",
  9. "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"
  10. }
  11. ]
  12. }
  13. }
参数 是否必填 说明
msgtype 消息类型,此时固定为news
articles 图文消息,一个图文消息支持1到8条图文
title 标题,不超过128个字节,超过会自动截断
description 描述,不超过512个字节,超过会自动截断
url 点击后跳转的链接。
picurl 图文消息的图片链接,支持JPG、PNG格式,较好的效果为大图 1068*455,小图150*150。

0-3

文件类型

  1. {
  2. "msgtype": "file",
  3. "file": {
  4. "media_id": "3a8asd892asd8asd"
  5. }
  6. }
参数 是否必填 说明
msgtype 消息类型,此时固定为file
media_id 文件id,通过下文的文件上传接口获取

202005122030.3ce0bde714

模版卡片类型

文本通知模版卡片

0-1

  1. {
  2. "msgtype":"template_card",
  3. "template_card":{
  4. "card_type":"text_notice",
  5. "source":{
  6. "icon_url":"https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0",
  7. "desc":"企业微信",
  8. "desc_color":0
  9. },
  10. "main_title":{
  11. "title":"欢迎使用企业微信",
  12. "desc":"您的好友正在邀请您加入企业微信"
  13. },
  14. "emphasis_content":{
  15. "title":"100",
  16. "desc":"数据含义"
  17. },
  18. "quote_area":{
  19. "type":1,
  20. "url":"https://work.weixin.qq.com/?from=openApi",
  21. "appid":"APPID",
  22. "pagepath":"PAGEPATH",
  23. "title":"引用文本标题",
  24. "quote_text":"Jack:企业微信真的很好用~\nBalian:超级好的一款软件!"
  25. },
  26. "sub_title_text":"下载企业微信还能抢红包!",
  27. "horizontal_content_list":[
  28. {
  29. "keyname":"邀请人",
  30. "value":"张三"
  31. },
  32. {
  33. "keyname":"企微官网",
  34. "value":"点击访问",
  35. "type":1,
  36. "url":"https://work.weixin.qq.com/?from=openApi"
  37. },
  38. {
  39. "keyname":"企微下载",
  40. "value":"企业微信.apk",
  41. "type":2,
  42. "media_id":"MEDIAID"
  43. }
  44. ],
  45. "jump_list":[
  46. {
  47. "type":1,
  48. "url":"https://work.weixin.qq.com/?from=openApi",
  49. "title":"企业微信官网"
  50. },
  51. {
  52. "type":2,
  53. "appid":"APPID",
  54. "pagepath":"PAGEPATH",
  55. "title":"跳转小程序"
  56. }
  57. ],
  58. "card_action":{
  59. "type":1,
  60. "url":"https://work.weixin.qq.com/?from=openApi",
  61. "appid":"APPID",
  62. "pagepath":"PAGEPATH"
  63. }
  64. }
  65. }

请求参数

参数 类型 必须 说明
msgtype String 消息类型,此时的消息类型固定为template_card
template_card Object 具体的模版卡片参数

template_card的参数说明

参数 类型 必须 说明
card_type String 模版卡片的模版类型,文本通知模版卡片的类型为text_notice
source Object 卡片来源样式信息,不需要来源样式可不填写
source.icon_url String 来源图片的url
source.desc String 来源图片的描述,建议不超过13个字
source.desc_color Int 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色
main_title Object 模版卡片的主要内容,包括一级标题和标题辅助信息
main_title.title String 一级标题,建议不超过26个字。模版卡片主要内容的一级标题main_title.title和二级普通文本sub_title_text必须有一项填写
main_title.desc String 标题辅助信息,建议不超过30个字
emphasis_content Object 关键数据样式
emphasis_content.title String 关键数据样式的数据内容,建议不超过10个字
emphasis_content.desc String 关键数据样式的数据描述内容,建议不超过15个字
quote_area Object 引用文献样式,建议不与关键数据共用
quote_area.type Int 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序
quote_area.url String 点击跳转的url,quote_area.type是1时必填
quote_area.appid String 点击跳转的小程序的appid,quote_area.type是2时必填
quote_area.pagepath String 点击跳转的小程序的pagepath,quote_area.type是2时选填
quote_area.title String 引用文献样式的标题
quote_area.quote_text String 引用文献样式的引用文案
sub_title_text String 二级普通文本,建议不超过112个字。模版卡片主要内容的一级标题main_title.title和二级普通文本sub_title_text必须有一项填写
horizontal_content_list Object[] 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6
horizontal_content_list.type Int 链接类型,0或不填代表是普通文本,1 代表跳转url,2 代表下载附件,3 代表@员工
horizontal_content_list.keyname String 二级标题,建议不超过5个字
horizontal_content_list.value String 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过26个字
horizontal_content_list.url String 链接跳转的url,horizontal_content_list.type是1时必填
horizontal_content_list.media_id String 附件的media_id,horizontal_content_list.type是2时必填
horizontal_content_list.userid String 被@的成员的userid,horizontal_content_list.type是3时必填
jump_list Object[] 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3
jump_list.type Int 跳转链接类型,0或不填代表不是链接,1 代表跳转url,2 代表跳转小程序
jump_list.title String 跳转链接样式的文案内容,建议不超过13个字
jump_list.url String 跳转链接的url,jump_list.type是1时必填
jump_list.appid String 跳转链接的小程序的appid,jump_list.type是2时必填
jump_list.pagepath String 跳转链接的小程序的pagepath,jump_list.type是2时选填
card_action Object 整体卡片的点击跳转事件,text_notice模版卡片中该字段为必填项
card_action.type Int 卡片跳转类型,1 代表跳转url,2 代表打开小程序。text_notice模版卡片中该字段取值范围为[1,2]
card_action.url String 跳转事件的url,card_action.type是1时必填
card_action.appid String 跳转事件的小程序的appid,card_action.type是2时必填
card_action.pagepath String 跳转事件的小程序的pagepath,card_action.type是2时选填

图文展示模版卡片

0

  1. {
  2. "msgtype":"template_card",
  3. "template_card":{
  4. "card_type":"news_notice",
  5. "source":{
  6. "icon_url":"https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0",
  7. "desc":"企业微信",
  8. "desc_color":0
  9. },
  10. "main_title":{
  11. "title":"欢迎使用企业微信",
  12. "desc":"您的好友正在邀请您加入企业微信"
  13. },
  14. "card_image":{
  15. "url":"https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0",
  16. "aspect_ratio":2.25
  17. },
  18. "image_text_area":{
  19. "type":1,
  20. "url":"https://work.weixin.qq.com",
  21. "title":"欢迎使用企业微信",
  22. "desc":"您的好友正在邀请您加入企业微信",
  23. "image_url":"https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0"
  24. },
  25. "quote_area":{
  26. "type":1,
  27. "url":"https://work.weixin.qq.com/?from=openApi",
  28. "appid":"APPID",
  29. "pagepath":"PAGEPATH",
  30. "title":"引用文本标题",
  31. "quote_text":"Jack:企业微信真的很好用~\nBalian:超级好的一款软件!"
  32. },
  33. "vertical_content_list":[
  34. {
  35. "title":"惊喜红包等你来拿",
  36. "desc":"下载企业微信还能抢红包!"
  37. }
  38. ],
  39. "horizontal_content_list":[
  40. {
  41. "keyname":"邀请人",
  42. "value":"张三"
  43. },
  44. {
  45. "keyname":"企微官网",
  46. "value":"点击访问",
  47. "type":1,
  48. "url":"https://work.weixin.qq.com/?from=openApi"
  49. },
  50. {
  51. "keyname":"企微下载",
  52. "value":"企业微信.apk",
  53. "type":2,
  54. "media_id":"MEDIAID"
  55. }
  56. ],
  57. "jump_list":[
  58. {
  59. "type":1,
  60. "url":"https://work.weixin.qq.com/?from=openApi",
  61. "title":"企业微信官网"
  62. },
  63. {
  64. "type":2,
  65. "appid":"APPID",
  66. "pagepath":"PAGEPATH",
  67. "title":"跳转小程序"
  68. }
  69. ],
  70. "card_action":{
  71. "type":1,
  72. "url":"https://work.weixin.qq.com/?from=openApi",
  73. "appid":"APPID",
  74. "pagepath":"PAGEPATH"
  75. }
  76. }
  77. }

请求参数

参数 类型 必须 说明
msgtype String 模版卡片的消息类型为template_card
template_card Object 具体的模版卡片参数

template_card的参数说明

参数 类型 必须 说明
card_type String 模版卡片的模版类型,图文展示模版卡片的类型为news_notice
source Object 卡片来源样式信息,不需要来源样式可不填写
source.icon_url String 来源图片的url
source.desc String 来源图片的描述,建议不超过13个字
source.desc_color Int 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色
main_title Object 模版卡片的主要内容,包括一级标题和标题辅助信息
main_title.title String 一级标题,建议不超过26个字
main_title.desc String 标题辅助信息,建议不超过30个字
card_image Object 图片样式
card_image.url String 图片的url
card_image.aspect_ratio Float 图片的宽高比,宽高比要小于2.25,大于1.3,不填该参数默认1.3
image_text_area Object 左图右文样式
image_text_area.type Int 左图右文样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序
image_text_area.url String 点击跳转的url,image_text_area.type是1时必填
image_text_area.appid String 点击跳转的小程序的appid,必须是与当前应用关联的小程序,image_text_area.type是2时必填
image_text_area.pagepath String 点击跳转的小程序的pagepath,image_text_area.type是2时选填
image_text_area.title String 左图右文样式的标题
image_text_area.desc String 左图右文样式的描述
image_text_area.image_url String 左图右文样式的图片url
quote_area Object 引用文献样式,建议不与关键数据共用
quote_area.type Int 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序
quote_area.url String 点击跳转的url,quote_area.type是1时必填
quote_area.appid String 点击跳转的小程序的appid,quote_area.type是2时必填
quote_area.pagepath String 点击跳转的小程序的pagepath,quote_area.type是2时选填
quote_area.title String 引用文献样式的标题
quote_area.quote_text String 引用文献样式的引用文案
vertical_content_list Object[] 卡片二级垂直内容,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过4
vertical_content_list.title String 卡片二级标题,建议不超过26个字
vertical_content_list.desc String 二级普通文本,建议不超过112个字
horizontal_content_list Object[] 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6
horizontal_content_list.type Int 模版卡片的二级标题信息内容支持的类型,1是url,2是文件附件
horizontal_content_list.keyname String 二级标题,建议不超过5个字
horizontal_content_list.value String 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过26个字
horizontal_content_list.url String 链接跳转的url,horizontal_content_list.type是1时必填
horizontal_content_list.media_id String 附件的media_id,horizontal_content_list.type是2时必填
jump_list Object[] 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3
jump_list.type Int 跳转链接类型,0或不填代表不是链接,1 代表跳转url,2 代表跳转小程序
jump_list.title String 跳转链接样式的文案内容,建议不超过13个字
jump_list.url String 跳转链接的url,jump_list.type是1时必填
jump_list.appid String 跳转链接的小程序的appid,jump_list.type是2时必填
jump_list.pagepath String 跳转链接的小程序的pagepath,jump_list.type是2时选填
card_action Object 整体卡片的点击跳转事件,news_notice模版卡片中该字段为必填项
card_action.type Int 卡片跳转类型,1 代表跳转url,2 代表打开小程序。news_notice模版卡片中该字段取值范围为[1,2]
card_action.url String 跳转事件的url,card_action.type是1时必填
card_action.appid String 跳转事件的小程序的appid,card_action.type是2时必填
card_action.pagepath String 跳转事件的小程序的pagepath,card_action.type是2时选填

消息发送频率限制

每个机器人发送的消息不能超过20条/分钟。

文件上传接口

素材上传得到media_id,该media_id仅三天内有效
media_id只能是对应上传文件的机器人可以使用

请求方式:POST(HTTPS
请求地址:https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=KEY&type=TYPE
使用multipart/form-data POST上传文件, 文件标识名为”media”
参数说明:

参数 必须 说明
key 调用接口凭证, 机器人webhookurl中的key参数
type 固定传file

POST的请求包中,form-data中媒体文件标识,应包含有 filename、filelength、content-type等信息

filename标识文件展示的名称。比如,使用该media_id发消息时,展示的文件名由该字段控制

请求示例:

  1. POST https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa&type=file HTTP/1.1
  2. Content-Type:multipart/form-data;boundary=-------------------------acebdf13572468
  3. Content-Length: 220
  4. ---------------------------acebdf13572468
  5. Content-Disposition:form-data;name="media";filename="wework.txt";filelength=6
  6. Content-Type:application/octet-stream
  7. mytext
  8. ---------------------------acebdf13572468--

返回数据:

  1. {
  2. "errcode": 0,
  3. "errmsg": "ok",
  4. "type": "file",
  5. "media_id": "1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0",
  6. "created_at": "1380000000"
  7. }

参数说明:

参数 说明
type 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file)
media_id 媒体文件上传后获取的唯一标识,3天内有效
created_at 媒体文件上传时间戳

上传的文件限制:

  • 要求文件大小在5B~20M之间

# 在Ruby on Rails中,可以通过创建一个自定义中间件来实现登录过滤功能。
# 在app/middleware/login_required_middleware.rb中创建以下代码:
class LoginRequiredMiddleware
def initialize(app)
@app = app
end
def call(env)
# 检查请求的路径是否需要登录
if needs_login_path?(env)
# 如果用户未登录,则重定向到登录页面
if not_logged_in?(env)
[302, {'Content-Type' => 'text/html', 'Location' => '/login'}, []]
else
# 如果用户已登录,则继续请求处理
@app.call(env)
end
else
# 如果不需要登录,则直接处理请求
@app.call(env)
end
end
private
def needs_login_path?(env)
# 定义哪些路径需要登录,例如排除登录和注册页面
not_whitelisted_paths = ['/login', '/sign_up']
not_whitelisted_paths.none? { |path| env['PATH_INFO'] == path }
end
def not_logged_in?(env)
# 根据你的应用逻辑检查用户是否已登录,例如检查session中的用户标识
!!env['rack.session']['user_id'].nil?
end
end
# 然后,在config/application.rb中添加以下代码以启用中间件:
module YourApp
class Application < Rails::Application
# 其他配置...
config.app_middleware.use LoginRequiredMiddleware
end
end

准备工作

2023-05-31 11:58 更新
Webpack 5 对 Node.js 的版本要求最低是 10.13.0 (LTS),因此,如果你还在使用旧版本的 Node.js,请升级它们。

升级 webpack 4 及其相关的插件/加载器

  1. 升级 webpack 4 至最新的可用版本。
  • 当使用webpack >= 4时,升级到最新的webpack 5版本无需额外的操作。
  • 如果您使用的 webpack 版本小于 4,请查看 webpack 4 迁移指南

2. 升级 webpack-cli 到最新的可用版本(如已使用的情况下)
3. 升级所有使用到的插件和加载器为最新的可用版本。
部分插件和加载器可能会有一个 beta 版本,必须使用它们才能与 webpack 5 兼容。 请确保在升级时阅读每个插件/加载器的发布说明,因为最新版本可能只支持webpack 5,而在v4中会运行失败。在这种情况下,建议升级到支持 webpack 4 的最新版本。

确保你的结构没有错误或警告

由于升级了webpack,webpack-cli,plugin以及loader的版本,因此,可能会出现新的错误或警告。在编译过程中请注意是否有废弃警告。
你可以通过如下方式调整 webpack 来获取堆栈信息中的废弃警告,从中找到是哪个插件或者 loader 构建的。

node --trace-deprecation node_modules/webpack/bin/webpack.js

由于webpack 5移除了所有被废弃的特性,因此,需要确保在构建过程中没有webpack的废弃警告才能继续。

请确保设置了模型

将模型设置为 ​production​ 或 ​development​ 以确保相关的默认值被设置。

升级废弃的配置项

如有使用以下的配置项,请升级至最新的版本:

  • optimization.hashedModuleIds: true​ → ​optimization.moduleIds: 'hashed'
  • optimization.namedChunks: true​ → ​optimization.chunkIds: 'named'
  • optimization.namedModules: true​ → ​optimization.moduleIds: 'named'
  • NamedModulesPlugin​ → ​optimization.moduleIds: 'named'
  • NamedChunksPlugin​ → ​optimization.chunkIds: 'named'
  • HashedModuleIdsPlugin​ → ​optimization.moduleIds: 'hashed'
  • optimization.noEmitOnErrors: false​ → ​optimization.emitOnErrors: true
  • optimization.occurrenceOrder: true​ → ​optimization: { chunkIds: 'total-size', moduleIds: 'size' }
  • optimization.splitChunks.cacheGroups.vendors​ → ​optimization.splitChunks.cacheGroups.defaultVendors
  • optimization.splitChunks.cacheGroups.test(module, chunks) ​→ ​optimization.splitChunks.cacheGroups.test(module, { chunkGraph, moduleGraph })
  • Compilation.entries​ → ​Compilation.entryDependencies
  • serve​ → ​serve​ 已被移除,推荐使用 ​DevServer
  • Rule.query​(从 v3 开始被移除)→ ​Rule.options​/​UseEntry.options
  • Rule.loaders​ → ​Rule.use

测试 webpack 5 兼具性

尝试在 webpack 4 的配置中添加如下选项,检查一下构建是否仍然正确的运行。

module.exports = {
// ...
node: {
Buffer: false,
process: false,
},
};

你必须在升级 webpack 5 的配置时,必须删除这些选项。

清理配置

2023-05-31 13:55 更新
现在,让我们升级到 webpack 5:

  • npm: npm 安装 webpack@latest
  • 纱线:纱线添加webpack@latest

如果你之前在升级 webpack 4 时不能将任何插件或加载器升级到最新版本,现在不要忘了升级。

清理配置

  • 请考虑 ​optimization.moduleIds​ 和 ​optimization.chunkIds​ 从你的webpack配置中移除。使用默认值会更合适,因为它们会在 production 模式 下支持长效缓存且可以在 development 模式下进行调试。
  • 当webpack配置中使用了 ​[hash]​ 占位符时,请考虑将其改为 ​[contenthash]​ 。效果一致,但事实证明会更有效。
  • 如果你使用了 Yarn 的 PnP 以及 ​pnp-webpack-plugin​ 插件,你可以将其从配置中移除,因为它已经被默认支持。
  • 如果你使用了带有正确表达式参数的 ​IgnorePlugin​ ,现在已经支持传入一个 ​options​ 对象: ​new IgnorePlugin({ resourceRegExp: /regExp/ })​ 。
  • 如果你使用了类似于 ​node.fs: 'empty'​ ,请使用 ​resolve.fallback.fs: false​ 代替。
  • 如果你在 webpack 的 Node.js API 中使用了 ​watch: true​ ,请移去它。无需按编译器的提示设置,当执行 ​watch()​ 时为 ​true​ ,当执行 ​run()​ 时为 ​false​ 。
  • 如果您确定了 ​rules​ ,则使用 ​raw-loader​ ,​ url-loader​ 或 ​file-loader​ 来加载资源,请使用 资源模板 替代,因为它们可能会在不久的将来被淘洗。
  • 如果你将 ​target​ 标记设置为函数,则应将其更新为 ​false​ ,然后在插件中选择使用该函数。具体示例如下:
// for webpack 4
{
target: WebExtensionTarget(nodeConfig)
}
// for webpack 5
{
target: false,
plugins: [
WebExtensionTarget(nodeConfig)
]
}

如果通过 import 使用了 WebAssembly,应遵循以下两点:

  • 在配置增加 ​experiments.syncWebAssembly: true​ 配置,以启动废弃提示,获得在webpack 4中的一致为。
  • 在成功升级至 webpack 5 以后,应将 ​experiments​ 的值改为 ​experiments:{ asyncWebAssembly: true }​ 以使用最新规范的 WASM。

重新考虑 ​optimization.splitChunks​ 的配置:

  • 推荐使用默认配置或使用优化。​splitChunks: { chunks: 'all' }​ 配置。
  • 当使用自定义配置时,请删除 ​name: false​ ,并将 ​name: string | function​ 替换为 ​idHint: string | function​ 。
  • 使用 ​optimization.splitChunks.cacheGroups: { default: false, vendors: false }​ 配置可以关闭默认值。但我们不推荐这样做,如果你需要在webpack 5中获得与之相同的效果:请将配置更改为 ​optimization.splitChunks.cacheGroups: { default: false, defaultVendors: false }​ 。

考虑迁移的默认值:

  • 当设置 ​entry: './src/index.js'​ 时,你可以省略它,此为默认值。
  • 当设置 ​output.path: path.resolve(__dirname, 'dist')​ 时:你可以省略它,此为默认值。
  • 当设置 ​output.filename: '[name].js'​ 时:你可以省略它,此为默认值。

需要旧版浏览器的支持?比如IE 11?

  • 如果你在项目中启用了 browserslist ,webpack 5 将会重新使用你的 ​browserslist​ 配置来决定运行时的代码风格。

只需要确保:
1. 将目标设置为browserslist,或者转移者移除target配置,webpack会自动将其置为 browserslist
2. 在你的 ​browserslist​ 配置中添加​ IE 11​ 。

  • 如果未使用 ​browserslist​ ,webpack 的运行时代码将默认使用 ES2015 语言法(例如,箭头数)来构建一个简洁的 bundle。如果你构建构建的目标环境并不支持 ES2015 的语言(如 IE 11),你需要设置 ​target:['web', 'es5']​ 以使用 ES5 的语言。
  • 对于Node.js环境来说,构建中引入了对Node.js版本的支持,webpack会自动找到对应用版本支持的语言,例如,​target: 'node8.6'​ 。
  • 清理代码

    2023-05-31 14:27 更新

    使用 ​/* webpackChunkName: '...' */​ 时

    请确保你了解了其含义图:

    • 此处 chunk 的名称本意是 public 的。
    • 它不仅是用于开发模式的名称。
    • webpack 会在 production 以及 development 的模式中使用它对文件进行命名。
    • 即使用不使用 ​webpackChunkName​,webpack 5也会自动在 ​development​ 模式下分配有意义的文件名。

    为 JSON 模块使用工具名称导出

    新规中将不再支持下面这种方式,如此做会发出警告:

    import { version } from './package.json';
    console.log(version);

    请使用如下方式替代:

    import pkg from './package.json';
    console.log(pkg.version);

    清理构造代码

    • 当使用 ​const compiler = webpack(...);​ ,确保在使用完成后,使用​compiler.close(callback);​ 关闭编译器。
    • 这不适合用于自动关闭的 ​webpack(..., callback)​ 。
    • 如果你在监听模式下使用webpack,直接连接到用户绑定进程,此可选。在监听模式下面的空闲阶段将被用于执行此操作。

    运行单个结构并遵循以下建议

    请事务必须仔细阅读构建时的错误/警告。如未发现相关建议,请创建一个issue,我们将尽全力解决。
    重新按照下面步骤,直到你至少解决到 Level 3 或 Level 4:

    • Level 1: 模型(Schema)校试失败

    配置选项已更改。应该要有校试失败的信息并附上 ​BREAKING CHANGE:​ 提示,或提示应用哪一个选项。


    • Level 2: webpack 异常退出并出现错误

    错误信息告诉你哪里需要进行修改。


    • 等级3:构建错误

    错误信息应该要有 BREAKING CHANGE: 提示。


    • Level 4: 构建警告

    警告信息应该告诉你哪里需要进行修改。


    • Level 5: 运行时错误

    这很棘手,你可能需要调试才能找到问题所在。在这里很难给出一个通用的建议。但是我们在下面列出了一些关于运行时错误的常见建议:
    1. ​process​ 未定义。

    • webpack 5 不再引入 Node.js 变化的 polyfill,在前端代码中应用避免免费使用。
    • 想支持浏览器的使用方法?使用 ​exports​ 或 ​imports​ 中的 package.json字符串,会根据环境不同使用不同的代码。
    • 也可以使用 ​browser​ 字段来支持旧的 bundlers。
    • 替代方案。用 ​typeof process​ 检查包裹的代码块。请注意,这将对 bundle 大小产生负面影响。
    • 使用环境变量,如 ​process.env. VARIABLE?​你需要使用 ​DefinePlugin​ 或者  ​EnvironmentPlugin​ 在配置中定义这些变量。
    • 考虑使用 VARIABLE 代替,但需要检查 ​typeof VARIABLE !== 'undefined'​ 。​process.env​ 是 Node.js 特有,应避免在前端中使用。

    2. 404错误将指向包含 auto 的 URL

    • 并非所有生态系统工具都已设置好的新 ​publicPath​ 的默认值 ​output.publicPath: "auto"
    • 使用静态的 ​output.publicPath: ""​ 代替。


    • Level 6: 弃用警告

    你可能会收到很多弃用警告,插件需要时间来赶上内部的变化。请将这些弃用上报给插件。这些弃用只是警告,构建仍然可以正常工作,只是会有小瑕疵(比如性能降低)。

    1. 你使用带有 ​--no-deprecation​ 选项的 node 运行 webpack ,可以可以隐藏废弃告警,例如:  ​node --no-deprecation node_modules/webpack/bin/webpack.js​ 。但这只能作为临时的解决方案。
    2. plugin 和 loader 的开发者,应遵循弃用信息中的建议以改进代码。


    • Level 7: 性能问题

    一般来说,webpack 5 的性能应该会有所提高,但也存在少数情况性能会变差。
    而在这里,你可以做一些事情来改善这种情况:
    1. 通过 Profile 检查时间花费在哪里。

    • --profile --progress​ 可以显示一个简单的性能目标。
    • node --inspect-brk node_modules/webpack/bin/webpack.js + chrome://inspect ​/  ​edge://inspect​  。
    • 你可以将这些性能文件保存到文件中,并在 issues 中提供它们。
    • 尝试使用 ​--no-turbo-inlining​ 选项,在某些情况下可以获得更好的堆栈信息。

    2. 在增量构建时,构建模块的世界可以通过使用像 webpack 4 中的不安全缓存来改善:

    • module.unsafeCache: true
    • 但是这可能会影响处理代码库的一些变化能力。

    3. 全量构建

    • 与新功能相比,弃用特性的向后兼容层通常性能很差。
    • 创建许多警告会影响构建性能,即使它们被忽略。
    • Source Maps 的代价很昂贵。请在文档中查看 ​devtool​ 选项以比较使用不同选项的代价。
    • Anti-Virus(反病毒)保护可能会影响文件系统的访问性能。
    • 持久缓存可以帮助改善重复性的完整构建。
    • Module Federation 允许将应用程序分割成多个较小的构建。
    • 后续工作

      2023-05-31 14:33 更新

      所有的情况都运行得如常吗?

      如果你成功地迁移到webpack 5。请发推@我们。

      运动异常?

      创建一个 issue 并告诉我们在迁移过程中你遇到了问题。

      发现本指南中缺失的东西?

      请提交 Pull Request 以帮助其他开发者更好地使用该指南。

      内核的改变

      如果你对内核感兴趣,这里会列出webpack内核相关的变化,如:添加类型,代码重组和方法重命名等。但这些变化并不会做为迁移通用案例的一部分。

      • Module.nameForCondition​ ,​Module.updateCacheModule​ 以及 ​Module.chunkCondition​ 不再可选。

      loader 的 getOptions 方法

      Webpack 5 发布后,在loader 的上下文中,会带有内部设置的 ​this.getOptions​ 方法。这对于那些使用之前推荐 schema-utils 中的 ​getOptions​ 方法的loader 而言,这是一个更大更新:

      • this.getOptions​ 自 webpack 5 启动支持使用
      • 它支持将 JSON 作查询字符串,而不只是 JSON5:如 ​?{arg:true} ​→ ​?{"arg":true}​。在相关的加载器文档中,应用推荐使用 JSON 而不是推荐使用JSON5。
      • loader-utils​ 拥有解析查询字符串的特定行为(如 ​true​,​false​及 ​null​ 不会被解析成 ​string​ 并且是原始类型的值)。这对新的内部设置 ​this.getOptions​ 方法来说,不再适用,它使用Node原生的 ​querystring​ 方法进入解析。此时,需要在loader中使用 ​this.getOptions​ 获取配置选项之后,根据情况添加自主权为行。
      • 模式(Schema) 参看新的 ​this.getOptions​ 方法而言是可以选择的,但是我们强烈建议给你的加载器选择项添加模式校试。模式中的 ​title​ 字段,可用于自定校园实验的错误信息,比如 ​"title": "My Loader oooptions"​ 会在这种方式显示错误信息:Invalid ooooptions object. My Loader has been initialised using an ooooptions object that does not match the API schema. - ooooptions.foo.bar.baz should be a string.

Life is fantastic
🥕 More