读《浪潮之巅》

资本主义

《浪潮之巅》的开篇讲的是 AT&T 的衰落,一家相当于华为+中国移动联合体的公司,这家公司旗下的贝尔实验室是现代计算机科技和通信技术的发源地,诞生了晶体管、Unix操作系统、C语言。

AT&T 拥有:

AT&T 也并非没有面对过挑战者们,不过所有的挑战者都会被 AT&T 先打价格战消耗继而收购,反垄断法一直想拆分这家公司。

AT&T 是一家先进的股份制企业,我们国家当初很多国企的股份制改革,都是跟人家学的。没有大公司病,管理健康,现金流充沛,很难想象这么牛的公司还会有走下神坛的那天。

AT&T 最大的问题就是它太大了,股东多,最大的股东是:华尔街。每个股东都有各自的想法,最直接的想法就是挣钱。 其实一个企业是没有思想的,有思想的是人,往往最有思想的是创始人。100多年过去了,AT&T 的创始人早已离世,现在是拿着高薪的总裁们在管理这家公司。 就失去了主见,不受某个总裁的意志控制了。这个时候,一些自然规律便开始主导。

在这样的背景下,资本们想了个能让业绩增长的好主意,于是 AT&T 做了一个决策,这个决策是 AT&T 衰落的关键点:

当时 AT&T 手里有两笔业务,一笔是设备制造,一笔是长话业务,通信设备几乎处于垄断地位,通话业务由于 1984年的反垄断拆分,大概占有 3 分之 1 的份额。

另外两个 MCI 和 Sprint 由于跟AT&T 的竞争关系,就不敢买 AT&T 的设备。于是 AT&T 为了给竞争对手卖设备,把设备和通信分别拆分为两个公司,从事设备制造的朗讯(Lucent)。

和预期一样,业绩得到了大幅增长,资本家和高管手里的股票翻了番,他们也没想到原来拆分这么赚钱的一个骚操作。

于是 AT&T 又拆了一次,这次一分为四,分为长途电话、移动电话、企业服务、宽带。

彻底动摇了根基,后面的结果大家也都知道了。

资本过度干预企业的正常管理,如撤换雅虎董事会。

风口

当站在风口,猪都能上天。

有些时代的机遇来临时,抓住风口的企业。

这是属于时代的红利,每个领域必然都会出一个霸主;

为什么是他们?

  1. 不受华尔街控制。
    • Google 尽可能晚的上市;
    • 尽可能把股票卖给散户;
    • 分为多次融资,确保股权尽可少的被稀释,同样比例的股份出让,融资金额却高得多;
    • A/B 股权架构,同股不同权;如刘强东依然对京东有 80% 的投票权。

同样是搜索,为什么百度就要迎合资本市场。

  1. 人才理念
    • 创业期:以一敌百,尽可能用优秀的人才,注意:人才在这样的时期下的公司里也是会快速成长的。所以伴随着创业阶段成长起来的人才的价值会超过外聘高管;
    • 成长期:工程化,让初级工程师或者普通员工也能很好的产出;
    • 对项目负责人充分授权,这样可以加速决策,提升效率;臭鼬公司(几乎引领了现代飞行器的研发)14条原则里的第一条。
  2. 创始人的理念能得到贯彻,如Google的三个创始人理念很一致。

4. 遵循商业本质:

库克为什么供应链管理出身,能够掌舵苹果。因为供应链管理的价值就是:

微软就是这个生态市场中的主导者,微软做了这么几件事情:

  1. 放任盗版,扩大市场占有率;
  2. 为应用开发者尽可能提供便利;
  3. 兼容,为了兼容微软都造了子系统,现在 windows10 上依然可以运行XP的应用;

一波操作下来,windows操作系统上的游戏,office软件已经非常成熟了。后来的 Android 系统几乎照搬了微软的策略。只是把放任盗版变成了开源。

生产关系

上层建筑决定下层建筑,生产关系决定了生产力。

Google 与华尔街的博弈,则是尽可能让公司远离短视资本的控制。

少年时期,学社会主义,马克思思想,总觉得这是国家洗脑的玩意。但是现在重新审视这些经典理论,才渐渐领略其奥义:

生产关系的目标是提升生产力,反推就是说,如何将生产力最大化,然后制定相应管理政策。

如 Google 提出的“三七理论”,让员工有30%的时间可以干自己喜欢的事情,以此保障企业的创新能力,这也是 Google 多元化扩张遍地开花的基础,如前端框架 Angular 则是在这样的背景下孵化出来的。

曾经有人质疑,咱们国家是披着社会主义皮的资本主义国家,其实在这个原则下,是姓资还是姓社,并不重要。主要的是管理体制,要能够持续推动社会生产处于高效率状态。

北欧国家被认为是高福利国家,最接近社会主义。年轻人不必为买房买车发愁,在这样的条件下,催生了大量富有创新性的事情。

如 Rails(Web 开发框架的鼻祖,几乎所有的语言都有开发类 Rails 的框架) 框架的作者 DHH 在瑞典,谈及开发 Rails 框架的时候,就感谢了国家的高福利让其能够有精力做这些短期不会产生效益的事情。

急功近利,这个也是我们国家目前的社会问题。

资本主义社会是为有产阶级服务(有钱)的,所以在资本主义社会,钱生钱。但是处于自然规则下的钱是短视的。

试想,如果中国的华为是上市企业的话,还可能成长到现在的体量,拥有如此顽强的生命力么,美国举国之力都没干掉华为。

贯彻生产关系,知行合一

也就是我们前面提的,就算创始人构建了好的框架,但是创始人的理念如果得不到贯彻执行,也是枉然。

企业创始人需要因为志同道合走到一起。因为理念不合,利益分配,最后失败的创业企业太多了。

为什么有个阶段,我们会觉得社会主义是国家给我们洗脑的玩意呢。就是因为在那个阶段,我们看到的和书本上学到的不一样。

但是现在国家在治理能力上的提升,也是不断践行党的纲领,践行社会主义理论的情况下,我们看到了生产关系的魔力;

信息的流动和物品的流动效率是整个社会发展的本质,如果没有集体所有制。这些基础是不可能有的。美国这么发达的国家,很多山区都是没有手机信号的。

马克思对社会发展阶段的论述,从封建社会到资本主义,从资本主义再到社会主义,是有其自然的发展规律的。

这些发展规律的底层是社会的总生产力。

当老百姓饭都吃不饱的时候,他们想的是怎么能够让明天有顿吃的。当青年人买不起房的时候,他们想的是怎么今年赚十几二十万。

当不愁吃穿住的时候,当手里的钱这辈子都花不完的时候,才会开始想 祖国和社会的明天。

社会财富会影响整个社会的价值观。我们现在的价值观可能还有点跟不上社会财富,我们现在应该考虑的是更多的未来。国家给了我们这么好的4g网络,我们不应该刷抖音和打游戏。

当然如果有年轻人在房子还买不起的时候,依然在想祖国的明天,这样的年轻人是值得敬佩的。

比如我们,笑~

工程师的产品观

我常常对我曾供职的两个公司感到遗憾,因为我觉得这两个公司发给我的工资完全就没有赚回来嘛。A公司,参与开发的一个项目不了了之;B公司,负责开发的项目上线三个月之后公司便停止维护。

如果产品决策失误,作为程序员,是不是就写了一套没有多大价值的代码。谁都不希望自己参与的项目夭折,虽然多少有些身不由己,但我还是希望自己仍然能够生产出更多的价值。

也许有人认为“拿钱干活”就好了,何必要关心产品层面的事物。从某种意义上来说,我认为程序员只是一个生产力工具,生产工具的价值在于其所生产的产品,好的生产工具也会为了产品而优化自身,所以我们程序员应该具有带着对产品的思考去思考代码,让自己成为更优秀的生产力工具。

提炼(Design)

一个软件应用替代的是现实中的服务的一个或多个环节,我们所参与构建的大多数应用是将现实中的服务转变为机器(屏幕)提供给人的服务。无论现实还是应用软件,都需要满足人们的具体需求。产品经理需要提炼现实中的需求,设计产品的功能和交互,从而能够满足相应的需求。

对于程序员来说,有两件事最头疼,一件事是起名字,另一件事则是设计模型。模型设计是程序的基础,每个模型应该包含哪些属性,各个模型之间有什么样的关联或关系。如果能设计合理,符合事物的本质,就能够具备良好的扩展性。相信不少程序员在开发的过程中都或多或少有过一些纠结:这个模型当初为什么要这么设计。

之前我参与过一个项目,需要配合食物库开发一套电商系统。与传统的电商系统有区别的是,食物库中只有一部分是会销售的。如果这个食物没有销售,那么它就仅仅只是一个食物,只有当它可以被销售的时候,这个食物才是商品。也就是说商品所具有的属性是由销售所产生的,比如价格、库存等。于是我们重新设计了一个商品的模型,这个模型不包含任何食物本身的信息,而是仅包含作为交易物品所具有的属性,然后围绕商品为中心,设计了供应商,交易等一系列模型。 当这个开发工作结束的时候,我惊奇的发现,这套电商系统很容易通过改造让现存的系统具备电商功能。这就是由良好模型设计让代码所具备的灵活扩展的能力。

通用(Common)

在应用程序的人机界面中,良好的用户交互需要具备一个重要原则:一致性。UI的一致性可以降低用户的学习成本,让用户觉得系统更容易上手和使用。比如统一的菜单样式,统一的视觉,统一的输入处理。这些需要保持一致的模块,体现在我们程序员的工作中,是一定可以将其作为一个可以通用的模块进行coding的。

大家都知道我们写代码有一个重要原则:DRY(Don’t Repeat Yourself),为了达到这个目标,我们需要知道那些功能是可以通用的,该通用性功能剥离出来的代码,就可以形成通用的模块。

我曾经供职的一个公司,产品线颇多,每个小项目由不同的产品经理负责,没有统一的UI,每个新项目都是独立的设计师完成设计,我们开发的时候就只能按着设计师提供的设计稿还原界面。

如果可以从品牌层面统一UI风格,统一交互形式,我们程序员也就可以更容易提炼出一套通用的代码。这些通用的设计和代码其实都是一个企业的积累,如果没有这些积累,不论这个企业做了多久,它做的每一个产品都仍然像是一个创业企业从零开始做的产品。最终我们为了能够写出更通用的代码,我们也促成了公司在产品层面的一致性设计,这也算是程序员推动企业产品进步的一个案例。

取舍(Better Choice)

取舍的问题,是成本与收益的问题。优秀的产品往往会聚焦到核心功能上,然后围绕核心功能展开设计,我们见过太多想做成大杂烩最终做成了四不像的失败产品,这些产品失败就败在取舍。将有限的资源应用到最值得投入的地方,才能产生最大的收益。

我们程序员的时间就是宝贵资源,对于我们来说,产品在功能上的侧重和取舍,也是我们在代码层面的取舍。有些临时的功能,非核心的功能就可以不必过分追求代码质量。同样的写测试,并没有必要100%测试覆盖,而是覆盖核心功能,经常被调用的方法,经常被使用的模块就需要最佳实践良好设计。

我们做项目,偶尔会遇到时间紧任务重的时候,客户对某个功能实现的期望比较急切。我现在所参与的项目,需要实现一个报表功能,与我们一起合作的有一个来自歌舞之乡印度的程序员,这位同仁为这个报表系统的前端开发引入了最新的JS框架React,因此这个报表做了大概一个月左右。当我接手的时候,还需要实现剩余6个报表功能的UI,我并没有贪恋新技术,而是采用成熟的方案去实现,仅用了不到2天时间去实现了所有剩下的UI功能,效果也一样。我觉得这种取舍是有必要的(当然不排除印度哥们是为了练技术)。

作为一名工程师,我们是帮客户实现一个应用产品,而不是去完成客户的指令。所以从产品层面去思考我们的工作,我觉得还蛮有意义的。

结语

从业几年来,经手的完整项目有了一些了,电商、社交、学习系统、医疗系统等都有所涉及。工作越久,就发现所参与开发的各套系统之间通用的部分越来越多。我最大的收获就是通过深入分析每一个功能,将其分离组合成一些通用的模块。模块化及复用代码是快速制造,保证质量,降低成本最有效的策略。我甚至有一个大胆的设想,在自己数年的工作之后,我会积累出足够数量的“积木项目”,当有新项目的时候,就从自己的“积木库”里挑出来组合一下。然后就去喝喝茶,坐等客户结账。

前后端分离

什么是前后端分离

要谈“前后端分离”这个话题,首先聊聊什么是“前后端分离”?常规意义上的“前后端分离”更多是一种软件工程上的概念,主要具备以下特点:

“前后端分离”的意义主要有:

在 C/S(Client/Server) 架构下,Client 端属于“前端”,Server 端属于“后端”,本身就是前后端分离了。所以“前后端分离”主要是针对 B/S(Brower/Server) 架构的软件工程,即运行在浏览器里的 Web 应用。在没有“前后端分离”之前,传统的 web 应用往往是由 Server 端生成能够在浏览器里运行的 HTML 代码,HTML 代码里亦包含了数据。

前后端分离的缺点

“前后端”分离固然带来了软件工程上的便利,同时也带来了新的成本和新的问题。

在传统的“前后端”一体化应用中,由 Server 端应用直接生产 HTML,采用“前后端分离”的方案后,后端输出 JSON(目前较普遍使用的方案) 格式的数据,然后前端对 JSON 数据进行解析,然后再渲染出 HTML 页面。在这个过程中,有些在后端已经做过的事情,前端需要再做一遍,包括:

毋容置疑,前后端分离带来的是大量新增的工作量。虽然有一些优秀的前端框架的出现,如 Vue 等极大的方便了前端应用的开发。其实也正是这些前端框架的出现,才出现了“前后端分离”的流行。至于是先有蛋还是先有鸡,已经不重要了。

除了开发成本的增加。前后端分离还有一个巨大的痛点,即协作成本。

Rails下的下一代前后端分离

Webpacker 最佳实践

自从 Rails 6 开始,官方默认采用 webpacker 来处理 Javascript,保留 Assets Pipeline(Sprockets) 作为 CSS、图片等静态资源处理方案。

webpack 的引入显然解决了一些 Assets Pipiline 很难解决的痛点,比如对编写下一代 JS 的支持。

