如何利用 Rails 在 21 天单枪匹马上线一个产品

李亚飞

For RubyConf China 2016

2016.09.24

李亚飞

深圳 Ruby 活动组织者

问题解决者, 和一个真诚的人

 

做过 cywin, jiaoluo, 51goda等, 目前持续创业在 80%

 

github:  @windy

ruby-china: @lyfi2003

做一个产品所需的能力

  • 产品思维
  • 技术能力
  • 设计能力
  • 运营能力

每一个 Rails 工程师都应该有一个自己长期维护的项目

内容

第零步

产品讨论

扫码体验

作者端

  • 一个 PC 端应用
  • 写付费文章并定价
  • 手机扫一扫分享
  • 微信服务号查询

读者端

  • 微信应用
  • 微信内支付并阅读
  • 评价, 及PC端同步阅读
  • 微信服务号查询

平台端

  • PC 端
  • 数据统计
  • 系统管理

第一步

  • 公司注册

  • 域名备案
  • 微信公众号

统筹方法学

先做最可能阻塞但可以并行的事情

先开始?

当然是 "微信认证" ...

  • 微信服务号
  • 微信订阅号
  • 微信开放平台
  • 微信商户号
  • 微信企业号
  • 微信小程序号

找不同

最终, 我们交了 1200 人民币!!!

  • 程序员
  • 小程序员

最终, 这个世界将分为

公司注册

  • 选个好名字
  • 比如 80percent vs 37signals( 先定个小目标 😊 )
  • 注册地很重要( 比如深圳前海 )
  • 推荐找个靠谱的代办公司

域名备案

  • 尽量用 .com 而不是 .cn
  • 准备好 20 天的备案时间
  • 电话及时接

第二步

技术选型

Rails5 全家桶

  • Ruby on Rails 5

  • turbolinks5

  • actioncable

  • bootstrap 3

  • font-awesome

  • figaro

  • postgres

  • slim

  • high_voltage

  • carriewave & upyun

  • sidekiq

  • kaminari

  • mina

VS Vue

  • 开发速度快( 轻前端应用 )
  • 用户体验好( turbolinks5 支援 )
  • 不会带来莫名的问题( SEO, 首页加载速度等 )
  • 后端生态无解肥

VS React

  • React 全家桶学习成本高
  • 速度快, 体验好( 轻前端应用, turbolinks5支援 )
  • 后端生态优秀且成熟

轻前端应用 turbolinks + Rails5 远优于前端框架

第三步( 技术篇 )

产品迭代开发

3.1 设计数据结构

  • 清晰的命名是关键
  • 关联关系的命名也很重要
  • 信息不要冗余
class User
  # other codes

  has_many :reader_orders, class_name: 'Order', foreign_key: 'reader_id'
  has_many :writer_posts, class_name: 'Post', foreign_key: 'writer_id'
end

3.2 Turbolinks5

  • 用户体验好( 可比 SPA 应用 )
  • 开发效率高
  • 学习成本低

优点

注意点

  • 操作不幂等时关闭 cache
  • 在微信6.1以下版本关闭调用 jssdk 的页面
// page1.html

- content_for :head do
  meta name="turbolinks-cache-control" content="no-cache"


// layouts/application.html.slim
= csrf_meta_tags
= content_for?(:head) ? yield(:head) : ''



// -----------------
// app/views/wechat/writer/readers/index.html.slim
= link_to order.post.short_title, reader_post_path(order.post), data: { turbolinks: false }

更多 turbolinks5 的问题? 请查阅:

3.3 wxpay 与 ActionCable

零延迟的支付体验

wxpay gem & sjr

function jsApiCall(){
  showSpin(<%= @order.id %>);
  WeixinJSBridge.invoke(
    'getBrandWCPayRequest',
    <%= raw @js_pay_hash.to_json %>,
    function(res){
      if(res.err_msg == "get_brand_wcpay_request:ok") {
      }else if( res.err_msg == "get_brand_wcpay_request:cancel" ){
        hideSpin();
        $.post("<%= cancel_reader_orders_path(order_id: @order.id) %>");
      }
    }
  );
}

function callpay(){
  if (typeof WeixinJSBridge == "undefined"){
      if( document.addEventListener ){
          document.addEventListener('WeixinJSBridgeReady', jsApiCall, false);
      }else if (document.attachEvent){
          document.attachEvent('WeixinJSBridgeReady', jsApiCall);
          document.attachEvent('onWeixinJSBridgeReady', jsApiCall);
      }
  }else{
      jsApiCall();
  }
}

callpay();

wxpay & actioncable

# order action cable( order_channel.rb )
class OrderChannel < ApplicationCable::Channel
  def subscribed
    stream_from "order_for_#{params[:order_id]}"
  end
end

# order.rb
class Order
  def send_cable_notify
    ActionCable.server.broadcast "order_for_#{self.id}", {}
    ActionCable.server.broadcast "user_post_for_user_#{self.reader_id}_and_post_#{self.post_id}", {}
  end
end
// javascripts/pay_helper.js
function showSpin(order_id){
  window.App.order_channel = window.App.cable.subscriptions.
    create( {channel: 'OrderChannel', order_id: order_id}, {
      received: function(){
        $('#spin').spin(false);
        Turbolinks.visit();
      }
  });
  $('#spin').css('z-index', 1).spin().css('background-color', 'rgba(0,0,0,0.2)');
};

