这个对 Web 的看法比较狭隘,一方面,没有认识到 Web 是目前最流行的开放平台:
另一方面,夸大了大数据、AI 的应用场景,AI 跟 Web 不是竞争关系。只有少数应用像 Google 搜索那样以大数据和 AI 为核心竞争力,大部分场景只能用数据和 AI 改良部分的功能——例如让广告投放更准确、风控更可靠。下一个以 AI 为核心竞争力的领域可能是自动驾驶,但是所有人都要去做自动驾驶吗?一窝蜂的投入大数据和 AI 领域,小心以后工作就是当个财务报表分析员。
我维护过一个面向全球客户的关键后台服务,以 Mobile App 为终端,不提供 Web 界面,一样基于 Rails 开发。一个后台服务需要数据库、鉴权、序列化、后台任务等等功能,用 Ruby 还是用 Go 没有什么不同,难点不在语言而在业务。
马云是个商人、老板,听老板讲话不要说什么就是什么,而是分析背后的目的,难道有了 AI 就要基于 AI 重写淘宝?大家都搞 AI 去就不用跟淘宝在电商领域竞争了。
最后推荐一篇文章:Betting on the Web https://joreteg.com/blog/betting-on-the-web
Docker
要额外处理 PATH 问题。
估计是那几个 gem 导致错误捕捉除了问题:
group :test do
gem 'rails-controller-testing', '1.0.2'
gem 'minitest-reporters', '1.1.14'
gem 'guard', '2.13.0'
gem 'guard-minitest', '2.4.4'
end
我看了下这本书只说了替换 Gemfile 没说更换这些 gem 的理由是什么,对初学者很不利。
我推荐用《Rails 5 敏捷开发》这本书 https://book.douban.com/subject/27615703/
贴代码和运行结果。
一个 test case 是指一个 test "..." do end
块,可以随意自己添加。
错误打出异常栈是正常的,不然不好 debug,应该是书缺少提示了。
在同一个 test case 里面,遇到 error 或者 fail 之后,后面的代码就不执行了,因为很可能也会失败,没必要执行下去。test case 之间是独立的。
路由未定义。
errors 打出错误栈是正确的,书里有没有写“以下省略部分输出”?
说 docker 的说说用什么编排工具啊,总不能一台台上去操作吧。
创建目录、链接文件、运行迁移、重启进程,这些工作总要有工具做啊。
cap 就是个远程执行工具。
我用 cap 部署,systemd 监视进程。
写第三次再提炼。
云端的部署架构是什么?
如果是典型的 nginx 跟 app 在同一服务器,要把 nginx 的 root 指向 public 目录;如果是 heroku,则需要设置 RAILS_SERVE_STATIC_FILES 环境变量让 rails 进程处理静态文件,但这通常会在前面加一层 CDN。
…你先买本入门书把例子做完。
API 控制器收到的数据存到模型层,后台控制器再从模型层查数据。
用 validates 比较容易收集错误信息,而且不用访问数据库。
数据库异常不好处理错误信息,而且耗费了网络请求。
不承认对象客观存在,又变着法子模仿对象的功能,只能说开心就好。
你满嘴跑火车偶尔说对一两句话不出奇,我点赞对事不对人。看我博客发布日期,你确定不是看过我博客后记了下来?
Ops, 我忘了 elixir 相对 erlang 的改变之一就是增加了可重复绑定的变量,这在 erlang 认为是不纯。
直接修改实例变量,不比新增一个 struct 然后覆盖它更直观吗?
order.status = 'paid'
不懂你的观点。
Struct 用来约束数据结构,数据一旦生成就不可变,要改变的时候只能新增一个数据。例如:
defmodule Order do
defstruct number: '', status: 'open'
end
iex> order = %Order{ number: '20180101' }
%Order{number: '20180101', status: 'open'}
iex> order_paid = %{order | status: 'paid'}
%Order{number: '20180101', status: 'paid'}
iex> order
%Order{number: '20180101', status: 'open'} # 旧的数据依然存在
此时内存中存在两份 number 为 20180101 的订单,就因为 elixir 的变量不可变,新变量只是在假装不知道旧数据存在罢了。我认为这不是很好的对现实关系的映射。
用脚可以拿筷子吗?练练也是可以用的,但是有手的情况下何须用脚拿筷子。
无状态在处理数据流的时候也许有不少优点,但在跟 DB 交互这里,DB 作为数据流终点就是有状态的,强用无状态实现不是一个优势。
看了下文档,ChangeSet 不就是 Elixir 这样的函数式语言没有实例变量而不得不把所有变量通过参数传递么。
而且用起来跟 ActiveRecord 没啥区别:
changeset = User.changeset(%User{}, %{age: 0, email: "[email protected]"})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [age: {"is invalid", []}, name: {"can't be blank", []}]
user = User.new(age: 0, email: "[email protected]")
user.save
user.errors #= > <#ActiveModel::Errors ... @message={:age=>["is invalid"], :name=>["cant be blank"]}
看 Ruby 还短一点。
ChangeSet 用作 validation 一个坏处是很容易被跳过,不能当作数据入库前的守门人。用作 filter 还比较合理。Ruby 2.5 的 yield_self
就可以方便写出链式过滤器调用了。 https://blog.bigbinary.com/2017/12/12/ruby-2-5-added-yield_self.html 下一版也许会把 yield_self
增加一个 alias then
。
学习新语言看到新特性,就感觉无所不能,忘了以前是怎么做的,这是一种通病。函数式语言推广者把“无状态”奉为金科玉律,无视现实世界还有很多有状态的事物,成也于此败也于此。
不改。对接过 Web、Android、iOS、C++,设请求头都没有问题。说困难是他们忽悠你。
十楼方法是增加一个过滤器判断额外的 token,不改变 devise 内部,原理一样但更安全。
这个问题是个好例子说明为什么 devise 很难用。
如果是自己实现的用户登录机制,那么很容易会知道应该改哪里。通常来说,user 应该带有一个 token 字段存在 session 中,访问的时候通过 token 查用户。如果要注销旧的登录态,只要把 token 重置就行了。
那么 devise 怎么做呢,应该 reset 哪个 token?我也不知道,因为内部实现太复杂了,查看数据模式根本没有这个字段,我尝试读一下源码。
首先根据我以前读源码的经验,我先找 devise 的 session controller:
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
关键在于 sign_in(resource_name, resource)
这一行,继续找 sign_in 如何实现:
def sign_in(resource_or_scope, *args)
options = args.extract_options!
scope = Devise::Mapping.find_scope!(resource_or_scope)
resource = args.last || resource_or_scope
expire_data_after_sign_in!
if options[:bypass]
ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
[Devise] bypass option is deprecated and it will be removed in future version of Devise.
Please use bypass_sign_in method instead.
Example:
bypass_sign_in(user)
DEPRECATION
warden.session_serializer.store(resource, scope)
elsif warden.user(scope) == resource && !options.delete(:force)
# Do nothing. User already signed in and we are not forcing it.
true
else
warden.set_user(resource, options.merge!(scope: scope))
end
end
关键在最后一行 warden.set_user(resource, options.merge!(scope: scope))
,warden 是另一个 gem,在到另一个库里搜 set_user
做了什么:
def set_user(user, opts = {})
scope = (opts[:scope] ||= @config.default_scope)
# Get the default options from the master configuration for the given scope
opts = (@config[:scope_defaults][scope] || {}).merge(opts)
opts[:event] ||= :set_user
@users[scope] = user
if opts[:store] != false && opts[:event] != :fetch
options = env[ENV_SESSION_OPTIONS]
if options
if options.frozen?
env[ENV_SESSION_OPTIONS] = options.merge(:renew => true).freeze
else
options[:renew] = true
end
end
session_serializer.store(user, scope)
end
run_callbacks = opts.fetch(:run_callbacks, true)
manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks
@users[scope]
end
关键是这个 session_serializer.store(user, scope)
,在找找 session_serializer 是什么鬼,然后找到这里:
def store(user, scope)
return unless user
method_name = "#{scope}_serialize"
specialized = respond_to?(method_name)
session[key_for(scope)] = specialized ? send(method_name, user) : serialize(user)
end
我这里跳跃一下,直觉告诉我 warden 里面都是些 proxy 方法,也许 devise 用到的 serialize 是在 deivse 里面定义的,于是我回去搜 devise 里面有没有 serialize 相关的方法,于是搜到这个:
def serialize_into_session(record)
[record.to_key, record.authenticatable_salt]
end
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
我也不知道是不是,看起来 devise 是通过 to_key 和 authenticatable_salt 定位用户的。
在 ActiveReocrd 里 to_key 约等于 id,不可变,那么我们最好从 authenticatable_salt 入手,搜一下 authenticatable_salt
:
def authenticatable_salt
end
# A reliable way to expose the salt regardless of the implementation.
def authenticatable_salt
encrypted_password[0,29] if encrypted_password
end
根据加载的模块不同,authenticatable_salt 的实现不一样。留意 authenticatable 里面的实现,authenticatable_salt 跟 encrypted_password 绑定,这样可以实现修改密码之后 authenticatable_salt 一起变更,但这怎么解决顶楼的问题呢?
我建议是新增一个字段,然后把 authenticatable_salt alias 过去。
class User < ActiveRecord::Base
has_secure_token :auth_token
def authenticatable_salt
auth_token
end
end
然后要改写 session_controller,在每次登录之后 reset remember_token:
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
resource.regenerate_auth_token # <- 重置 auth_token
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
以上方法我没有实践过,不保证能用。
读源码是最费时的一种方法,有的人可能会提醒我,用 Google 搜一下,也许会有现成答案。没错,我搜了一下发现这个特性有人写成了 Gem,https://github.com/phatworx/devise_security_extension (session_limitable)。大多数的 devise 问题都能搜一搜,粘贴段代码,或者装个 gem 解决,但是用户登录是系统的核心模块,这样做真的对吗,如果用户登录的原理都搞不懂,怎么能确保系统是安全的呢?
所以我的建议是别用 devise,读 devise 源码的耗时远远大于自己写,当然楼主维护现有系统是没得选了。这回复得比较啰嗦,我留作以后讨论 devise 的例子。
接入 https://newrelic.com/ 再跑压测,看 NewRelic 分析。