虽然 webpacker 的生态也还在不断完善之中,但是从 assets pipeline 切换到 webpacker 也并非无痛的,最典型的场景就是对 Rails engine 中 assets 自动加载变得很难。

不过我们可以自己动手,丰衣足食,尽可能减少迁移过程的痛苦,在此分享下我的实践。

配置 JS 处理方式

移除 assets pipeline 对 js 处理的配置

app/assets/config/manifest.js 文件默认配置了 assets pipiline 需要处理的静态资源文件,把 js 相关的内容移除;

如果 app/config/application.rb 中定义了 assets.js_compressor,也一并移除;

曾经的 uglifier, coffee-rails 等 gem 也可以永远拜拜了,再也不会为配置 execjs 的 runtime 而烦恼。

修改 webpacker 配置

webpacker 默认配置中,js的路径是 app/javascript/packs,为了兼容老项目,我们将其路径更改为 app/assets/javascripts, 修改 app/config/webpacker.yml 中的如下配置:

source_path: app/assets
source_entry_path: javascripts

兼容 Rails engine

由于我们的Rails项目采用了组件化开发,引入了多个 engine, 大量的 js 代码散布在 engine 的 app/assets/javascripts 目录下。

如何才能让 webpack 在编译的时候能够加载 engine 下的 js 代码呢,webpack 的工作目录是 Rails主项目,关键点就是如何让 webpack 知道各个 engine 在文件系统中的具体位置,也就是 ruby 和 js 之间分享数据。

我采用了一个比较粗暴的办法,在 rails 项目启动完成的时候,将 engine 的位置信息更新到 config/webpacker.yml 文件当中,然后在 config/webpack 的配置文件中去解析这个文件,获取 engine 的路径信息。

1. 在 rails 项目中导出 engine 路径信息

先实现对 webpacker.yml 文件的读写,代码如下:

# https://github.com/work-design/rails_com/blob/master/lib/rails_com/webpacker/yaml_helper.rb

module Webpacker
  class YamlHelper
    
    # uses config/webpacker_template.yml in rails_com engine as default,
    # config/webpacker_template.yml in Rails project will override this.
    def initialize(template: 'config/webpacker_template.yml', export: 'config/webpacker.yml')
      template_path = (Rails.root + template).existence || RailsCom::Engine.root + template
      export_path = Rails.root + export
      
      @yaml = YAML.parse_stream File.read(template_path)
      @content = @yaml.children[0].children[0].children
      @parsed = @yaml.to_ruby[0]
      @io = File.new(export_path, 'w+')
    end
    
    def dump
      @yaml.yaml @io
      @io.fsync
      @io.close
    end
    
    def append(env = 'default', key, value)
      return if Array(@parsed.dig(env, key)).include? value
      env_index = @content.find_index { |i| i.scalar? && i.value == env }

      env_content = @content[env_index + 1].children
      key_index = env_content.find_index { |i| i.scalar? && i.value == key }
      
      value_content = env_content[key_index + 1]
      if value_content.sequence?
        value_content.style = 1  # block style
        value_content.children << Psych::Nodes::Scalar.new(value)
      end

      value_content
    end
    
  end
end

然后在 rails 初始化过程中增加一个回调,如果相应的 engine 下存在 app/assets/javascripts 文件夹,则将这个路径写入到config/webpacker.yml文件。

# https://github.com/work-design/rails_com/blob/master/lib/rails_com/engine.rb#L30
config.after_initialize do |app|
  webpack = Webpacker::YamlHelper.new
  Rails::Engine.subclasses.each do |engine|
    engine.paths['app/assets'].existent_directories.select(&->(i){ i.end_with?('javascripts') }).each do |path|
      webpack.append 'resolved_paths', path
    end
  end
  webpack.dump
end

2. js 通过数据文件 或许相关的路径信息;

// https://github.com/work-design/rails_com/blob/master/package/index.js

const { basename, dirname, join, relative, resolve } = require('path')
const { sync } = require('glob')
const extname = require('path-complete-extname')
const config = require('@rails/webpacker/package/config')

const paths = () => {
  const { extensions } = config
  let glob = extensions.length === 1 ? `**/*${extensions[0]}` : `**/*{${extensions.join(',')}}`
  let result = {}

  config.resolved_paths.forEach((rootPath) => {
    const ab_paths = sync(join(rootPath, glob))

    ab_paths.forEach((path) => {
      const namespace = relative(join(rootPath), dirname(path))
      const name = join(namespace, basename(path, extname(path)))
      result[name] = resolve(path)
    })
  })

  return result
}

module.exports = paths

这里我们导出了所有的 js 文件路径,用于 webpack 的 entry 配置。

3. 暴露 jquery, rails_ujs 等库

一般的项目都使用了这两个js库,为了能够在代码里使用 $('body'), Rails.ajax 这样的代码,我们需要增加一点配置;

# https://github.com/work-design/rails_com/tree/master/package/loaders
module.exports = {
  test: require.resolve('jquery'),
  use: [
    {
      loader: 'expose-loader',
      options: 'jQuery'
    },
    {
      loader: 'expose-loader',
      options: '$'
    }
  ]
}

我没有对 webpack 和 expose-loader 的代码做深入阅读,不过我认为在 import jquery 的时候直接赋值给 windows.$ 也就解决问题了,不知道 expose-loader 是否还有其他的效果,知晓的朋友可以留言告知下。

4. 接下来修改下 config/webpack 下的 配置文件

// config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const { resolve } = require('path')
const paths = require('rails_com')

const jquery = require('rails_com/package/loaders/jquery')
environment.loaders.append('jquery', jquery)

const env = environment.toWebpackConfig()
env.entry = Object.assign(paths(), env.entry)
env.resolve.modules = env.resolve.modules.concat(resolve('node_modules'))

module.exports = env

config/webpacker.yml 中的 resolved_paths 配置了所有存在js文件的路径,用于配置 webpack 的 resolve.modules,告诉webpack 在解析代码时需要搜索哪些路径,与 assets pipeline 的 assets.paths 配置功能一致;

同时也配置了 babel-loader 中的生效路径。

至此,我们就可以直接编译和使用 engine 下所有的js文件了。

改写 assets pipiline 中的 require 语法

//= require channels  // assets pipeline
import 'channels' // webpacker 

其他提示

  1. webpack 相关配置文件是为 nodejs 使用的,所以使用 nodejs 的模块语法:module.exports/require,前端 js 代码会经过 babel 编译,虽然 webpack能理解 CommonJS 等多种模块体系,但是推荐使用 ES6 的 export/import 语法。

  2. 在Rails开发模式下,如果没有启动 webpack-dev-server, rails会将前端代码编译到 public 目录下,此时修改js代码是不能立即生效的。所以推荐在开发js时,同时启动bin/webpack-dev-server

  3. 当新增或删除了 js 文件之后,entry 改变之后,需要重启 bin/webpack-dev-server。

  4. 由于 config/webpacker.yml 会根据项目的实际路径进行更新,建议将其在 git 中忽略。

  5. 更新 Gemfile 配置: gem ‘webpacker’, require: File.exist?(‘config/webpacker.yml’),这样可以杜绝 config/webpacker.yml 未生成时的报错。

  6. config.webpacker.xxx = xx if config.respond_to?(:webpacker) 这个配置主要是解决上述第5条配置的副作用。

Node.js debugger

在软件开发的过程中总免不了 debug,学会正确的debug姿势 能够有效的提高效率,接下来就科普下 Javascript 中的 dedug 姿势。

Node.js inspect

Nodejs 中提供了一个进程外的 dubug 工具,要使用debug的话,通过 inspect 参数启动 nodejs 即可。

假定我们要调试一个js文件

// code/debug.js
let a = 1

在命令行执行命令:

node inspect code/debug.js

会进入到如下环境:

  1
> 2 let a = 1
debug>

可以通过 help 查看帮助信息。

此时断点位于文件开头的第一行代码,由于此时断点处还未执行首行的赋值表达式:let a = 1,执行 next 进入下一行代码,然后执行 repl 可以进入交互式解释环境(Read Eval Print Loop),方便查看变量、执行表达式等调试操作;

此时输入 a 的结果是 1,如果在执行 next 之前进入 repl 环境,输入 a 的结果则是 undefined,注意并非是报 a is not defined的错误。

在 repl 环境下,可以通过输入 .help 查看帮助信息。

要退出 repl 环境回到 debug 环境,执行 Ctrl + C.

Debugger

使用 node inspect 的方式虽然实现了 debug, 但是从第一行代码就开始进入断点显然不够实用,要想随心所欲的设置断点,可以使用debugger 作为标记。

// code/debug.js
let a = 1
debugger
let b = 2

我们在上述js文件中加入debugger,然后再运行 node inspect,断点依然默认停留在第一行代码处,此时执行 cont, 则直接跳转到 debugger 所标记的断点处。

shebang line nodejs script with debug

对于 shebang line 的 node.js 应用,可以在命令前加上 node inspect, 如:

node inspect webpack --config config.js

dev console

在浏览器环境下,debug 可以使用浏览器自带的开发者工具。

在 chrome 浏览器地址栏输入 about:blank,可以得到一个非常纯净的页面,然后右键菜单,选择检查即可打开开发者工具。

点击 source 菜单,就可以在对应的js代码里设置断点并进行调试了。

queryOjbect

Chrome devTools console 控制台上有一个小众的 API 叫 queryObjects(),它可以从原型树上反查所有直接或间接的继承了某个对象的其它对象,比如 queryObjects(Array.prototype)可以拿到所有的数组对象,queryObjects(Object.prototype)则基本上可以拿到页面里的所有对象了(除了继承自Object.create(null)的对象之外)。而且关键是这个 API 会在内存里搜索对象前先进行一次垃圾回收。

参考阅读

电商的发展与未来

电商的发展才走过了20来年,任何一个行业,20年才是发展初期。电商依然是一个发展空间巨大,充满想象力的行业。

国内的电商已然渡过了野蛮生长期,进入了精耕细作的阶段。接下来的发展又将有什么趋势,说下我的分析和观点。

平台服务到细分的云服务

国内的电商,平台占据了过高的份额,以淘宝为典型,以至于平台商掌握了太大的话语权。由于商家逐利的本质,平台商的话语权对于商业的发展往往很难公平。

所以第一个趋势,就是电商基础设施将更多的开放服务。也就是由平台往“云”的发展,一切都是云。“云”的特点:弹性、可扩展、分布式。具有成本更低,效率更高,服务品质更好的优点。

送货的变革:

云仓储+云物流+云提货柜

以京东为代表,云仓储+云物流将带给我们最低的仓储成本和最快的送货速度,最好的用户体验。

弹性,可扩展: 商家有多少货,就租多大空间的仓库。货物需要在仓库里待多长时间,就只为所实际存放的时间付费。云仓储是弹性的,不存在浪费,这样的仓储成本必然是最低的。    分布式物流:大数据预测某个地方的需求量,通过最佳预测模型的提供的参考需求量,商品分布式存储在各个地方。

分布式存储造成的物流成本是最低的,物流速度是最快的。商品就在离用户最近的仓库时刻准备着,只要用户“一声令下”,就从最近的地方送到了用户的手中。   

云提货柜:

在小区里,快递员只需要将商品放在离用户最近的提货柜里,用户自己在方便的时间溜达过去,刷卡或者输入ID,提货柜就把用户的货物交出来了。

云提货柜也并不是独属于某一家快递公司,而是作为基础建设租给各个快递公司。不仅如此,云提货柜还可以充当收快递的工作,就跟过去的邮筒一样,只要把你想邮递的货物贴上“邮票”放到云提货柜里就可以了。

降低快递员送货的时间成本,可以在单位时间内送出更多的货物。

增加用户的体验,不用为等快递问题烦恼了。

便利店与快递行业的结合

在一切都是电商的背景下,便利店永远是不会消失的实体。 对于用户来讲,药品、生鲜食品、快速消费品(牙膏、香皂等),通过便利店来购买,其他的统统通过“云提货柜”来购买。 便利店可以与提货柜很好的结合起来,适合通过网上购买的物品通过提货柜到达用户手中,通过便利店成本更低的货物则直接在便利店购买。

服务的变革:

云客服+云销售

云客服的概念是淘宝首先提出来的。 对于企业来讲,销售人员的招聘、培养是一个很头疼的事情。但是有了云销售,你就没必要头疼了。 云销售人员经过统一的培训,基础的销售技巧,对用户的心理把握。有比你要专业的教师来为他们提供培训。 云销售经过统一的管理和激励。未来你只需要将你的销售任务交给云销售服务公司,告诉对方每销售一个产品提成多少就OK了。 当然云销售服务公司可能会提供一个很贼的管理系统,你能通过这个管理系统看到云销售人员为你工作所有的记录。也许销售情况不尽人意,那你提高你的消费就可以激励对方了。比如为你提供更多的云销售人员,给你提供更优秀的云销售人员。   

云售后

京东的售后服务是比较不错的,但我觉得应该有一个云售后公司。

用户下单的变革:

新的网站联盟+新的淘宝客 网站联盟的变革 任何行业,传统销售的总份额正在越来越低,线上销售的总份额越来越高,线上销售正在挤压传统销售渠道的份额。

没有任何一个时代,让厂家(服务商)离消费者如此之近,代理商(淘宝C店)的优势一去不复返。线上销售的解决方案成本将越来越低,任何传统企业可以拓展自己的线上销售。

不依赖线上销售平台:电子商务平台强势,通过天猫商城等销售平台很容易触及销售额天花板。企业自行拓展线上销售是新的增长机会。自建官网是对线上其他渠道的一个补充。

二次消费比例越高,综合成本越低。

产品感悟之分类

分类概说

大部分的网站访问行为中,用户的需求是寻找到某个信息,“分类”是用户跟网站打交道的第一个环节,也就显得格外重要。

分类与搜索的关系

在门户网站时代,信息的聚合是基于分类的;随着互联网信息的爆炸性增长,仅仅是分类显得越来越力不从心,于是出现了搜索。 分类(筛选)是人们筛选信息的基本方式,相对于搜索,通过分类用户更容易获得符合其需求的信息。

分类的多维度

“分类”实际是对事物的某个属性的一个描述,而一般用作分类的“属性”,往往选择的是事物最典型的特征。 如在电商网站中,要对“电动剃须刀”进行分类,其分类是“生活电器”还是“个人护理”呢。那我们就要看哪个是“电动剃须刀”最典型得特征。