function hideSpin(){
  $('#spin').css('z-index', -1).spin(false).css('background-color', 'transparent');
}
  • 零延迟体验支付通知
  • 逻辑直观
  • SJR 与前端JS良好配合

3.4 JS 和 CSS 拆分

Layout 拆分

  • wechat layouts
  • writer layouts
  • admin layouts

JS 拆分

  • application.js
  • wechat_base.js
  • writer_base.js
  • admin_base.js

CSS 拆分

  • application.css
  • wechat_base.css
  • writer_base.css

核心步骤

  • 移除 require_tree .
  • 为每一种角色创建一个 layout
  • 分别准备它们的 js 与 css, 并共用结构
  • 在 initializers/assets.rb 添加对应的 js 与 css

结果

300KB -> 80KB, 加载速度提升 300%

配合 cdn 用户移动端在 700ms 左右打开应用

3.5 ActionCable 更多应用

用户在电脑端打开文章, 扫码购买, 电脑端即时同步阅读

 

考虑登录与未登录情况

思考一个需求

解决方案

  • Token for user&post
  • Redis
  • Channel Name: user_post_for_user_id_post_id

Channel

# user_post_channel.rb
class UserPostChannel < ApplicationCable::Channel
  def subscribed
    stream_from "user_post_for_user_#{params[:user_id]}_and_post_#{params[:post_id]}"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

Notification


# order.rb
def send_cable_notify
  ActionCable.server.broadcast "order_for_#{self.id}", {}
  ActionCable.server.broadcast "user_post_for_user_#{self.reader_id}_and_post_#{self.post_id}"
    , {}
end
# post_token.rb
class PostToken
  redis = Redis.new
  @hash = Redis::Namespace.new(ENV['TOKEN_NAMESPACE'], :redis => redis)
  @order_token_map = Redis::Namespace.new(ENV['TOKEN_NAMESPACE'] + '_order_token_map', :redis => redis)
  @expire_time = 60 * 60 * 24 * 7 * 2
  class <<self

    def generate
      SecureRandom.uuid.tr('-', '')
    end

    def bind_order(order_id, token)
      @order_token_map.set(order_id, token)
      @order_token_map.expire(order_id, @expire_time)
    end

    def is_bind_order?(order_id)
      @order_token_map.get(order_id).present?
    end

    def order(order_id)
      token = @order_token_map.get(order_id)
      if token
        @hash.set(token, true)
        @hash.expire(token, @expire_time)
        # notify
        ActionCable.server.broadcast "order_token_for_#{token}", {}
      end
    end
  end
end

Post Token

第四步

测试与发布

测试快起来

  • Test Mode
  • wxpay
  • sidekiq: async -> sync

Test Mode


# routes.rb

Rails.application.routes.draw do
  if ENV['USER_TEST_DEBUG'].present?
    get '/test' => 'visitors#test'
    get '/test_for_new_user' => 'visitors#test_for_new_user'
    get '/test_for/:id' => 'visitors#test_for'
  end
end
# OrdersController#pay

  if ENV['USER_TEST_DEBUG'].present?
    if @order.may_pay?
      @order.pay!
      order_token_for_order_id
      @order.send_cable_notify
    end
    render js: 'alert("测试模式支付成功");Turbolinks.visit();'
    return
  end

Test Mode( 2 )


# sidekiq worker

if ENV['USER_TEST_DEBUG'].present?
  ServerNotificationWorker.new.perform(title, text)
else
  ServerNotificationWorker.perform_async(title, text)
end
  • 自动创建与登录用户
  • 尽可能同步操作测试
  • 极大节省测试成本

发布云端化

  • ASSETS CDN
  • Image CDN
  • Virtual Host

CDN

# config/environments/production.rb

config.action_controller.asset_host = 'https://p8020cdn.b0.xxyun.com'
# app/uploaders/image_uploader.rb

class ImageUploader < CarrierWave::Uploader::Base
  storage :xxyun
end

维护自己的镜像或模板

某云模板

发布一键化


$ mina deploy

数据可视化

  • Charts
  • Data Collection
  • Server Notification

Charts

# chart
gem "chartkick"
gem 'groupdate'

gem 'linux_fortune'
  • 新增用户数
  • 新增文章数
  • 浏览量 / 订单 / 取消订单 / 偷偷看
  • 每周 / 每天

运营数据观测

最后一步: 运营

  • V2EX
  • RubyChina
  • 36kr
  • 爱范
  • .... ....

外围修炼

  • UI 设计与审美能力
  • 产品设计能力
  • 运营能力
  • 公司运营
  • ... ...

开发至上线运营

仅 21 天

05.16 ~ 06.06

当前数据

2800+ 用户

150+ 文章

3000+ 销售额

在 Rails5 轨道上运营你的产品

生产力总结

  • 最佳实践套装
  • 简洁高效的开发理念
  • 生态强大

产出的几个轮子

80 学院

  • 一对一导师制
  • 线上教学
  • 3 个月

AD

百分之八十

深圳市

网络技术有限公司

招人中

同时提供: 创业产品 MVP 开发

RubyConf 赞助商, 资料袋有详细介绍

QA & Thx

rails-21-days-release-production-app

By Li Yafei

rails-21-days-release-production-app

  • 3,131