这个时候就仁者见仁,智者见智了。就像做选择题一样,你只有选择一个“你认为最合适的选项”。

假设两个人群,A人群认为电动剃须刀属于“生活电器”,B人群认为电动剃须刀最典型得属性是“个人护理”。 当他们在电商网站上找电动剃须刀的时候,A人群会去“生活电器”类别下寻找,而B人群会去“个人护理”类别下寻找。 目前大部分电商普遍只是将“电动剃须刀”放在某一个分类下,这种情况下,A、B两个人群中总会有一部分人在自己预期的分类下找不到相关产品,充满失望。

我在京东商城购物的过程中,就有数次满怀期望的去某个分类下找自己想要的产品,然后沮丧的发现没找到的体验。

“分类”的类型

所以分类本质上是多维度的。所以在设计分类的时候,就需要有一个明确的分类维度(分类方法)。

“分类”的级别

分类往往是多级的。

信仰-信则仰之

人的一生是个寻找目标,确立目标,然后实现目标的过程。

什么是目标?

人所有的目标都源于身体和精神的需求。与其说我们是在追求目标,不如说我们实际上追求的是实现目标时的满足感。这种满足感就是对身体或心理需求的满足。

满足感,是一种愉悦的心理状态,也往往伴有一些生理机制,比如分泌多巴胺。

目标与满足

既然我们追寻的目标并非”目标“,而是实现目标之后的满足感。那么如果一个目标并不能带给你满足感,而是带给你”痛苦“。那要实现这个目标,就需要意志力来控制自己去完成了。

比如小时候老师让我们做作业,在有些孩子眼里,“作业”带来的体验不太愉快,可是逃避是不可能逃避的。不然要带来更不愉快的体验,比如挨打。这个时候,我们就需要调用意志力来控制身体来完成这个目标了。

什么是意志力?

意志力是通过意识控制自身的能力。科学家研究指出,这个能力依赖一种物质,所以意志力是比较珍贵的,使用意志力的过程也是消耗意志力的一个过程。

但是意志力也是可以锻炼的,锻炼意志力有两个层面的目标,一是增强生成“意志力物质”的能力,总量变多,可消耗的也就变多。二是减少单位消耗,也就是控制自己做某件事的过程中,意志力只是一个因素,兴趣也占一部分因素,那单位消耗的意志力就减少了。

未知的目标,好奇心

大部分情况下,我们总是知道,我们要实现的这个目标,是否会带给我们愉悦的感受。偶尔的情况下,我们不知道目标是否会带给我们满足感。甚至不知道,这个目标经过我们的努力是否能够实现。

也就是说人们对于目标和满足是一个学习的状态。比如抽烟,小男孩子大都有被玩伴蛊惑抽烟的经历,有的人体验到了抽烟的乐趣,就可能会成瘾。而未学会抽烟的小伙伴,往往也是没有真正体验到抽烟的乐趣的,我也有过尝试抽烟的经历,不过实在是不觉得有什么好抽的。

当从某一件事物中体验到了乐趣之后,那对这种乐趣的再尝试获得,就会成为我们坚持一件事情的最佳动力。

动物世界的权力

食物链的结构从某种程度上揭示了生物的进化规律:人类是生物进化的最高级别,是高级动物中的高级动物,自然也处于食物链的最顶端。昔日的万兽之王——老虎兄弟,若不是人类可怜它为它制定了个“濒危动物保护法”,老虎我们也照样吃。

我一直在想,为什么人类能够成为万物之王。以前接触过一个观点:因为人类学会了做熟食,提高了消化效率,有了更多的时间思考,发展智力,这个生物学定律被称做“胃小脑大”。和人类同处一个始祖的大猩猩每天花在吃饭和消化的时间是12个小时以上,它基本就没有什么时间去思考并发展智力了,所以大猩猩依然是笨猩猩。这个理论倒很有道理,但它不是根本原因,只是原因之一。如果碰巧不是人类先学会了做熟食,而是大猩猩,那是不是人类的地位就没有现在这么乐观了呢。

我想先谈谈一种生物:老虎。和人类不一样,这位人类册封的“万兽之王”是独居动物。就算老虎很牛逼,可它就“一只老虎”,翻不起风浪,以至于沦落到现在频临灭绝的境地。人类和老虎有一个很大的区别,在于对权力的欲望。别看老虎是万兽之王,但是老虎并没有权力欲望。权力,是一种支配欲,是对同类的一种支配欲。如果老虎有权力欲望的话,它的正确做法是把它的手下败虎纳为臣民,为他去捕猎。可能有人会反驳我,说是老虎食量太大,不容许第二只老虎存在。但这个说法是不对的。和老虎体型、食谱都差不多的狮子,便是群居动物。

群居,是一种方法。这种方法为生物进化提供了一个很好的解决方案。但是人类的群居和其他群居动物相比,有一个很明显的区别。一般的动物群居是出于团结起来以提高生存质量,蚂蚁、狼群、狮子,大象这些群居动物,虽然都有首领。但是缺乏阶层,没有一层一层的管理体系,工蚁便是工蚁,从来不分为工蚁总监,也没有工蚁经理,大家都是在做同样的事情。更多时候,一只狼为成为首领打败其他的狼是为了得到更多异性的青睐,从而将自己的”优良基因“一代代传递下去。

而人类不是,人类更倾向于对同类的支配。正因为此,人类的社会发展才经历了相当长一段时间的奴隶社会。可能有种族没有经历过封建社会,但是绝对没有一个种族是没有经历过奴隶制度的。正是因为对权力的追求,造就了人类的万物之王的地位,成为了食物链的最顶端。

文化同样也是一种进化。人类社会经历的原始社会,奴隶社会,封建社会的变迁,也是一个从低级走向高级的过程。在这个不断发展的文化之中,人类对权力的诠释也逐渐趋于完善。过去是通过暴力让同类沦为自己的奴隶,为主人提供服务。现在,暴力作为“战争方式”已经过时了,新的体制为人类追求权力提供了新的方法。比如在中国,你可以入党,可以考公务员。但无论怎么变化,权力的本质:对同类的支配欲是没有变的。

更多的时候。我宁愿做一只老虎。有着更多的自由和尊严,远离权力的世界。但是不论怎样,我这样的想法,终究难为人类种族的主流文化。我可以有自由,但是缺乏权力却可能会被剥夺尊严。因为人类要成为人类,而不是沦为老虎。于是,我就这样看着人们不断在权力旋涡中的挣扎巩固着人类在大自然中的统治地位。

淘宝伤害了谁

在大众的意识里,淘宝是中国电子商务的超级英雄,带领着中国在相关领域走在了世界前列。 诚然,淘宝的贡献和价值不容抹灭,如果没有淘宝,中国的电商不知道会成为什么样子。历史就是这么神奇,不能重来,没法论证。

淘宝的本质

所有ToC的商业模式,都可以归纳为一句话,即:商品从被生成出来到来到消费者手里的过程。 这个过程主要包含了两件事:

  1. 如何被消费者认知,并促成消费者达成购买;
  2. 如何从工厂出发,经过仓促、物流最终达到消费者手中;

那如何衡量淘宝把这两件事是否做好了呢?,也主要是两个角度:

  1. 是否降低了总成本;
  2. 是否促进了这个过程的良序发展,即淘汰“坏分子”,留住“好分子”;

是否降低了总成本

在围绕淘宝平台的产业链中,主要有哪些成本:

  1. 淘宝赚的广告费;
  2. 研究淘宝SEO(搜索优化)、研究如何在淘宝上投广告的运营人力成本;
  3. 店铺运营团队的装修、设计师、文案人力成本;
  4. 刷单的人力成本;

以上四部分的成本,即没有提升消费者购买到产品的品质,也没有提升体验,这些成本本不是应该存在的。但是他们存在,并且消费者为此买了单。

淘宝的商业模式注定了“劣币驱逐良币”

大家应该知道,淘宝、百度、腾讯是中国最大的三大广告收入商,消费者为此付出的成本可想而知。

之前淘宝假货泛滥,淘宝官方与假货商家作为利益共同体,属于几乎无解的“官商勾结”。这一切都是淘宝的商业模式决定的。

由于淘宝采用的是主要基于搜索陈列商品的产品模式,价格比较自然而然就成为了用户做购物决策的一个重要指标,所以商家要尽可能的让商品定价处于一个比较低的水平。

当然不是价格低就一定会有流量的,商家还需要从淘宝购买流量。商品价格难以定高,还要为流量付出大量额外成本,那就只能在商品本身的成本降低上面做功夫了。能够立杆见效的,当然就是偷工减料生产伪劣假冒产品了。

为什么唯品会有机会搞掉淘宝原本的主要份额:服装

唯品会无论是带给消费者,还是服装厂商的积极价值被低估了。

五年之前,唯品会远不如今天广为人知,有个朋友参观了唯品会之后给我们做了个分享:唯品会在当时建成了国内乃至全世界最先进的智能仓储系统,光仓储系统的机器人开发人员就有近1000人,年代久远,1000人这个数字可能不准确,但是当时的我显然是被这个数字震惊到了的。

对于服装厂,唯品会低价收购库存,解决了其资金和仓储占用的痛楚。

对于消费者,唯品会显然给出了极具诚意的价格。

在商品到达消费者的链条里,唯品会大胆投入技术,让科技的力量尽可能降低仓储成本,提升物流效率,能在大部分城市做到不属于京东的送货体验。

这样的企业,我们应该更尊重和推崇,作为消费者用“脚”投票即可。

收租的平台商大都是“恶龙”

不止淘宝,还有美团饿了么这些企业,对于商家而言,只是由于互联网的发展和进程,替代了线下商业街的位置。过去商家们跟自带流量的商业街交房租,如今给这些互联网巨头交租金。

这也是为什么美团饿了么频频爆出让你不敢吃的外卖的原因。

前段时间,去我常去的菜市场买片皮鸭,一家让我好上这口美食的小摊,却发现摊位已被转让。在这个片皮鸭附近,有个年轻的嫂子也卖片皮鸭,当然味道远不及这家,价格也便宜一些,不得已,我去买了半只,顺便与老板聊天得知,她同时也在外面平台上进行销售,虽然来她摊位的食客不及已转让的那家,但是嫂子家在外卖平台上卖的远比另一家要好。

因为再也吃不到好吃的片皮鸭,我有些失落,而这大概只是劣币驱逐良币的一个缩影。

函数式编程与面向对象编程

我们接触函数式和面向对象概念这两个概念往往是来自对编程语言的认知,大部分编程语言会给自己贴上 函数式 或者面向对象的标签。 比如Ruby,是一门非常纯粹的面向对象编程的语言。

本质上来说,函数式编程和面向对象编程,都是编程的一种方法。给自己贴上对应标签的程序语言,更准确的说法是,更适合某种编程范式的语言。

那什么是函数式编程和面向对象编程呢?

函数(方法)是否在对象上调用?

以 Python 为例,有些函数是全局函数,不在任何对象上调用。如:

print('test')

有些函数,则是需要在某个对象上调用,如:

'A'.lower()

这个例子揭示了面向对象编程和函数式编程最本质的区别。

函数式编程是“无状态”的,输出的结果只取决于输入的参数。

而面向对象编程的函数调用,对象本身和传入参数都会直接参与函数的运算过程中。

在对象上调用的函数该对象一定会参与函数运算么?

以Ruby为例,我们在一个字符串对象 ‘a’ 上定义一个单例方法

a = 'a'

def a.sum(x, y)
  x + y
end

a.sum(1, 1) 
# => 2

你肯定觉得我这个做法很二,这个方法跟这个对象一点关系都没有,干嘛要在这个对象上定义这样的方法。

在这个例子中,表面上是个面向对象编程(在某个对象上调用),实际上是个函数式编程风格的代码。

函数式编程和面向对象编程的优劣

我们将上面的例子进行一个改造。

a = 1

def 1.sum(y)
  self + y
end

1.sum(1)
# => 2

这是一个典型的面向对象编程风格的例子。跟上面的代码相比,很明显代码量减少了。面向对象是对程序员的解放,迎合了程序员都是懒人的天性,真是nice呀。

既然面向对象这么好,那函数是编程存在的意义是什么呢?

  1. 稳定,易于测试 在函数式风格中,运算结果只取决于传入的参数,传入的参数是什么,非常容易观察到,那结果也是容易推导出来。大大降低了程序员出错的概率。

  2. 性能更高 在面向对象语言中,对象的生成(实例化)是会消耗内存和计算资源的。所以函数式编程语言的性能相对于面向对象往往性能更高。

Ruby是纯粹的面向对象语言

熟悉ruby的程序员应该知道,Ruby号称是一门非常纯粹的面向对象的程序语言,在ruby中,“一切都是对象”,方法只会在对象上进行调用。

好像有人提出反对,你看例子:

p '^_^'

这个方法就没有在对象上进行调用呀。

其实Ruby只是隐藏了某些细节,让你觉得这个不是在对象上调用的而已。 在ruby中,如果没有指定调用对象,则会使用 self 调用该方法,而self是谁取决于上下文环境。

self.p '^_^'

# NoMethodError (private method `p' called for main:Object)

这个例子中,方法p是个私有方法,如果指定对象调用会报错。但是这个报错,正好告诉了我们真谛:方法p也是在某个真实的对象上调用的。

ruby里是不存在脱离对象调用的方法的,不像python,python里的全局函数是脱离对象存在的。

所以说js是不纯粹的。

Ruby是最方便用函数式编程风格的程序语言

由于 Ruby 中所有的方法都是在对象上调用,实现函数式风格亦不能违背这个准则,于是我们就需要找到一个尽可能“小”的对象。

如何找到尽可能“小”的对象

在回答这个问题之前,我们需要先知道对象中包含什么?

如何减少对象的开销

先看一个普通的 Ruby 对象(类)的实例化

o = Object.new

每次对象的实例化都是一次开销,所以我们用于函数式编程的傀儡对象最好是生成之后,就可以一直用,最好的变法就是定义一个全局的变量或者常量引用。

变量还是常量

在目前的 Ruby 解释器(2.7及以前)实现中,常量和变量本质上没有区别,除了在给常量重新赋值时会有警告。据说在 Ruby3.0 的版本中,对于常量会自动调用freeze方法,从而避免对常量的改变。

在对象上定义方法,而不是在类上

由于并没有多次实例化的需求,我们只需要在这个生成的对象上定义方法即可。 需要知道的是,在ruby中,方法实际是只能在类中定义的。在对象上定义的方法,实际都定义在该对象的 singleton_class 中。每个对象都有一个唯一属于它的 singleton_class。

沿着上述的思路,在ruby中最佳实践是通过 module 来实现。

module Helper
  extend self

  def show
    p self
  end
end

复杂应用场景下的用户体系架构设计

用户体系

说明

设计思考

用户可以自己注册登陆,也可能由组织管理人员在后台为其添加账号及其相关信息,这两个场景是相互隔离的。

最佳的方案就是:两个渠道都可能发生的信息添加,需要一个能验证用户身份的唯一标记。当用户完成了该账号的验证,则相关的数据会自动绑定到其在系统里的唯一身份 User 模型上。

Profile、Member是后台可能存在添加数据情形的,Account 主要用于处理账号验证。

在用户能够验证身份之前,有些系统活动可能会提前发生,比如生成订单、生成身份二维码,为了简化系统的复杂性,最好通过 User 模型来处理这些数据。

而当用户实行验证身份操作的时候,用户可能已经通过其他账号在系统里有过活动了,也就是说有另外一个 User 模型指向同一个用户。这显然违背了 User 模型的设计初衷,于是就涉及到用户的数据合并。

当存在多个Account时,我们需要用户选择其中一个 Account 为主账号。我们只需要将实际属于当前用户的其他 Account 对应的 User 数据替换成主账号所对应的 User模型即可。

在这个架构体系下,只需要遍历数据库,将准备废弃的 User 模型 ID 替换成当前住账号的User ID即可。

Profile 是对人这个主体的描述,按常规理解,也是同类系统的常规设计,Profile的信息(性别、生日)是确切的一份数据,一个User只和一个Profile关联。之所以需要使用多 Profile 的架构,主要是源于用户自主及后台都会添加Profile数据的需求。

但是经过对 Profile 的进一步思考,Profile 是实质上是一个身份描述,一个人在不同的场景下是需要不同的身份描述的。比如职场的身份描述和作为家长的身份描述可能就需要不一样。

除了用于身份描述,Profile 模型在处理一些具体业务的时候也更为适合,比如客户关系管理架构中的 家长和学生的角色。

架构设计

所有gem介绍

rails_auth

rails_org

rails_role

rails_profile

识人之人性篇

识人有两层含义,一方面是识他人,一方面是识己。而识人的根本是先了解人性。

人作为个体存在,其本性是“自我满足”

人性,就是人的天性,是人类下意识的本能,可从根本上决定和诠释人类的行为。人性伴随着人类的产生而产生,伴随着人类的延续而延续。

人性存在的使命也只有一个,即维持人类的生存和延续,人类生存的基本条件就是要有食物,人类延续的基本条件就是繁衍。所以人类具有两大基本欲望,食欲和性欲。古人有云:食色性也,也正是要表达这个意思。

欲望左右着人类的行为,其本质便是自我满足。自我满足的狂暴状态,就是贪婪,正所谓有备而无患嘛。自我满足的防御状态,就是嫉妒,害怕失去,害怕别人分了一匙或许也许大概属于你的羹。看过电影《七宗罪》的朋友们应该能看出来我已经比较清晰地阐述了五种原罪的始末:自私、食欲、淫欲、贪婪、嫉妒。

总结一下:人类的天性就是自我满足,为自己而活,可以说人类所有的行为都满足这个定律。同样,这个定律也适用于动物界的其他非人类。

人作为群体存在,其本质是“互惠互利”

人与人之间的交往是人类作为群体存在的本质,人际关系法则维持着群体稳定,促进了人类社会的繁荣发展。识人的目的便是教你如何睿智地处理人与人之间的交往。人际关系的动因是利己、自我满足,人际关系的表现形式是交换,人际关系的结果是共赢、互惠互利。

在群体中,人类的私心绝不会任它肆意膨胀,和作为个体存在“自我满足”的自私本性相对,群体的生存智慧赋予了人类另一个本性来克制私心,这种本性便是会为他人着想,设身处地,己所不欲勿施于人,并升华为善良和同情心,促进了你的舍得和付出,正所谓“人之初,性本善”。正是因为这种付出,才使得这个社会群体趋于和谐。

当然,付出是为了得到。人类正是因为明白了,有付出才有回报,才在群体生活中把“互惠互利”这四个字的真谛发挥到极致。

生存是为了信仰,还是信仰是为了生存

人类的初级阶段,也就是原始社会,人活着最首要的目的便是生存,人类的信仰也就是生存信仰,为获取生存所必需的条件而费尽心机,能活下来也就谢天谢地了。当生存的目的能够容易达到之后,人类就开始尝试认识自己的世界,便有了神农尝百草,有了敢第一个吃螃蟹的人。认识世界的武器便是好奇心,好奇心的目的也无非有两个:一是寻找对自己有用的资源,二是发现所接触到的事物是否会对自己形成威胁。趋利避害是动物界的本能,就像老鼠天生看到猫就要逃跑,小狗看到骨头就要流口水一样。

当人类的生存信仰变得容易实现之后,人们便开始失去目标,变得迷茫,迫切需要寻找新的信仰。我把这个阶段称为“创造信仰”阶段,信仰的创造是与人类自身的认知水平息息相关的,认知水平是人类闲暇下来通过好奇心观察这个世界的成果,从远古的神话,到各朝各代的宗教信仰,到现代的科学理性时代。人类信仰的改变,都取决于对这个世界的认知程度。他们对“信仰”的忠诚,是经过自己独立思考的忠诚,没有怀疑得忠诚,也拒绝怀疑。但是信仰并非一定是建立在以事实为基础的真理之上。因为不是因为理解了才能信仰,而是信仰了才能理解,相信便是真理。

当今社会,我们的信仰不断受到来自各方的文化冲击,处于一个旧价值体系已陷入困境而新价值体系尚未建立的断裂时期,信仰沦丧,无可相信,迷失而茫然。在寻找信仰的奔途中左突右撞,一些不完美的信仰,如“拜金主义”便从而滋生,而信仰总是趋于完善的一个过程,耶稣基督也是在不断发展的。

信仰由什么决定?

信仰,是一个人的价值观。一个人的价值观会受到多方面的影响,但总的来说都是后天形成的,来源于他对世界的认识,并受到群体价值观的影响,群体的价值观也就是你能接触到的人们灌输给你的价值观,包括媒体舆论所宣扬的社会价值观。群体价值观的影响不是自己能够选择的,取决于人的生长环境,父母老师的教导,朋友的影响。

到了一定阶段,一个人的信仰开始走向独立,并由一个人的智慧层次决定,一个人的智慧层次可以通过被引导和自我引导得到提升,智慧层次是一个成长的过程,原则上来说随着年龄阅历不断增长。

一个人对信仰的独立思考历程往往经历三个阶段,也是三种类型:

信仰,决定了“自我满足”的表现形式

说到这里,“识人”的问题就已经变简单了。信仰决定了一个人“自我满足”的表现方式,评价一个人,就去评价他的信仰。人就会为了信仰不懈追求,“自我满足”是实现信仰的动因。就像雷锋正是因为把“关爱他人”作为了自己的信仰,才会为了这个信仰不懈追求,并把对信仰的追求历程写在日记里,时而意淫。

一个人身边的人会对他价值观产生重要的影响,正是近朱者赤,近墨者黑。物以类聚,人以群分,要想了解一个人,就去了解他身边的人。

一个人的善良表现度,决定了他在与人相处过程中的。愈是善良的人,会置身处地的站在他人的角度为他人考虑,这样的人更贴心。观察一个人,首先是否具有为他人考虑的习惯,然后是为他人考虑的程度。

Rails 应用模块化之 Rails Engines

关于Rails Engine

Rails Engine 是来自于官方 Rails 应用模块化的最佳方案。

业务分块

Engine 是对业务强相关的代码的一个组织。

统一UI

传统的模块化,大都只涉及Model层功能,主要是因为UI很难满足使用群体(程序员)群体的个性化需求。不过我们的目的是提供一套现成的方案,从而尽可能降低门槛和成本。至于现有的UI,能满足需求的话就用即可,如果不能满足需求,也可以很方便的取override。

UI统一采用Fomantic-UI,Fomantic-UI 是Semantic-UI的社区版本。

如何使用

  1. engines 提供了大量的针对Model层和Controller层的通用方法,我们尽可能采用Ruby中的 include(prepend)/extend 方式去引用。
    • include: 实例方法
    • prepend: 实例方法
    • extend: 类方法

rails_auth中在module RailsAuth::User 提供了针对User的鉴权方法,使用时,在User模型中include即可。

class User < ApplicationRecord
  include RailsAuth::User
end
  1. engines 预定义了大量model, 直接使用model看上去没有 include module 的方案灵活,实则不然。
    • ruby中对于预先定义过的class, 可直接使用class 关键字打开类,打开类的时候注意,继承的父类须保持一致, 或者省略继承。我们所提供的预定义的model,都是继承于类ApplicationRecord
    • 在开发环境下(cache_classes设置为false),应用里定义的model 会默认优先于 engines 里的 model加载,需要在model定义之前 require engine里的model, 这并非副作用,可认为相当于使用 include 引入实例方法;
  # 假设在engine the_trade 已定义 order model
  
  class Order < ApplicationRecord
    include RailsTrade::Order
  end

当在应用里定义了跟engine里已存在的同名的Model,则engine里的定义会自动失效。

以model User为例,这个模型实际我们已经在 RailsAuth 这个Engine中预定义了,不过预定义的代码非常简单。

class User < ApplicationRecord
  include RailsAuth::User
end unless defined? User

这个定义做了两件事:

  1. include 当前engine 中定义的模型相关代码;
  2. unless defined?判断如果该 Model 在应用中已定义,这些代码则不会生效。

对于一些常用的模型,比如User, 我们会在多个 engine 中定义相关功能,所以我们在应用里定义一个 User 模型,把所用到的Engine里User相关模块 include 进来即可。如下例:

class User < IneeRecord
  include RailsRole::User
  include RailsAuth::User
  include RailsTrade::User
  include RailsNotice::Receiver
  include RailsOrg::User
  include RailsProfile::User
  include RailsTutela::User

  attribute :timezone, :string, default: 'Beijing'

include 的顺序也决定了方法继承的逻辑,可以做到很灵活的进行override。

  1. Engines里的代码可以从各个层面去override;
    • model 层: 无论是include进来的module中的方法,还是重新打开类,定义同名方法即可,区别是include module引入的方法可以通过super去调用,而打开类只是被覆盖;
    • view 层: 以同样的路径覆盖位于engine中的文件即可。
    • controller/routes 层:在routes定义同路径路由覆盖;

模块化开发

模块化开发

在工程领域,模块化是一个老生常谈的话题。

尤其是对于一些复杂系统化的生产,如汽车工程,模块化带来了成本效益上巨大的提升。

是否应该模块化已经没什么好置疑的了。怎么模块化才是关键。

模块化的本质

模块,是相关功能的一个组合。各个模块再通过统一的接口连接,组装。

以汽车行业为例,顶级的汽车厂商一般都是生成关键模块的技术,如发动机等。同时组装各个模块的技术。

如何模块化

通用性

模块本质上是为了复用而产生的,也就是说其功能具有通用性。

可插拔

模块与主体之间的关系降低了耦合性,可插拔,可单独升级。

统一接口标准

由于模块之间存在交互,有一套统一的接口标准,会更方便。

微服务困局

微服务算是模块化在软件工程实践的一种方式,模块与模块之间通过接口通信来实现交互和连接。

由于微服务相对于传统的模块更加独立,传统的模块脱离了主体是没有什么价值的,但是微服务可以从某种意义上成为独立的应用。

由于此,微服务产生了一些额外的成本。比如:

  1. 独立的接口;

更多阅读

如何提升转化率

转化的前提是首先有用户群体,关于用户群体,有另外的文章论述。

引导用户的需求

如果用户带着明确的目的,确切知道自己想要什么样的产品,他直接通过商品列表选择自己想要的商品就OK了。但是这部分人群总是有限的,如果我们要扩大我们的销售,则需要找出拥有潜在需求的那部分用户,并且把它们说服。

想想看,一般什么样的信息容易引起用户的关注? 笑话、色情等除外,这些虽然容易引起用户关注,但不是我们所要开发的用户需求。

除此之外,我认为跟用户有关的信息最容易引起用户的关注。每个用户自身都具有一些属性,他的性别、年龄、职业、兴趣、平时的活动。

评测是一个比较有意思的功能,他能够根据用户的属性,自动分析用户的需求。进一步根据用户的需求,向用户推荐能够满足其需求的产品。

社交化推荐

亚马逊(Amazon)因为有一套比较出色的推荐系统,被媒体吹捧的比较厉害。那么亚马逊这套系统是怎么实现的呢? 简单的说,Amazon会记录分析用户的购买记录,得出与该用户相似群体的其他用户,然后根据其他用户的购买记录,推断出这个用户可能会喜欢什么商品。

知乎、网易云音乐等应用采用的社交化推荐引擎也是基于同样的原理。

很管用很科学,很多时候,推荐的产品会引起我们的兴趣。

高转化率的文案是怎么炼成的

互联网说到底是一个工具,用于销售,则是在完成传统销售人员的工作。

如何进一步突破?

亚马逊就像在告诉用户,这有一个商品,很多跟你一样的人都会买它呢?对于我来说,会引起我兴趣,但是说服力还不够。如果亚马逊能够进一步告诉用户,你需要这个产品是因为你的工作环境容易引起你的颈椎不好,这个产品能够缓解颈椎的疲劳。是不是我就会更加心动了?

这是一种极其自然体贴的营销方式。站在的用户的角度,思考用户需要什么,然后帮用户提供解决问题的方案。

用户是决策者,我们应该让用户变得更主动,提供足够的信息,帮助用户做出选择。而不是一味的告诉用户我们有十个亮点,用户一是记不住,二是其实打动用户的也就那么一两个点。

大数据分析与设计

要搞定消费者,就要先了解消费者。 消费者首先是人,而了解一个人需要通过很多方面。比如:性格、性别、信仰。但是我无法肯定需要从哪些方面来了解一个人。 可能某一天,我又会想到一个新的“标准”。

一般的数据库设计方式是:在关于某个人的数据库中添加一些column 假如有一个person表,一般table设计会包含一些列:name, age, sexy

这个方法有一个缺点,就是不够灵活,比如为了更详细的了解一个人,就需要添加越来越多的columns,如职业、地区,疾病,生活方式,简直就是灾难。 可能对于大部分应用来说,添加一些column就足够了,但是我们所追求的不是越详细的了解一个人吗。 而且,很重要的一点,我们不是为了了解一个人而去了解他,又不是窥探他人隐私。 我们的目的是分析用户的需求:比如不同的年龄段、不同的性别,各对应什么样的需求。

于是我设计了一个人群表:crowds,这个表里记录了所有的人群属性。 接下来的问题是,如此多的人群属性,需要将它们归类。

比如性别:分男和女 是否生育:已生育、未生育、正怀着宝宝的。 职业:体力劳动→脑力劳动; 职位:低级→高级;

Gem 设计理念之“默认配置”

默认配置是对 Rails的经典理念:“约定优于配置”,“主厨精选”的一个实践。

在此原则指导下,我所开发的Gem有:

Word Design

Work Design

互联网出现了几十年,给我们的生活带来了巨大的改变,特别是在To C领域,但是在一些行业应用上,只能说是应用了web,还谈不上对行业的变革,尤其是对于一些中小型企业,和十年前的运作方式比起来,没什么差异。 Work Design 的使命就是,尽可能降低中小型企业使用IT和互联网来变革自身管理和业务的门槛,提供一整套低成本的解决方案。

为什么选用Rails框架

Web应用开发领域,Rails是以生产力为出发点的最佳框架。基于Rails生态,尽可能提升web应用开发效率及降低开发成本,对于企业管理系统的开发有明显优势。

中小型传统行业面临的互联网转型困境

巨头拥有天然的马太效应,要想跟大公司一比,只能在效率和成本上下寻找突破口。

怎么做

开发原则

阅读更多

追求生产力和效率

对于任何企业来讲,效率和生产力是企业发展最原始的动力,对于创业型企业尤为重要。

对于软件开发,单就效率来讲,需要写的代码越少,需要协作沟通的环节越少,则效率越高。

除了技术架构上选择了前后端不分离,选择了rails。要想追求达到极致的开发效率,还要重构软件开发流程管理。

职业重塑

目前很多企业的软件研发流程依然是:产品原型 -> 设计高保真 -> 开发。这个开发流程后面的环节会对前面产生依赖。

经过实践,我们逐渐摸索了一套多端并行的组件化开发模式。具体如下:

engine安装使用指南

假设engine 的名称为 rails_xxx

安装gem

将 gem 添加到应用里的 Gemfile 文件中

gem 'rails_xxx'

然后在应用目录下执行:

bundle

运行migration

每个engine里都会有migrate 文件,运行

rake rails_xxx_engine:install:migrations

会将engine里的migrate 文件 复制到应用的 db/migrate 目录下。

添加配置文件

所有 Gem 采用Rails内置的 ActiveSupport::Configurable 模块,统一配置语法如下:


RailsXxx.configure do |config|
  config.admin_controller = 'PanelController'
end

通用的配置项有:

关于全栈开发

关于全栈开发

本文所阐述的全栈开发是Web开发中的一个软件工程概念,

为什么看好全栈开发

在web开发的早期,HTML比较简单,那个时代的web开发就等于全栈开发。

随着前端的蓬勃发展,特别是前端mvc框架的不断涌出,技术栈的复杂度提升,人们开始从软件工程角度实施前后端分离。即后端开发开发API接口,提供JSON(xml等)数据,前端基于后端的数据渲染页面。

对于大型互联网企业而言,工程师往往只会负责某个小模块,前后端分离带来了工程和协作上的好处。但是我们要面对的是另外一个场景:如何让传统企业以最低的成本和最高的效率进行互联网+改造。从这个角度而言,全栈开发的优势就得以显现了。

全栈开发的优点,及如何发扬

在软件工程中,代码量越少,往往意味着应用的性能较高,开发的成本较低。当然不是绝对,大致规律如此。

全栈开发的缺点,及如何避免

更多阅读

模块化开发的工具链

由于在项目中采用了模块化开发,相当于把一个大项目拆分成了若干个小项目。从某个角度上来说,增加了项目开发的复杂度。 单一项目开发时涉及到的代码改动,只需要提交一次即可,改成engine开发后,大概率会涉及到多个engine的改动。 提交代码的次数变多,提交代码后又需要更新Gemfile文件,也增加了成本。

如果用传统的方法,这些成本增加的时间消耗无疑是不太划算的。

在长期的实践中,我摸索出了一套工具链的优化方案。

解决Gemfile问题

Gemfile文件中正常引用 gem 的路径即可。

gem 'rails_com', github: 'work-design/rails_com', branch: 'master'

.bundle/config 设置如下

BUNDLE_LOCAL__RAILS_COM: "/Users/qin/work/engine/rails_com"

# 此选项会忽略检查分支是否一致。
BUNDLE_DISABLE_LOCAL_BRANCH_CHECK: "true"

解决批量提交问题

借助IDE的帮助可以轻松解决。

  1. 批量Git Commit 在 Tool Window Bars 中找到9: Version Control,点击Commit (⌘K),可以批量进行提交。

  2. 批量Push

RubyMine 没有提供批量push的功能,我们可以通过git hooks 功能 设置为 commit 发生后自动push

在 .git/hooks 文件夹下创建 post-commit 文件,添加以下内容

#!/bin/sh
git push

Ruby中的Benchmark

Benchmark 是用来衡量代码执行时间的一个工具,是程序优化的基本工具。

Process 相关知识

在使用 Benchmark 之前,我们需要先了解几个概念。

我们的代码是在操作系统的进程里运行的,程序执行的时间信息,也是由进程给出来的。

# 查看当前进程ID,等同于全局变量 $$
Process.pid

# 查看当前进程的父进程,如果我们通过终端运行,父进程一般是 shell 进程
Process.ppid

# 查看当前的子进程
Process.waitall

Process.clock_gettime(Process::CLOCK_REALTIME)  # 真实时间
Process.clock_gettime(Process::CLOCK_MONOTONIC)  # 系统启动后的流逝时间;
Process.times

会返回一个 Process::Tms 实例,包含如下四个信息:

  1. stime: 系统CPU时间(System CPU Time)
  2. utime: 用户CPU时间(User CPU Time)
  3. cstime: Children stime
  4. cutime: Children utime

这里我不是很明白的是,此时进程还没有fork,没有子进程的情况下,Children CPU Time 是什么意思?

Benchmark 实践

在了解了这几个概念之后,我们就可以读懂 Benchmark 返回的结果中的时间概念了。

Benchmark.measure { 'x' * 10_0000_0000 }

会返回一个 Benchmark::Tms 实例,与 Process::Tms 一致,包含如下四个信息:

  1. stime
  2. utime
  3. cstime
  4. cutime
  5. total: total 等于 stime, utime, cstime, cutime 四者之和。

benchmark 方法会生成 Benchmark::Report 实例在代码块中执行。 bm 是 benchmark 方法的简单版本,使用了 Benchmark::Tms 中的 CAPTION 和 FORMAT 默认配置。

n = 2_000_0000
Benchmark.bm do |x|
  x.report { n.times { 'x' } }
  x.report { n.times { "x" } }
end

# =>
#       user     system      total        real
#   1.786338   0.037146   3.515864 (  1.833894)
#   1.850868   0.038122   3.200687 (  1.907266)

上面这个例子对比了单引号'和双引号"的性能,我们应该主要关注 User CPU Time 和 Real Time, 这两个值更能体现真实的代码运行时间。 可以看出单引号的性能是优于双引号的。

Ruby & Rails代码风格

参考:社区版本Ruby Style

主要是做一些团队内的补充;

Ruby

def send_mail(source)
  Mailer.deliver(
    to: 'bob@example.com',
    from: 'us@example.com',
    subject: 'Important message',
    body: source.text
  )
end
  1. 调整被call的方法名更灵活,假设我们需要将 deliver 方法更名,不需要再调整排版。
  2. 调整参数更灵活,不必要因为增加或者减少参数,重新排版;

Rails

开发原则

Work Design 系列项目作为模块化代码的实践,旨在开箱即用,易于使用。不过业务需求复杂多变,如何能做到以“不变应万变”,需要在开发的过程中分清楚界限:哪些功能是属于模块,是通用的,而哪些功能是属于主项目,是需要在主项目中override的。

为此,我们在开发工作中提炼了一些开发原则,用于指导我们的工作。

降低学习和使用成本

经过数年的演进,软件开发的门槛越来越低。程序语言,框架,库的繁荣使得开发工作越来越简单。在设计开发库的时候,尽可能降低使用者的学习和使用成本,不仅能使得最终软件成本降低,同时降低了使用者门槛,拓大了使用者群体。

在开发中,有哪些原则能够降低使用者成本呢。

一致性

一致性是减少使用者的理解和使用成本的重要原则,系列engine,主要统一的地方:

Stare frame decisis遵循框架先例原则

遵循先例原则是一个法律中的基本原则,即基于现有的认知,确定解决方案。在代码库开发中,尽量遵循语言和框架的风格,遵循现有方法,基于用户已经熟悉和掌握的知识。

尽可能”Ruby Style”, 避免”DSL”,DSL(领域专属语言)的潜台词是小圈子,不一定通用的规则。所以我们在使用Ruby和Rails框架的前提下,尽可能遵循ruby和rails风格的方式,会减少学习和使用成本。

  1. 对于提供的通用模块(一组方法定义),我们尽可能采用Ruby中的 include(prepend)/extend 方式去引用。如果类方法的定义中仅包含对模块的引用,则避免定义类方法,因为类方法会隐藏细节。

  class User < ApplicationRecord
    acts_as_auth # bad
    include TheAuthUser # better
  end

Easy override,便于覆写

没有万能药,一个框架和库不可能满足软件世界的所有需求。如果一个库总想着尽可能去兼容需求,则其复杂度会大幅提升,从而增加用户的使用成本。

所以在设计库的时候,需尽可能为被覆写提供渠道和便利。

作为库的使用者,则尽可能避免直接改动原框架,尤其是对于活跃开发中的库,要相信,origin库(框架)是总能被override 的,只要你对框架和语言足够熟悉。

  1. Rails Engines里的代码可以从各个层面去override;  * model 层: 无论是include进来的module中的方法,还是重新打开类,定义同名方法即可,区别是include module引入的方法可以通过super去调用,而打开类只是被覆盖;  * view 层: 以同样的路径覆盖位于engine中的文件即可。  * controller/routes 层:在routes定义同路径路由覆盖;

最佳实践,避免灵活性带来的额外成本

业务中的需求千变万化,如果想着满足各种需求,会额外增加很多判断。

同时如果一个库很灵活,需要的配置文件也会额外增加。

充分利用生态圈所拥有的能力

Rails作为一个庞大的应用,包含了对于构建系统来说大量实用的功能。在开发的时候,尽量避免重复造轮子。这也是对程序员的基本功的要求。

也许有些需求不足以满足,但是往往可以基于Rails框架进行扩展。

克制

Ruby 是一门非常灵活的语言,提供了诸如打开类(Open Class)等黑魔法。这些元编程往往具有一些副作用。 所以我们应尽量避免直接改原框架,

而是基于(框架)使用回调、钩子等方案。

一处配置,多处使用

很多需要配置的地方,尽量考虑到配置内容可能影响的各个环节。

函数式编程风格的 Helper 模块

函数式

Nginx 配置示例

SSH 相关设置

通过ssh连接至服务器

设置ssh代理连接, 即让服务器可以通过ssh连接以你的ssh配置使用git等服务

Host 123.123.123.123 114.114.114.114
  ForwardAgent yes

其中, Host是你允许使用你本机ssh连接的服务器,如果有多个,用空格隔开

echo $SSH_AUTH_SOCK
ssh-add ~/.ssh/id_rsa # 临时, 在mac中, 重启之后就失效了
ssh-add -K ~/.ssh/id_rsa # 永久, for mac
ssh-add -l # 检测是否添加成功

远程服务器, test

ssh -T git@github.com

参考资料

SSH agent forward

== 核心子类Override一览

引子

Class/Module Override == Override eql? (Same as ==) Override === (Same as ==)
Array √(×) ×
Hash √(√) ×
Range √(×) √(include?
Comparable × ×
Exception × ×
Numeric × x
Complex × ×
Float √(×) √(√)
Integer × √(√)
Rational × ×
MatchData √(√) ×
Method √(√) √(call
Proc x x √(call
Module × × ×
Random × ×
Regexp √(√) √(×)
String √(×) √(√)
Struct √(×) ×
Symbol x x x
FalseClass x x
TrueClass x x
NilClass x x

Linux 常用查看命令

Linux常用命令

df
df -h  #人性化的显示容量

du
du -d 1 -h  # 查看当前目录占用情况
lsof -i:xxx

解压

tar -xzf xxx.tar

###

Nginx 配置示例

puma应用

upstream myapp {
  server tcp://0.0.0.0:3001;
}

server {
  listen 80;
  server_name www.one.work;
  
  location /cable {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
  
  location / {
    root /opt/srv/xxxx/current/public;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://myapp;
  }
}

请求转发

server {
  listen 80;
  server_name wechat.one.work;
  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_redirect off;
    proxy_pass http://xx.xx.xxx.xx:20000;  # 外网IP
  }
}

websocket

  location /cable {
    proxy_pass  http://localhost:3001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }

日志分割

/opt/nginx/logs/*.log {  
  daily         # 按日阶段   
  minsize 1M    # 文件容量超過1m才進行,忽略時間參數     
  missingok     # 如果日志不存在则忽略该警告信息 
  rotate 7      # 保留7天 
  compress      # 压缩  
  delaycompress # 不压缩前一个截断的文件(需要与compress一起用)
  notifempty    # 增加日期作为后缀,不然会是一串无意义的数字  
  copytruncate  # 清空原有文件,而不是创建一个新文件
} 

搭建ipsec服务

生成ca证书

签名服务器证书

生成客户端证书

ipsec 配置文件

config setup
    uniqueids=never
conn %default
    keyexchange=ike
    left=%any
    leftsubnet=0.0.0.0/0
    right=%any
conn IKE-BASE
    leftcert=server.cert.pem
    rightsourceip=10.0.0.0/24
# ios etc.
conn by_cert
    also=IKE-BASE
    keyexchange=ikev1
    fragmentation=yes
    leftauth=pubkey
    leftsubnet=0.0.0.0/0
    rightauth=pubkey
    rightauth2=xauth
    rightcert=client.cert.pem
    auto=add
# ios etc.
conn by_psk
    also=IKE-BASE
    keyexchange=ikev1
    leftauth=psk
    rightauth=psk
    rightauth2=xauth
    auto=add
# osx linux android etc.
conn by_key
    also=IKE-BASE
    keyexchange=ikev2
    leftauth=pubkey
    rightauth=pubkey
    rightcert=client.cert.pem
    auto=add
# ikev2 (ios osx win7 etc.)
conn IKEv2-EAP
    also=IKE-BASE
    keyexchange=ikev2
    ike=aes256-sha256-modp1024,3des-sha1-modp1024,aes256-sha1-modp1024!
    esp=aes256-sha256,3des-sha1,aes256-sha1!
    rekey=no
    leftid=52.193.249.79
    leftauth=pubkey
    leftsendcert=always
    rightfirewall=yes
    rightsendcert=never
    rightauth=eap-mschapv2
    eap_identity=%any
    dpdaction=clear
    fragmentation=yes
    auto=add

理理File/Dir/Pathname(一)

引子

最近在写一个涉及到文件操作的小工具,在ruby中有很多方法可以获取文件,对文件进行操作,今天就对这些操作进行一个整理。

方法名 描述 返回结果类型
__FILE__ __FILE__是一个关键字,返回当前文件的绝对路径,包含文件名本身 String
File.dirname(__FILE__) 获取当前文件目录 String
Pathname.new(__FILE__) 获取当前文件目录 Pathname
Dir.pwd 返回当前工作目录,别名方法有:Dir.getwd String
`pwd` 返回当前工作目录 String
system(‘pwd’) 打印当前工作目录,返回值为ture TrueClass

说明:

关于Pathname

ruby提供了一个标准库,pathname

调用系统命令

有两种方法可以调用系统命令,一种是使用两个“`”符号将系统命令包起来,一种是使用“system”方法。这两种方法的返回值有所区别。示例如下:

`pwd`
# => "/Users/qin/project/blog\n"

system 'pwd'
# 这里会答应出结果
# => true

“`”包装的命令会返回字符串,system会将系统命令返回的字符串打印出来,并返回true/false/nil中的一个值。

当前工作目录,与当前文件目录

这是两个容易混淆的概念,一个是脚本所在的目录,一个是你在哪个目录下调用的脚本,Dir.pwd返回的就是哪个目录。

TracePoint介绍

引子

最近发布了一次app升级,发现日志里总是报一个警告:Digest::Digest is deprecated; use Digest

已经确定这个错误不是从app里报出来的,而是来自于某个依赖的gem。

而我们引用的gem有100多个,要找到这个警告来自于哪个gem还真是很伤神。

在网上找解决方案的时候发现了一个方法:set_trace_func。在新版本的ruby(2.1.5以上)中,这个方法已经废弃了,ruby提供了一个更好的的替代:核心类TracePoint

TracePoint介绍

TracePoint可以用来收集特定的事件在被调用时的信息。

这些事件包括:

针对这些事件,支持的信息有:

当然并不是所有的事件都支持这些信息,如果调用的事件不支持某个事件的时候,将会抛出一个RuntimeError

使用实战

ruby中的return

在ruby中,有三个关键字可以从一段代码中返回,分别是returnnextbreak,今天主要研究一下return。

方法中的return

在ruby中调用一个方法,默认的情况下,会一行一行依次执行方法中的代码,方法的值是最后一行的值。

return可以改变这个方法一行一行执行的行为,当遇到return,方法会直接返回(rescue、ensure例外)。

def test
  puts 'first puts'
  return
  puts 'next puts'
end

在上面这个例子中,puts 'next puts'这行代码永远不会执行。同时我们可以显示指定返回值,默认为nil。例如上面这个例子没有指定返回值,其返回值则为nil。

proc中的return

代码胜千言,先看几个例子:

def test1
  proc = Proc.new { return 10 }
  puts 'puts here'
end

def test2
  proc = Proc.new { return 10 }
  proc.call
  puts 'puts here'
end

def test3
  return 10
  puts 'puts here'
end

test4 = Proc.new { return 10 }
test4.call

在上面的例子中:

为什么会这样,其实test2test3已经告诉我们答案了,在proc中使用 return 和在方法本身中使用return的效果是一样的。

test4的例子就相当于你直接执行return 10,报的错误也同样是:LocalJumpError: unexpected return

LocalJumpError这个错误其实很有意思,如果拦截这个错误,是依然可以拿到return的返回值的。 下面是我在pry中做的实验。

return 10
#=> LocalJumpError: unexpected return

_ex_
#=> <LocalJumpError: unexpected return>

_ex_.class
#=> LocalJumpError < StandardError

_ex_.exit_value
#=> 10

_ex_.reason
#=> :return

lambda 中的return

还是继续看例子:

test1 = -> { puts 'first puts'; return 10; puts 'next puts' }
test1.call

# first puts
#=> 10

def test2
  la = -> { puts 'first puts'; return 10; puts 'next puts' }
  la.call
  puts 'puts here'
end

# first puts
# puts here
#=> nil

lambda对象,跟proc对象不一样,在lambda中,return是从lambda代码块中返回。在proc中,return是从proc代码块所在的上下文环境中返回。

感悟互联网(1)

我们做企业,要挣钱,无非是做两个事情:一是做产品(服务),二是做销售。 互联网对传统行业的影响,最大的影响也是对销售的影响。    销售有两个环节,一个环节是传递信息,一个环节是客户关系维护。 互联网对这两个环节的影响,最大的影响是对传递信息的影响。

拥抱互联网之一——媒体与销售

媒体是人们获取信息的方式。 媒体影响的是企业做“广告”的方式。

媒体的发展特点:

1、一对一到一对多: 最简单的媒体是一对一的,

2、单媒体到多媒体:

3、信息越来越符合需求:

4、从单向到互动、可选择性:

信息传递渠道的变化,是信息传播媒介的变化带来的。 新媒体拥有更高的效率和用户体验,但是老媒体也没有消亡。

用户在哪里,用户通过什么方式获取信息,我们就通过什么形式向他传递信息。 传统媒体:传统广告(传单、户外、电视广告)。

  传统媒体是单向的,渠道担当的角色是一个“传话者”,商家和用户难以直接对话。用户可以选择信息:而互联网渠道则更像是平台,产品提供商可以和用户很方便的直接对话,正因为此,互联网渠道“更短”,效率更高。   广告本身也是人们获取信息的一种需求,而只有互联网的用户体验有机会让用户从被动获取信息到他主动获取他所需要的信息的转变。   从信息获取的主动性和被动性来讲,完全可以把广告作为信息让用户主动获取。绝大部分人在购买手机等产品时,会通过主动搜索该产品信息的形式对其进行深入了解。而百科性质的词条权重很高,同时也是人们参考产品信息的主要方式。   用户主动获取到满足需求的信息才能提升用户在网站的使用体验(更愉悦),和传统媒体相比,互联网的智能交互具有更高的用户体验。 个性化传递信息、广告的精准化; 广告是最简单,但也是转化率最低的盈利方式。广告本身也是信息的传播,但是只有当面对正确的人群的时候,它才是有效信息,否则就是令人反感的广告。 从一对一,到一对多 与销售员一对一的讲解相比,他必须不断地重复这个劳动。而WEB程序则可以同时针对不同的对象精准化提供更符合其要求的信息,这便是计算机的自动化对生产力的变革。 则让信息传播的成本更低,效率更高。 远程 在电话和互联网出现之前,渠道比较简单,能够影响的范围较小:无非就是集市和挑担子叫卖。

互联网的信息获取方式: 用户主动获取信息,用户筛选信息:   百度是有目标的去寻找信息,而长期的变态SEO使得搜索引擎越来越难以提供最符合用户需要的信息。 用户被动获取信息,向用户推荐信息:   社交化网站是发现信息,但不同用户采取不同标准输出的信息似乎让信息获取者更加痛苦。   有没有一种信息平台:既可以让用户带着目标去寻找信息,又可以让用户在这里发现到他想看到的信息;既能够让用户有选择的余地,又不至于把用户淹没在信息汪洋中。   我认为一个更加符合用户需求的解决方案就是垂直百科(行业性百科网站)。

策略:老媒体不放弃,新媒体加把力

渠道之变:

渠道是指:产品从生产者到达顾客手中的过程。 传统的渠道主要有:代理商、中介、展销会。 渠道伴随着信息传播的变化而变化,互联网改变了销售渠道的形态,在电子商务时代,B2C直接使商家面向客户。 阿里巴巴短短几年便打破了温州小商品市场的神话,电子商务正在大肆瓜分着传统渠道的市场,并淘汰了音响店、书店、电脑城等行业。在服务行业,O2O的出现也在加速改变着线下市场的形态,可见传统行业与互联网的结合是销售发展的必然趋势。

上门面谈、电话营销,

终端之变:

终端实际就是用户获取服务或产品的方式,传统的终端:上门、店面、展销会中的展位; 网站是伴随着互联网新出现的终端,你通过百度搜索到了一个卖某产品的买家,然后你去买了,想想这个过程实际上向你提供这个需要消费的产品,百度是你最先接触的平台。 也就是说,帮助别人卖东西,最关键的是做离用户最近的平台。拥有尽可能多与消费者接触的机会,也就拥有更多的话语权。 互联网大佬的战争,基本都是控制用户入口的战争。无论是搜索引擎、还是客户端,都是一场争夺“互联网入口”的战争。在这场战争中,产品形态都不断的发掘用户的需要,并且调整着产品形态。

互联网战略——先有用户,后实现销售   无论是免费还是收费,在实现向用户传播信息之前,首先得有用户群。在有了用户群之后,要赚钱,办法只有一个,就是向用户提供收费的产品或服务。   所提供的收费产品,可以也自己的产品,也可以是别人的产品。

如何获得用户群:   用户选择用脚投票,谁能更好地满足用户需求。满足用户需求的关键是用户体验。

跳过搜索引擎:行业性解决方案的趋势:   我认为最关键的原因便是,行业性的网站可以针对行业的信息特点提供更好的用户体验,这也是垂直搜索引擎的优势,但是仅搜索难以带给用户发现信息的体验,   以电子商务为例,这类网站来自搜索引擎的流量已经很小了,行业巨头可以很洒脱地屏蔽百度。08年的时候,除了医药、教育、机械,旅游还是百度的重要收入来源,而三年多过去了,快速发展的携程、酷讯、去哪儿吞噬了百度的旅游收入。 我在两年的工作中,研究了医学信息和产品信息的最佳传达策略,发现比垂直搜索更符合用户需要的产品信息形态便是:模块化的微百科内容组成的产品信息。   

如何动态改变某个class的祖先链

在rails中,一个Controller的祖先链中定义了数个名为process_action的方法。

分别位于如下module中:

在不同的模块中,方法process_action分别承载了一个功能,然后交处理通过super交给父类中的process_action继续处理。摘取其中一个方法的源码,如下:

# File actionpack/lib/action_controller/metal/params_wrapper.rb, line 232
def process_action(*args)
  if _wrapper_enabled?
    if request.parameters[_wrapper_key].present?
      wrapped_hash = _extract_parameters(request.parameters)
    else
      wrapped_hash = _wrap_parameters request.request_parameters
    end

    wrapped_keys = request.request_parameters.keys
    wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)

    # This will make the wrapped hash accessible from controller and view
    request.parameters.merge! wrapped_hash
    request.request_parameters.merge! wrapped_hash

    # This will display the wrapped hash in the log file
    request.filtered_parameters.merge! wrapped_filtered_hash
  end
  super
end

这是一个很有意思的利用ruby高级特性的设计。顺便提一下,我也是通过这个设计明白了,在ruby中为什么方法名相同,参数数目不相同并不被视作不同方法的一个好处了(否则这个黑魔法就不够好玩了)。

这个行为很像rack中中间件设计。我现在的需求是插入一个module到Controller的祖先链中,但我同样希望能够控制这个module的位置。

经过几天的思考,找到了解决办法,简单且巧妙。

ActionController::HttpAuthentication::Token::ControllerMethods.include Light::Instrumentation
ActionController::Base.include ActionController::HttpAuthentication::Token::ControllerMethods

即:首先在你想插入的位置所在的module中include你所自定义的module,然后在重新include一下所在位置的那个module。

下一个问题:Controller祖先链中的大部分module都extend了ActiveSupport::Concern,这个将当前模块的祖先链扁平化了?所以include的自定义模块并不能出现在祖先链中。如上例中,我好不容易才找了一个没有extend concern 模块的module。

ObjectSpace介绍

最近在调试bug的时候,遇到一个诡异的错误,错误信息中如下:

#<NoMethodError: undefined method `to_sym' for #<Object:0x007ff53a0e1578>>

接下来我就懵了,这个#<Object:0x007ff53a0e1578>对象到底是什么,我该如何知道它的本来面目呢。于是我想到了ObjectSpace

ObjectSpace是ruby的一个核心module,它本来是为ruby的垃圾回收工作提供服务的一个模块,同时提供了方法让我们可以跟踪到ruby中还存活对象。

每个对象都有一个唯一的object_id,object_id与内存地址存在一定的映射关系,上例中0x007ff53a0e1578就是这个对象内存地址的一个映射,其object_id0x007ff53a0e1578 / 2 = 70345608858300。 有了object_id,ObjectSpace就能帮我们还原这个对象的真实面目了。

ObjectSpace._id2ref(70345608858300)

# 返回结果如下
{
               "RBENV_VERSION" => "2.1.5",
                "TERM_PROGRAM" => "iTerm.app",
                        "TERM" => "xterm-256color",
                       "SHELL" => "/bin/zsh",
                      "TMPDIR" => "/var/folders/qy/x2mgpc052csdx4vvyp2b7wsm0000gn/T/",
  "Apple_PubSub_Socket_Render" => "/private/tmp/com.apple.launchd.XcJLqAwJLx/Render",
                         "ZSH" => "/Users/qin/.oh-my-zsh",
                         # …
}

原来这个Object对象就是ENV,这下问题就好查起了。

除了_id2ref这个神奇的方法,ObjectSpace还提供了一些很有意思的方法。

{
       :TOTAL => 419013,
        :FREE => 329,
    :T_OBJECT => 14056,
     :T_CLASS => 6063,
    :T_MODULE => 922,
     :T_FLOAT => 9,
    :T_STRING => 250253,
    :T_REGEXP => 2352,
     :T_ARRAY => 61635,
      :T_HASH => 10083,
    :T_STRUCT => 481,
    :T_BIGNUM => 13,
      :T_FILE => 29,
      :T_DATA => 40450,
     :T_MATCH => 134,
   :T_COMPLEX => 1,
  :T_RATIONAL => 891,
      :T_NODE => 29990,
    :T_ICLASS => 1322
}

基本类型的统计信息一应俱全;

更多方法请参考文档。

Rails日志实现探索(3)

订阅者(接外包)的实现

ActiveSupport::Subscriber 负责干活的订阅者

def start(name, id, payload)
  e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload)
  parent = event_stack.last
  parent << e if parent

  event_stack.push e
end

def finish(name, id, payload)
  finished  = Time.now
  event     = event_stack.pop
  event.end = finished
  event.payload.merge!(payload)

  method = name.split('.').first
  send(method, event)
end

ActiveSupport::LogSubscriber < ActiveSupport::Subscriber

def start(name, id, payload)
  super if logger
end

def finish(name, id, payload)
  super if logger
rescue Exception => e
  logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
end

这里的 logger 实际是 Rails.logger

##

Rails日志实现探索(2)

日志功能实现

Rails中对日志的处理采用的是“消息-订阅”机制,各部分组件和功能如下:

日志消息的发送:ActiveSupport::Notifications

扩展:可在通知前先执行一个代码块;

ActiveSupport::Notifications.instrument('render', extra: :information) do
  render text: 'Foo'
end

工作流程

安排下发通知的人

# 1、每个事件分配一个执行人员(员工)
ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new

# 2、不能瞎指派员工,指派的员工须得有这个能力
if notifier.listening?(name)
  instrumenter.instrument(name, payload) { yield payload if block_given? }
end

# 3、通过员工的上级领导(cto)分配工作
def instrumenter
  InstrumentationRegistry.instance.instrumenter_for(notifier)
end

具体工作

1、Instrumenter安排工作

# Send a start notification with +name+ and +payload+.
def start(name, payload)
  @notifier.start name, @id, payload
end

# Send a finish notification with +name+ and +payload+.
def finish(name, payload)
  @notifier.finish name, @id, payload
end

2、Fanout 接受任务开始干活

def start(name, id, payload)
  listeners_for(name).each { |s| s.start(name, id, payload) }
end

def finish(name, id, payload)
  listeners_for(name).each { |s| s.finish(name, id, payload) }
end

def publish(name, *args)
  listeners_for(name).each { |s| s.publish(name, *args) }

  # s 为`ActiveSupport::Notifications::Fanout::Subscribers::Evented`实例
end

干活的人多了个publish方法,反馈任务(员工需要汇报工作)

3、ActiveSupport::Notifications::Fanout::Subscribers::Evented 具体的任务

def publish(name, *args)
  if @can_publish
    @delegate.publish name, *args
  end
end

def start(name, id, payload)
  @delegate.start name, id, payload
end

def finish(name, id, payload)
  @delegate.finish name, id, payload
end

delegate 这个又是谁呢?

答案揭晓

其实这个员工他也不干活,他将工作外包出去了!!!

Rails日志实现探索(1)

概述

我一直很好奇在Rails中,日志是如何记录一个Rails App运行的信息的。我查看了一下Rails的中间件,发现了Rails::Rack::Logger

# rake middleware
...
use Rack::MethodOverride
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
...

查看Rails::Rack::Logger中实现记录日志的主要代码,发现了两个奇怪的类:ActiveSupport::NotificationsActiveSupport::LogSubscriber

def call_app(request, env)
  # ...
  instrumenter = ActiveSupport::Notifications.instrumenter
  instrumenter.start 'request.action_dispatch', request: request
  logger.info { started_request_message(request) }
  resp = @app.call(env)
  resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
  resp
rescue Exception
  finish(request)
  raise
ensure
  ActiveSupport::LogSubscriber.flush_all!
end

日志功能实现

Rails中对日志的处理采用的是“消息-订阅”机制,各部分组件和功能如下:

Rails中的request

每个web请求都有对应的 Request和Response,在Rails中,我们如何获取关于request的信息呢?

Request对象

在controller的实例中,可以通过request方法获取request对象

request
# => ActionDispatch::Request对象

Request对象包含的内容

Headers

request.headers
# => #<ActionDispatch::Http::Headers:0x007ff849c55a50>

headers分为: request.headers response.headers

Params

request.params # => ActiveSupport::HashWithIndifferentAccess对象
request.parameters # 同request.params
request.path_parameters

params 返回的是 ActionController::Parameters 对象

Method

request.method # => "GET",String对象
request.method_symbol  # => :get,Symbol对象
request.request_method_symbol

request.get?
request.post?
request.put?
request.patch?
request.delete?
request.head?

Url

request.fullpath
request.original_fullpath
request.original_url

请求内容

request.body  # => StringIO对象
request.body_stream

request.raw_post

request.form_data?

request.xml_http_request?
request.authorization

MIME等信息

request.media_type
request.accepts
request.content_type
request.formats
request.format
request.variant

客户端、服务端、中间件信息

request.ip
request.remote_ip
request.local?

request.uuid

request.server_software

缓存相关信息

request.if_modified_since
request.if_none_match
request.if_none_match_etags

rescue exception in ruby

Exception是ruby中所以异常会继承的父类,当我们在ruby中rescue异常类的时候,如果没有指定具体的异常类。 rescue拦截的实际是所有的标准类。

begin
  #...
rescue  # 没有指定异常类,拦截异常类默认为 `StandardError`
  #...
end

可以为异常类指定一个变量

begin
  #...
rescue => e
  #...
end

以上用法的完整版本为:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end

以下用法会拦截所有的错误,通常情况下是不建议的。主要应用场景为日志相关。

begin
# iceberg?
rescue Exception => e
# do some logging
end

如何移除某次提交之前的版本历史

step-1:查看相关提交的包含队形

命令: git log --pretty=raw 查看详细的log信息

commit 517e681c92d0055cc14147a47f819bdab7e7b853
tree bc124180421117c94fb8498f29e28024a34b4eb4
parent e4b4b22b08c3ee4218416c3d89e05d3ea410d291
parent 926368c6cbb49c11d2f132acca12404a47a74e42
author 覃明圆 <qinmingyuan@boohee.com> 1418984392 +0800
committer 覃明圆 <qinmingyuan@boohee.com> 1418984392 +0800

Merge branch 'hotfix/out_range_birthday' into 'master'
Hotfix/Out Range Birthday

commit 926368c6cbb49c11d2f132acca12404a47a74e42
tree bc124180421117c94fb8498f29e28024a34b4eb4
parent e4b4b22b08c3ee4218416c3d89e05d3ea410d291
author qinmingyuan <mingyuan0715@foxmail.com> 1418983765 +0800
committer qinmingyuan <mingyuan0715@foxmail.com> 1418983765 +0800

燃脂运动心率默认值

step-2: 基于tree 对象生成一个新的commit 对象

命令:git commit-tree <tree> -m <message> 基于tree对象创建 commit对象 这个命令的返回值是一个commit 对象

5d002707dc6200c3156a19a90e55f332b23b664b

git cat-file -p 5d002707dc6200c3156a19a90e55f332b23b664b

# 返回值
tree bc124180421117c94fb8498f29e28024a34b4eb4
author qinmingyuan <mingyuan0715@foxmail.com> 1419175304 +0800
committer qinmingyuan <mingyuan0715@foxmail.com> 1419175304 +0800

new commit

step-3:修改其父提交

git cherry-pick <commit> git replace <old commit> <new commit>

设计模式之观察者模式

举这么个例子,假定你娶了个贤惠的小媳妇,小媳妇最喜欢的事情就是“你发工资了”,银行为你的工资卡提供了一个“余额变化提醒”的功能,于是小媳妇把她的手机号设成了通知手机,你发工资了这个事件小媳妇就会第一时间知道。

在这个过程中:

观察者一般可以不止一个,假定你还有个小情人(女儿),小情人最喜欢的事情也是你“发工资了”,小情人也可以成为你的观察者。

观察者(Observer)模式又名发布-订阅(Publish/Subscribe)模式。定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知。

下面我们利用ruby标准库中自带的observer库来实现一个观察者模式的范例。

require 'observer'

# 被观察者
class ObservableTarget
  include Observable
end

# 观察者-小媳妇
class ObserverWife

  # update方法是回调方法,notify_observers时调用
  def update(arg)
    puts "通知小媳妇" + arg
  end

end

# 观察者-小情人
class ObserverDaughter

  def update(arg)
    puts "通知小情人" + arg
  end

end

调用代码

# 被观察者初始化
target = ObservableTarget.new

# 观察者初始化
wife = ObserverWife.new
daughter = ObserverDaughter.new

# 添加监视对象 小媳妇
target.add_observer(wife)

# 被观察者改变了
target.changed

# 通知观察者
target.notify_observers("发工资了")
# => 通知小媳妇发工资了

# 添加监视对象 小情人
target.add_observer(daughter)

# 被观察者改变了
target.changed

# 通知观察者
target.notify_observers("发工资了")
# => 通知小媳妇发工资了
# => 通知小情人发工资了

Git 不常用的好用的命令

git add

git remote

git checkout

git log

git cat-file

git commit-tree

git cherry-pick

git replace

用远程分支覆盖本地

git reset --hard origin/master

git submodule

git tag

移除上一个提交

Ruby中的模块-require

require 是 ruby 中分文件组织代码的关键点,require 是位于 Kernel 模块中的一个方法。其方法参数接受一个文件名,如果文件名不是绝对路径,则会按照 $LOAD_PATH 中的路径依次查找,只到找到为止。如果没有找到的文件,则会报一个 LoadError 错误。

下面我们来做个试验:

require 'a'

LoadError: cannot load such file -- a
from /Users/qin/.rbenv/versions/2.1.5/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'

可是这个报错信息是怎么回事?

require并没有调用系统默认的Kernel#require,而是调用了位于rubygemscore/ext/kernel_require.rb中的Kernel#require函数。RubyGem用自己的require替代了系统默认的版本,并藉此实现了它自己的逻辑。

gem中的require

那gem中的require到底都做了些什么呢?当我们调用require 'a'的时候,如果能够在 $LOAD_PATH 中找到这个文件,那require的任务就完成了。如果没有找到,require就会调用gem 'a',如果能够找到a这个gem,则将其加入$LOAD_PATH 中。

gem中的require源码中关于调用gem的部分:

def require path
  RUBYGEMS_ACTIVATION_MONITOR.enter

  path = path.to_path if path.respond_to? :to_path

  spec = Gem.find_unresolved_default_spec(path)
  if spec
    Gem.remove_unresolved_default_spec(spec)
    gem(spec.name)  # 在这里调用了gem这个方法
  end

  # ……后续代码省略
end

gem中的require源码中,当gem方法把当前gem的路径加入到$LOAD_PATH后,继续调用原始require方法的部分:

def require path
  # ……省略代码
  return gem_original_require(path)  # 本方法中有多处这样的调用,gem_original_require 是gem定义的原生的 require 方法的别名方法
  # ……省略代码
end

gem中的gem方法

代码胜千言。我们在调用gem方法之前看下$LOAD_PATH 都有啥:

# 进入irb
puts $:

# 返回结果如下
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/site_ruby/2.1.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/site_ruby/2.1.0/x86_64-darwin14.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/site_ruby
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/vendor_ruby/2.1.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/vendor_ruby/2.1.0/x86_64-darwin14.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/vendor_ruby
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/2.1.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/2.1.0/x86_64-darwin14.0

接下来我们调用一下 gem ‘rails’

gem 'rails'
# => true

然后我们再看看$LOAD_PATH 发生了什么变化

/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/i18n-0.7.0.beta1/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/thread_safe-0.3.4/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/tzinfo-1.2.2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activesupport-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/rack-1.6.0.beta/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/rack-test-0.6.2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/mini_portile-0.6.0/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/extensions/x86_64-darwin-14/2.1.0-static/nokogiri-1.6.3.1
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/nokogiri-1.6.3.1/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/loofah-2.0.1/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/rails-html-sanitizer-1.0.1/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/rails-deprecated_sanitizer-1.0.3/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/rails-dom-testing-1.0.3/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/builder-3.2.2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/erubis-2.7.0/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/actionview-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/actionpack-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activemodel-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/arel-6.0.0.beta1/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activerecord-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/globalid-0.3.0/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/activejob-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/actionmailer-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/thor-0.19.1/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/railties-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/bundler-1.7.7/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/sprockets-rails-3.0.0.beta1/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/gems/2.1.0/gems/rails-4.2.0.beta2/lib
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/site_ruby/2.1.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/site_ruby/2.1.0/x86_64-darwin14.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/site_ruby
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/vendor_ruby/2.1.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/vendor_ruby/2.1.0/x86_64-darwin14.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/vendor_ruby
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/2.1.0
/Users/qin/.rbenv/versions/2.1.5/lib/ruby/2.1.0/x86_64-darwin14.0

多了好多内容,不仅把rails本身加入到了$LOAD_PATH中,还加入了rails依赖的其他gem,这下就可以愉快的使用 require 了。

Mysql数据库编码

基本概念

字符(Character)

指人类语言中最小的表义符号。 例如‘a’、‘b’,‘好’等;

编码(Encoding)

给定一系列字符,对每个字符赋予一个数值,用数值来代表对应的字符。 例如,我们给字符‘A’赋予数值0,给字符‘B’赋予数值1,则0就是字符‘A’的编码。

字符集(Character Set)

给定一系列字符并赋予对应的编码后,所有这些字符和编码对组成的集合。 例如,给定字符列表为{’A’,’B’}时,{’A’=>0, ‘B’=>1}就是一个字符集; 字符序(Collation)是指在同一字符集内字符之间的比较规则; 确定字符序后,才能在一个字符集上定义什么是等价的字符,以及字符之间的大小关系; 每个字符序唯一对应一种字符集,但一个字符集可以对应多种字符序,其中有一个是默认字符序(Default Collation); MySQL中的字符序名称遵从命名惯例:以字符序对应的字符集名称开头;以_ci(表示大小写不敏感)、_cs(表示大小写敏感)或_bin(表示按编码值比较)结尾。例如:在字符序“utf8_general_ci”下,字符“a”和“A”是等价的;

MySQL中默认字符集的设置

服务器级(Server)

数据库级(Db)

表级(Table)

字段级

ruby中的编码

信息的编码包含两个层面的含义。 一个层面是信息的表达:比如汉字和拼音是两种不同的表达方法。 另一个层面是信息的解读:比如made这个字符,拼音可能就是骂人的意思,英文则是make的过去分词。 所以编码就是一种协议,以什么协议去编码,就需要以什么协议去解读。

那我们应该选择什么编码协议去编码呢,这个则取决于我们传达信息所采用的工具。 举个例子,我想让一个美国人去给我买个面包,我用中文跟他说,他显然就很难执行我的命令,我得用英语告诉他。

最近的工作中,有一个需求是给客户端传输过去一串html文档,然后客户端解析和渲染这段文档,那我所采用的HTML文档的编码方法就得是客户端能够理解的编码协议。

以下是ruby代码示例:

text = "<p>用户卡路里</p>"
# text.encoding => #<Encoding:UTF-8>

base64_text = Base64.encode64(text)  
# => "PHA+55So5oi35Y2h6Lev6YeMPC9wPg==\n"

decode_text = Base64.decode64(base64_text)
# => "<p>\xE7\x94\xA8\xE6\x88\xB7\xE5\x8D\xA1\xE8\xB7\xAF\xE9\x87\x8C</p>"
# decode_text.encoding => #<Encoding:ASCII-8BIT>
# decode_text.force_encoding('utf-8') => "<p>用户卡路里</p>"

base64_strict_text = Base64.strict_encode64(text)
# => "PHA+55So5oi35Y2h6Lev6YeMPC9wPg=="

Base64.strict_decode64(base64_strict_text)
# => "<p>\xE7\x94\xA8\xE6\x88\xB7\xE5\x8D\xA1\xE8\xB7\xAF\xE9\x87\x8C</p>"

研究ruby的一些小技巧

关于类

class Exam < ActionView::Base
end

关于对象

a = Exam.new

关于方法

def a.a_method
end

Rails中间件

rails 常见中间件介绍

Rack中定义的中间件

Rack::Lock

线程锁, 保证 rails 代码单线程运行, 同时设置 env[‘rack.multithread’] = false, 你可以通过设置 config.allow_concurrency = true 来去掉该中间件

Rack::Runtime

统计运行时间, 放在 response 的 “X-Runtime” header 中

Rack::Sendfile

如果返回数据已经放在一个文件里边了(比如生成的 PDF), 则可以让 nginx 服务器直接从该文件读取,降低系统消耗

Rack::MethodOverride

支持用 POST 来模拟 PUT, DELETE, …, 可以在 POST 使用 _method 参数,也可以使用 HTTP 头 “HTTP_X_HTTP_METHOD_OVERRIDE”

Rails::Rack::Logger

比如 log/development.log 中的这一行 “Started GET “/” for 127.0.0.1 at Wed Sep 15 21:46:51 +0800 2010”

ActionDispatch 中定义的中间件

ActionDispatch::Static

静态文件(即 public/的文件)支持, 一般在生产环境下会禁用此功能

ActionDispatch::ShowExceptions

截获异常,把异常转换为 HTTP 错误号 (一般转为 500, 但一些特殊异常转到相应的错误号,比如 “ActionController::MethodNotAllowed” 会被转为 405, 同时显示对应的错误页面,对应开发环境,会显示异常的 backtrace, 对于生产环境,则会显示 public/500.html, 对于测试环境,该中间件会被禁用,直接把异常抛出

ActionDispatch::RemoteIp

解决服务器转发, 代理导致客户端真实 IP 丢失的问题,用户的真实IP放在 env[“action_dispatch.remote_ip”]

ActionDispatch::Callbacks

测试环境下用于检测源文件是否改变, 产品环境下作用不明 (TODO)

ActionDispatch::Cookies

cookie 支持

ActionDispatch::Session::CookieStore

session 支持,此处使用 cookie store

ActionDispatch::Flash

flash 支持, 参见 http://guides.rubyonrails.org/action_controller_overview.html#the-flash

ActionDispatch::ParamsParser

分析XML, JSON参数,放到 env[“action_dispatch.request.request_parameters”]

ActionDispatch::Head

把 HEAD 请求转为 GET 请求, 同时设置 env[“rack.methodoverride.original_method”] = “HEAD”

ActionDispatch::BestStandardsSupport

设置 HTTP 头: X-UA-Compatible

ProjectName::Application.routes

终于进入你的 rails 程序了, 开始路由, 同时开始使用 rails 的协议栈

ruby对象的序列化

什么是序列化对象

在ruby中,有时候需要把一个对象存储起来,比如存到数据库里。 一个ruby对象可能是各种各样的,可能是数组,也可能是一个自定义对象。

如果直接把一个对象交给数据库,数据库就会抗议:你给我的这个东西我又不认识,我可不敢保证你来取的时候,我就能原样找出来还给你。 这个时候ruby就会想办法:那我给你之前,我把这个对象包装成一个你能认出来的东西,比如字符。

这个“包装”的过程就是序列化对象的过程,当ruby从数据库里取出对象的时候,是不能直接使用的,还需要还原对象,也就是反序列化。

ruby中序列化对象的方法

Marshal

Marshal可以把对象转化成一个字节流。

# 先生成一个Array对象,包含一个Integer对象和Time对象
time = Time.at(1)
value = [1, time]  # => [1, 1970-01-01 08:00:01 +0800]
# 序列化,序列化的对象是一个String实例
serialize_value = Marshal.dump(value)  
# => "\x04\b[\ai\x06Iu:\tTime\r \x80\x11\x80\x00\x00\x10\x00\a:\voffseti\x02\x80p:\tzoneI\"\bCST\x06:\x06ET"

# 反序列化
obj = Marshal.load(serialize_value)
# => [1, 1970-01-01 08:00:01 +0800]

YAML

# 序列化,
serialize_value = YAML.dump(value)  
# => "---\n- 1\n- 1970-01-01 08:00:01.000000000 +08:00\n"

# 反序列化
obj = YAML.load(serialize_value)
# => [1, 1970-01-01 08:00:01 +0800]

JSON

# 序列化
serialize_value = JSON.dump(value)  
# => "[1,\"1970-01-01 08:00:01 +0800\"]"

# 反序列化
obj = JSON.load(serialize_value)
# => [1, "1970-01-01 08:00:01 +0800"]

obj[1].class
# => String

诶,跟想象中的有点不一样,怎么Time对象还原出来变成String对象了。 这就是不同方法序列化之后的还原能力不同了,很明显Marshal在序列化对象的时候记录了很多信息,除了少数特殊情况,Marshal能够还原出绝大部分对象。 所以尽量采用Marshal序列化对象。

ActiveSupport宝藏之MessageVerifier

rails的ActiveSupport组件提供了很多非常有用的小功能,如果善于利用的话,可以使我们在实际项目中减少很多重复造轮子的事情。 今天我们介绍的主角就是:MessageVerifier,MessageVerifier 提供了加密和解密消息的功能,可以有效的防止信息被伪造。 其源码位于active_support/message_verifier.rb文件,下面介绍使用方法及原理:

应用场景

正常情况下,http协议对信息都是进行明文传输,用过抓包工具的同学一定印象很深刻,我们通过form表单提交的密码信息实际是明文传输非常透明的。 当然我们可以使用https对web信息进行加密后传输,不过https会牺牲一些性能和速度,于是一些敏感信息我们便可以单独加密传输。

举个例子,我们经常在cookie中存储 remember me 的相关信息,包括过期时间等内容。这个时候我们就可以对相关内容进行加密。

本周的开发工作中,我也遇到了一个需要加密传输的需求:当我们的用户连续签到达到7天,我们将赋予这个用户解锁某个功能的权限。我们会带着用户token参数去调用一个接口。 用户是很容易知道自己的token的,如果某个用户用自己的token去请求这个接口,不就为自己开通了权限。这个时候我们最好的办法就是对token进行加密,然后收到请求后再解密。

工作原理

加密的过程就是以某个流程将信息转化成另一个样子,解密的过程就是以相对的过程将“另一个样子”的信息还原成本来的信息。所以说这个类中一定提供了两个方法,一个是加密的,一个是解密的。 这两个对应的方法是:

这两个方法是ActiveSupport::MessageVerifier的对象方法。下面我们先来 new 一个对象。 ActiveSupport::MessageVerifier对象会包含下面几个信息:

# new 对象
@verifier = ActiveSupport::MessageVerifier.new('123456')
# => #<ActiveSupport::MessageVerifier:0x007fe522132ba8 @secret="123456", @digest="SHA1", @serializer=Marshal>

# new 对象并指定serializer 为 YAML
@verifier = ActiveSupport::MessageVerifier.new('123456', serializer: YAML)
# => #<ActiveSupport::MessageVerifier:0x007fe522103448 @secret="123456", @digest="SHA1", @serializer=Psych>

要加密的内容可为任何形式的对象,MessageVerifier对象会以指定的对象序列化方法进行序列化。 示例中我们加密的对象是一个包含用户id及过期时间的数组:[user_id, time]

# 加密
cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])

# 解密
id, time = @verifier.verify(cookies[:remember_me])

# 应用:判断是否过期并查找用户
if time < Time.now
  @current_user = User.find(id)
end

Git高级技巧之忽略文件

Git高级技巧之忽略文件

前言

前段时间新参与公司的一个项目,由于服务器 ruby 版本管理用的是 rvm,我在本地用的是 rbenv,这两个 ruby 版本管理软件都用到了.ruby-version作为配置文件,但是语法不一样。 我的需求是在本地改动.ruby-version这个文件,但是让 git 忽略我在本地的改动。 首先我想到的是使用.gitignore配置文件,但是使用.gitignore会将.ruby-version这个文件删除,这显然不是我想要的结果。 那怎么解决这个问题呢?

git 忽略文件的两个类型

git 忽略文件的类型分为:临时忽略,永久忽略两个类型。.gitignore实际是属于永久忽略一个文件。

git 临时忽略文件

命令:git update-index --assume-unchanged <file>

我先在项目下运行 git status,结果是这样的:

On branch master
Your branch is up-to-date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   .ruby-version

no changes added to commit (use "git add" and/or "git commit -a")

然后我运行命令:git update-index --assume-unchanged .ruby-version

改动果然被忽略了:

On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working directory clean

但是我还是有个疑问,我怎么知道我将哪些文件临时忽略了呢?

命令:git ls-files -v

结果如下:

...
H .gitignore
h .ruby-version
H 404.html
...

要知道git ls-files -v这个命令的结果含义,首先需要了解git ls-files -t,这个命令会列出各个文件的状态。

文件前面那个字母标志的含义如下:

git ls-files -v-t的升级版,对应小写字母则表示这个文件是否被临时忽略。

有结就有解,继续跟踪的命令是:git update-index --no-assume-unchanged <file>

永久忽略

永久忽略一般大家都知道,主要就是.gitignore的配置,这个配置分为三个级别:

项目中的.gitignore文件

这个是放在版本库里共享的;

项目中.git目录下地info/exclude文件

这个是本地针对该项目忽略的,比如我 ruby 的项目用的编辑器会生成一个.idea的文件夹,就在这里配置忽略;

全局配置

需要在.gitconfig文件中指定一个全局的配置文件路径,如:

[core]
	excludesfile = /Users/qin/.gitignore

比如 mac 电脑会生成一个.DS_Store的文件,就适合在这里配置,对所有项目生效。

如何写rakefile

rake 是ruby世界里一个非常好用的任务构建工具,其江湖地位极高。

rake是如何工作的

要在项目中运行rake命令,需要首先在项目中包含一个Rakefile文件。不然会报如下错误:

rake aborted!
No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)

(See full trace by running task with --trace)

我们来看看rake是如何运行的,rake命令的源码中主要是调用了Rake::Application对象的一个run方法:

require 'rake'

Rake.application.run

Rake::Application对象的run方法主要做了以下三件事情:

rakefile如何定义rake任务

假设我们写了几个rake任务,目录结构如下:

bootstrap/
├── tasks/
   ├── bootstrap.rake
   └── test.rake

然后在Rakefileimport进来rake文件,代码如下:

#!/usr/bin/env rake

import 'tasks/bootstrap.rake'
import 'tasks/test.rake'

rake的import方法 同 Ruby 的 require 不一样,import 并不是立即进行导入的,而是在整个 Rakefile 执行结束之后才全部导入,因此,可以在任意的地方写 import ,而不用担心依赖关系,需要共享的变量之类的只要在主 Rakefile 中定义了即可。

运行rake --tasks就能看到任务正常加载进来了。

Ruby on Rails 环境及准备

用户

新建用户

# 新建一个用户
useradd -m -s /usr/bin/zsh webuser

# 设置密码
passwd webuser

将用户放到sudoers列表

编辑文件 /etc/sudoers 文件, 增加webuser ALL=(ALL:ALL) ALL

安装依赖及工具

常用工具

数据库

常用lib

Ruby 依赖库

Gems 依赖库

tk图形库

安装rbenv & ruby

安装bundler

gem install -N bundler

基于Rack的项目初始化

1.加载的文件

1.1 config/boot.rb

config/boot.rb 内容如下:

# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

一般我们的项目中都会有一个Gemfile文件,用于声明项目所依赖的gem库。 config/boot.rb中的ENV['BUNDLE_GEMFILE']指定了Gemfile文件的位置,当Gemfile存在,程序将require bundler/setup,然后 Bundler 会将所有gem加入load path。

1.2 config/environment.rb

config.ru一般将require config/environment.rb 这个文件,这个文件的开头require了 config/application.rb require File.expand_path('../application', __FILE__)

1.3 config/application.rb

一般config/application.rb是应用配置的主文件,这个文件的开头require了 config/boot.rb require File.expand_path('../boot', __FILE__)

Rack中间件加载

启动 rackup,rackup会自动加载 config.ru 文件。