保华的Rails学习笔记

如何利用github和git进行协作

牒权声明

本文首发于微信公众号:
杜具一格(baohua-xuexi)
请注意:
无需授权即可转载

如何利用Github进行协作开发(图文讲解详细步骤)
团队协作开发中,大部分都会用到版本控制软件,比如Git等。本文将通过实例,图文详细讲解在工作环境中,如何利用Git+Github进行协作开发,我们分场景讲解

情景1(主程与副程都在master开发)

将团队成员加入Collaborators
主程(A)在Github新建Repo项目,并加成员B为合作者
B把Repo项目clone回去
A修改代码,然后推代码上去commit & git push
B拉代码下来 git pull
B修改代码,然后推代码上去commit&git push
A拉代码下来git pull

1、在repo中,把成员B加入成为副程

2、我们开两个iTerm窗口,左边设为A用户,右边设为B用户,B用户从github上边把jd-test这个repo给clone下来,放入不同的目录里边,二者所在的专案目录不同(为了区分不同用户协作)

3、A用户,我们从截图看出,它当前不是在master分支,我们现在切换到master,把最新分支story5合并入master


4、此时,A用户只是在本地终端把最新代码(在story5分支)合并到master了,但是在github上边以及B用户终端,它们的master分支都不是合并后的最新代码,此时,我们需要让B用户以及github上边的代码与A用户本地终端的代码保持同步,我们在A用户终端执行git push origin master,把master分支的最新代码(合并story5后的代码)push到github

我们进入github,发现commits已增加到38个了,

而且点击commits链接进去后

发现这些代码是之前story5里边的,现在已合并入master分支了

6、这时候B用户需要从github上边拉最新master代码(合并story5后的代码),使用命令git pull origin master


此时,A用户、B用户以及github三者的代码保持一致了

7、这时候,A用户,对app/views/welcome/index.html.erb文件进行了修改(加入了一行内容),然后commit,并push到github的repo

这时候观察SourceTree,我们可以看出,B用户比起github落后了一个版本

或者B用户执行git status(数据稍有延迟),也可以看出来比github落后了一个版本

8、此时B用户把github上边的最新代码拉下来就可以了,执行命令git pull origin maste

这时候A用户、B用户以及github上边repo三者的代码又保持一致了

同样的,当B用户在本地修改代码后,需要commit并且push到github
A用户执行git pull拉最新代码下来后,A、B用户以及github三者代码就保持一致了
1、首先B用户修改并commit和push


这时候我们用SourceTree观察,发现A用户比github上的repo(origin/master)落后了一个版本


或者用git status也可以查出来(数据会有延迟,A用户push上以后,停顿一下再执行git status)

2、这时候,A用户执行git pull origin master就可以把最新代码拉下来了
执行完以后,A、B用户以及github上边的repo(origin/master)三者代码就保持一致了

情景二(出现rejected)

A修改代码,然后推代码上去 commit & git push
B修改代码,然后推代码上去 commit & git push
碰到![rejected]master->master(fetch fitst)的失败信息
B要先拉代码下来git pull进行代码自动合并
B再次git push
A拉代码下来git pull

1、A用户修改README文件


A用户修改后,进行git commit及git push

2、同时,B用户在修改app/views/welcome/index.html.erb文件

B用户进行git commit和git push,在进行git push的时候,出现错误

3、我们可以看到![rejected] master -> master(fetch first)的报错信息,之所以出现这样的rejected报错,原因是github的repo上边有A推的最新代码,B用户并没有拉最新代码下来,就又执行git push,它们接不起来
解决办法是B用户先运行git pull把最新代码拉下来,然后再运行git push推送代码,这时候,A用户需要再进行git pull操作拉最新代码下来
当B进行git pull操作的时候,跳出了一个atom的编辑框(MERGE_MSG)

4、这时我们保存这个文件就可以了

发现B用户已经拉最新代码下来了
5、这时我们用git status或者SourceTree进行检测的时候,发现B用户本地代码比github上边代码已超前了2个版本

B用户需要用git push推代码上去

6、我们再用git status或SourceTree检测下

B用户终端代码和github保持一致了
7、此时用git status或者SourceTree检测A用户代码

发现A落后两个版本(是指本地终端比github落后2个版本),所以A用户要执行git pull,把最新的代码拉下来

这时候,A用户、B用户以及github上边的repo三者的代码保持一致了

场景二总结:两个用户同时都修改代码并且push的时候,后边的用户push的时候,就会收到rejected报错信息,这时候后边的用户就要先执行git pull,然后再重新push,同时前边的用户需要运行git pull拉最新代码下来

情景三(出现rejected)

上边情景二是A和B修改不同的档案,情景三是A和B修改相同的档案(同一档案不同部位)

A修改代码,然后推代码上去commit & git push
B修改代码,然后推代码上去commit & git push
碰到![rejected]master-> master(non-fast forward)的失败信息
B要先拉代码下来git pull进行代码合并
B再次git push
A拉代码下来 git pull

1、A用户修改app/views/welcome/index.html.erb文件,并且进行commit & git push


2、同时呢,B用户也在改这个app/views/welcome/index.html.erb文件(不同部位)
B修改后然后进行commit & git push
当git push的时候出现报错信息

3、用情景2的方法,B先执行git pull拉代码,然后git push

4、此时A要执行git pull拉最新代码下来

其实与情景二是相同的解决方法

情景四(同时修改同一文件的相同部位而出现conflict)

A修改代码,然后推代码上去commit & git push
B修改代码,然后推代码上去commit & git push
碰到![rejected]master-> master(non-fast forward)的失败信息
B要先拉代码下来git pull进行代码合并
自动合并失败(Automatic merge fiailed),需要手动解决冲突,然后git commit
B再次git push
A拉代码下来 git pull

情景四与情景三不同的是,A和B同时改同一个档案的同一部位
1、A用户修改app/views/welcome/index.html.erb文件,第二行代码前后各插入一行代码


然后进行commit并git push到github

2、同时B也在修改app/views/welcome/index.html.erb这个文件,并且也是在第二行代码前后各加一行代码

B用户修改后进行commit并git push
当git push的时候,出现报错如下图(rejected):

3、这时,与情景二、三解决方法一样,要先git pull拉代码下来,然后再git push,当进行git pull的时候出现报错(conflict)

4、提示有confilct,当自动merge时出错,我们执行git status查看

提示both modified了同一个文件,即A和B用户同时修改app/views/welcome/index.html.erb这个文件
5、这时候,我们打开app/views/welcome/index.html.erb这个文件

上图中,第2行到10行,是冲突部分,即A和B同时修改的部分,用<<<<<<和 >>>>>>符号包起来,同时======符号把A用户commit的代码与B用户commit的代码分开,
其中上边是B用户commit的代码,下边是A用户commit的代码
这时候,我们要修改这个文件,决定保留A用户或者B用户commit的代码

如果保留B用户commit的代码,则第6到10行的代码直接先删除,第2行的代码也删除
如果保留A用户commit的代码,则第2到6行的代码直接先删除,第10行的代码也删除

6、我们决定保留B用户commit的代码


则修改后的代码为:

7、修改后,进行git commit并git push

8、这时候,A用户需要执行git pull把最新代码拉下来

现在,A的代码与B的代码一致了(即都更新到B用户commit的最新代码了)

情景五(切新分支做新功能)

情景五就是我们教程上边的协作方法了
在协作之前,主程A用户

在本地终端把最新分支的代码merge到master(切换到master分支,git merge 最新分支)
git push origin master,把合并后的最新master代码push到github

副程B用户

在本地需要git clone最新代码下来
cd 专案名,进入clone后的专案
cp config/database.yml.example config/database.yml
bundle install
rake db:migrate

这些步骤上边情景中已做过,我们这里不再赘述
1、B用户在master分支基础上切新分支出来


我们在BB分支上,做些改动

然后进行git commit操作

并且git push新分支到github的repo

2、这时候,B用户登录github并且打开github上边相应的repo

我们可以看到最新的BB分支已push到github
B用户现在要发出pull request,点击New pull request按钮

然后跳转到下图

base后边需要选择master,compare后边需要是副程的新分支BB
然后点击下边的Create pull request

点击后,就成功创建了一个pull request,如下图

从上图中可以看出,这个migrate#1是daniel-hua发出的,是合并一个分支BB到master,只有1个commit,现在它的状态是open(主程处理后的pull request显示状态为close)
我们往下拉,可以看到


提示没有冲突,可以进行合并,并且只有对这个repo有write权限的用户才能merge这个pull request,B用户是副程,没有这个权限,主程A才有merge权限

3、这时候主程A用户登录github,打开相应的repo,点击Pull requests,显示如下图


1 Open表示有1个pull request处于open状态(主程还没做merge处理),closed表示已处理过的pull request
点击下边的migrate,显示如下图:

在上图中,Commits 1表示这个pull request有1个commit,File changed 5表示这个pull request中有5个文件发表了改动,下边的Leave a comment中可以添加这次pull request的描述信息,显示"This branch has no conflicts with the base branch" ,表示没有冲突,"Merging can be performed automatically."表示可以进行自动合并,我们点击Merge pull request按钮,然后出现下图

点击comfirm merge,就完成了merge pull request操作了

4、此时,A用户运行git pull把最新代码拉到本地

5、这时候B用户,已完成了BB分支的开发而且被主程合并到master了,B用户首先要切换到master分支,然后从github拉master分支的最新代码下来

此时,A用户和B用户以及github的repo,三者的代码一致了

B用户可以在master重新切新分支出来,做新的功能了

因此副程B用户的任务就是:在本地master基础上切新分支出来-> 开发完后push当前分支到github -> 上github上开pull request给主程 ->主程合并后切换到master分支 -> git pull拉master最新代码下来 -> 在master上切新分支出来做新功能....

情景六:合并时有冲突

在情景五中,如果主程A在合并的时候有冲突,需要处理完冲突再合并(有冲突也不能合并)
前边的步骤与情景五类似,由于篇幅问题,我们快速做完前边步骤
切新分支CC出来


1、B修改seeds.rb文件,并且commit和git push新分支cc到github

2、B用户登录github发出pull request

从上边截图中可以看出,不能自动合并,不过这个pull request还是可以create的,我们先create这个pull request,让主程解决这个冲突并合并代码
点击下边的Create pull request,创建完成pull request
3、此时,A用户登录github并打开相应的repo
点击pull request进入到这个待合并的pull request

点击Resolve conflicts按钮,进入有冲突的文件

左边是有冲突的文件,右边是冲突明细
有右侧明细中,有冲突的代码用<<<<<<<和>>>>>>>包起来
======= 又把这部分分成了上下两半
上半部分是当前pull request中这个文件的写法
下半部分是当前master分支上这个文件的写法
如果我们保留cc分支的代码,则把23-26行的代码直接删除,同时把20行的代码也删除
点击Mark as resolved按钮

之后会跳转回当前pull request地址,显示 This branch has no conflicts with the base branch,点击 Merge pull request,然后 Confirm merge 就可以了

4、这时候,A用户git pull拉最新代码下来,B用户切换到master分支并拉最新代码下来

此时,A用户、B用户以及github上的repo三者代码都一致了

情景七(主程在github进行merge时,还想修改副程的代码)

1、副程在master分支切新分支出来,并修改文件,然后commit、git push新分支到github


2、然后去github创建一个pull request

3、此时主程A在github上边merge的时候,发现有些代码还要进一步修改,修改后再合并,先拉B用户新增的分支到本地

4、在SourceTree里边依次点击origin-dd-检出,这时候A用户终端自动(按回车)切换到dd分支了
或者在终端执行git checkout -b dd origin/dd,这样也可以在本地建立一个dd分支用于跟踪远程origin/dd分支内容

5、主程A用户在dd分支下边,修改一些文件,并且commit

6、然后切到master分支,并且合并dd分支到master

7、现在执行git push把master最新内容push到github

8、这个时候,B用户在dd分支,dd分支已开发完并且被合并到master了,首先要切到master分支,然后拉最新代码下来

这时候,A、B用户都是最新的代码了

补充说明

1、在终端运行stree直接打开SourceTree的配置方法
首先,在终端执行atom ~/.zshrc
然后,在.zshrc文件里边加入下边这行代码
alias stree='/Applications/SourceTree.app/Contents/Resources/stree'


现在,在终端输入stree就会直接打开SourceTree了
2、rejected报错
出现rejected报错一般发生在下列情形
远程github上边有更新代码,但是没有git pull拉最新代码下来,就在本地执行了git push操作
解决方法:在本地先git pull拉最新代码下来,然后再git push推本地的新改动到github
3、conflict报错
出现conflict报错一般发生在下列情形
主程与副程,都修改了一个文件的相同部位,当git pull拉代码下来并与本地代码合并时发生了冲突
解决方法:手工进入有冲突的文件,修改好以后重新push代码到github,然后另外一个协作者git pull拉最新代码下来

更多文章,请关注我的公众号:

如何部署七牛云

1、需要在gemfile文件里边添加

gem 'carrierwave-qiniu'
gem 'qiniu-rs'

然后bundle install 和 bundle update
2、创建touch config/initializers/carrierwave.rb
加入内容

CarrierWave.configure do |config|
  config.storage             = :qiniu
  config.qiniu_access_key    = ENV["qiniu_access_key"]
  config.qiniu_secret_key    = ENV["qiniu_secret_key"]
  config.qiniu_bucket        = ENV["qiniu_bucket"]
  config.qiniu_bucket_domain = ENV["qiniu_bucket_domain"]
  config.qiniu_block_size    = 4*1024*1024
  config.qiniu_protocol      = "http"
  config.qiniu_up_host       = "http://up.qiniug.com"  #选择不同的区域时,"up.qiniug.com" 不同

end

注意config.qiniu_up_host的填写,需要根据区域
可根据https://developer.qiniu.com/kodo/manual/1671/region-endpoint这个链接来填写
3、修改app/uploaders/image_uploader.rb
加入

if Rails.env.production?
    storage :qiniu
  else
    storage :file
  end

或者加入

if Rails.env.production?
    storage :qiniu
elsif Rails.env.development?
    storage :file
end

4、安装figaro密码管理gem
在gemfile里边添加

gem 'figaro'

然后执行 bundle install 和 figaro install
会自动生成 config/application.yml 文件并自动添加到 .gitignore 档案里

5、设置机密信息
cp config/application.yml config/application.yml.example
config/application.yml添加如下内容:

...(略)

 production:
   qiniu_access_key: xxxx  # 你的 qiniu AccessKey

   qiniu_secret_key: xxxx  # 你的 qiniu SecretKey

   qiniu_bucket: xxxx  # 你的 qiniu bucket

   qiniu_bucket_domain: xxxx  # 你的 qiniu bucket domain



 development:
   qiniu_access_key: xxxx  # 你的 qiniu AccessKey

   qiniu_secret_key: xxxx  # 你的 qiniu SecretKey

   qiniu_bucket: xxxx  # 你的 qiniu bucket

   qiniu_bucket_domain: xxxx  # 你的 qiniu bucket domain

6、创建heroku app并将设定好的机密资讯同步到这个app

heroku create 先创建一个heroku app
figaro heroku:set -e production
heroku config 可以列出目前所有的设定

我是如何学习rails的(微信群语音分享文稿)

自我介绍

大家好,我是baohua,现在自由职业,在家工作,同时也做全栈营的线上助教,学习rails也算是兼职学习,我今天给大家分享的是我的rails学习方法,希望对大家有帮助,也感谢大家来听我的分享

1、首先,一定要多打代码

先对着教程多做几遍,每章都需要至少做3遍,其实对于一个无经验的小白来说,3遍是远远不够的,我当时做了3遍后,也是没什么感觉的,rails101做了5遍后,才慢慢的有了一点编程的感觉,当时离招聘课程,还有一段时间,于是就一直练习101课程,当招聘课程开了后,101基本做了八九遍了,做多了以后,其中的许多逻辑关系也慢慢理清了,例如怎么加入群组、退出群组,以及my groups,my post是怎么调用数据的

rails101我觉得是经典教程,也是基础,它就像功夫中的扎马步,如果大家有时间,尽量还是要多做,或者当你学了后边的课程,可以时不时的回来做一做101,相信我,每遍都会有新的收获的,当你把101理解透、做熟练后,其实已经可以做很多的迁移练习了,例如在招聘大赛中,大家做的收藏功能,其实就是加入群组的小迁移

我做助教前,101又做了3遍,101课程累计做有13遍了,不但觉得简单多了,而且逻辑关系算是弄懂了

所以首先呢,代码一写要多写,形成肌肉记忆,这是必须要经过的阶段

2、做针对性的学习

当我把101做熟练了后,发现自己当时最容易出现bug的地方是路径的写法,还有调用数据的方法,于是我就对这两块,进行了专门的研究

对于routes.rb的写法,可以这样标出来,或者用笔在纸上写写画画,这样我们就很清楚它的嵌套关系了


为了学习routes.rb的写法,我专门建立了一个专案,这个专案文件,只用在route.rb中书写代码就可以了,其它文件不用动,先从简单的写起,先写只有resources的,然后终端运行rake routes看看产生了哪些路径,从易到难,依次加入resources嵌套、member do、namespace等,再分别运行rake routes观察它们所产生的路径

对于调用数据的学习,我是进入到rails console中,练习调用各种数据
例如对于user、group、group_relationships之间的关系,我们可以rails c中输入各种命令来调用它们之间的数据,例如下边的命令行语句

User.first.group_relationships
User.first.posts
User.first.grous
User.second.groups
User.find(2).groups
GroupRelationship.all
User.find(3).participated_groups
Group.find(2).members
Group.first.group_relationships
Group.find(5).user
Group.count
GroupRelationship.first.user
GroupRelationship.first.group
GroupRelationship.second
GroupRelationship.where(user_id:User.second.id)
GroupRelationship.where(user_id:1)
GroupRelationship.where(group_id:15)
GroupRelationship.where(group_id:15).where(user_id:1)
Group.first.title
Group.ids

结合我上次在公众号中分享的Hirb这个gem,在rails c中,把上边的这些命令行都输入进去,弄懂这些命令行所要调用的数据,当你需要什么数据时,能很快的写出命令行,说明对数据表之间相互关系的理解,基本过关了

当时为了记住这些知识点,我还把它们的结果,用手机拍下来,存到自己的手机相册,在自己的碎片时间里,时不时的都会瞄一眼,记一记,例如坐公交车时,上厕所时,都会拿出来看一眼,不求每次记很多,只要频率高,总是能记住,老师强调高频小套路,我们就是要让自己,在短时间内多接触代码,这样才能形成肌肉记忆

而且我还把这些比较重要、又需要记忆的知识点,按1 2 3 4那样罗列出来整理成一篇文章,放到自己logdown里边,并且置顶,这样当我每天,写报错日志以及ORID的时候,都基本会先看到这些,我还把这些难记的打印下来,贴到自己电脑屏旁边,强化自己的记忆

3、灵活运用教材

当对教程比较熟练后,我们可以有意识的去观察,在终端输入命令行的时候,看看终端会输出什么结果,例如当我们在终端输入

rails g model topic


我们可以看到其实就是产生了几个文件,如果想删除topic,除了rails d之外,用rm -rf手工删除这几个文件,也是可以实现的

再例如我们执行rake db:migrate的时候,我们可以观察下运行结果:


从执行结果中可以看出来,这个命令的执行结果,其实就是建立数据表、修改数据表等的过程

我们在做练习时,对于类似这样的命令行的执行结果,可以留意下,而且可以试着输入进去运行,不但能学到知识,而且对于理解rails的逻辑关系,还是有很大帮助的

随着我们对代码越来越熟练,不但要观察命令执行的结果,而且对于教程中问题的解法,我们可以想想是否有其他解法,例如管理员帐户的设定,教程中是先进入rails c中,然后执行三行命令来实现


实际上,执行这几行命令的结果,就是把user的is_admin栏位值更新为true

如果我们对前边的知识点足够熟悉,在第二课的初级练习中
https://fullstack.xinshengdaxue.com/posts/47


就用到了update_attributes方法,它也是更新栏位值的

那么把user设为管理员帐户的方法,除了教程中的方法,也是可以用update_attributes来实现的,具体的命令如下:

User.first.update_attributes(is_admin: true)

这样设定管理员帐户,一行命令就可以实现了

再比如在教程中,对于无主群组的解法,


它是把group表的内容全部清空了,这时候,其实我们可以想想,是否有更好的解法,当我们想保留已经输入的group数据时,是否有解法呢?
我们看一下报错的截图

它提示我们email的值是空的,而email是group.user的email,所以它的真实意思是group.user这个值为空

这是因为前边添加group的时候,还没有user,所以group.user是空的,知道了原因后,我们可以给这些group的user栏位赋值就可以了,批量赋值语句为:

Group.update_all(user_id:1)

如果没有id为1的use,可以用下边的语句

Group.update_all(user_id:User.first.id)

这样当group都有user值时,这个bug就解决了,而且group的值还存在,没有被清空删除

update_attributes我们在教程中就有用过,但是update_all没用过,我是怎么挖出这个命令的呢?

其实就是先有了这种需求(就是怎么批量赋值),当有这个需求后,就去google搜索,这样就挖掘到了update_all的用法,扩充了我们的知识,因此当我们学了一段时间后,要培养独立解决问题的能力,遇到问题,先自己试着去解决,实在解不出来,再找助教或者同学帮助,不要做纯粹的拿来主义者,那样容易产生依赖

4、分块学习法

既然支撑我们的编程理论是拼图理论,我们可以对教程进行切分,一块一块的学习

例如招聘课程中,is_admin的实现方法,我们就可以做一个小提取,一共分为4个步骤,这4个步骤是实现is_admin的最小必要步骤,需要记下来

再比如user与group的多对多关系表的设定,也可以做个小提取


它分为3个步骤,user.rb文件、group.rb文件,以及group_relationship.rb中把它们的代码记住就可以了,这是多对多关系的最少必要知识,我们需要把它记下来,以后再做其他多对多关系时,用这个最少必要知识,进行灵活的迁移就可以了

分块学习法,对我还是挺有用的,当我进行debug调试时,例如关于is_admin方面的bug,我只需要检查这几个最少必要知识的每一步写法是否正确,就可以了

所以,大家进行学习的时候,可以像我这样,把教程拆成一块一块的进行学习,哪些部分薄弱了,可以进行那一块的专门提取练习

把这些一块一块的知识点都掌握后,再对它们进行串联学习,这样对知识点的掌握就会比较牢固、系统

5、去slack频道学习

每天可以抽出一块时间,例如一小时,来专门去slack频道学习,我们没必要一直守着slack(那样有一点浪费时间),你可以每天抽出1小时时间,去自己所在的slack频道,进行集中学习,对自己所在频道同学们遇到的每一个问题,都看看助教是怎么解答的,自己也可以尝试着去解答,看看自己的思路与助教的思路差别在哪里

大家的时间都很紧,又要上班,又要学习,确实还挺累的,所以我们要提高时间的利用率,例如当我们在slack浏览每一个问题时,遇到自己完全能解决的问题的时候,我们就快速跳过,只看那些我们没有遇到过的、自己解决不了的问题,对于这些自己解决不了的问题,要记下来

例如可以整理到自己的logdown,logdown有一个功能,可以发表私人文章,我们在整理slack没有遇到的问题的时候,可以随意一些(当然如果自己时间很充足,也可以完整的整理出来)可暂时放到私人文章中

我刚开始做助教的时候,就是这样做的,16个频道,我每个频道都看,每个问题都看,自己特别熟悉的,就快速跳过,对于自己没遇到过的,我就记到logdown的私人文章中,然后特意去记这些自己不熟悉的,这样坚持了一段时间,感觉自己的debug能力有很大的提升,而且对教程的理解,也提升了许多

slack上边回答问题,其实就是一个提取练习,而且要求更加精准、更加快速的找到问题所在

解决一个一个的问题,其实就是做一次一次的提取练习,而且是在不同的场景下的提取练习(许多场景自己之前还没遇到过)学习效果非常好,所以,如果大家有时间,尽量每天抽出一定的时间来slack频道学习,这是必要的,也是值得的

当自己掌握熟练后,建议到slack上边帮助其他同学,教是最好的学,当你为自己解决问题,与为别的同学解决问题,其实心态是不一样,当你为别的同学解决问题时,甚至会更加用心,同时也倒逼你去学习

最后,请大家相信自己,付出总有回报,我们走的每一步都算数,我们一起加油!

我今天的分享就到这里,谢谢大家!

Refactor效能提升

效能提升基础概念

说到网站效能提升,一个网站大概分成几个结构

网站放置主机的地方(美国,日本,北京,等等等等)
前端代码结构(CSS/JavaScript)
后端代码结构
Ruby Code 效能
数据库的效能
那我们从哪里入手进行优化呢?
做优化,我们也要有最有效率的地方做起
也就是得从我们的「注意力」可以换取最高的网站提升效率速度来考虑优先级,「一天能够提升的有感速度」。
首先,地理位置是一定会对开启速度造成差异的。
例如,我们在中国访问heroku(它服务器在美国),速度明显很慢,可是我们访问天猫、京东等,明显速度快许多

网站代码产生网页的速度 + 下载网页的速度 + 下载素材的速度
网站代码产生网页的速度(通常在100ms-200ms)
下载网页(通常一张网页约 100-200KB)
下载素材 x 15(通常一张网页大约有 15 个以上素材)
由这个公式我们会总结出

下载所需时间 > (远大于)网页生成时间
如果你的网站放在美国(如 Heroku),素材又很多的话,网站会巨慢

调整前端架构或搬机器比起调整后端代码或数据库架构来说,性价比远远为高。

但是,搬机器的地理位置手续非常复杂,特别是机器往往不是一台的,而是一组的,依据地理位置分配资源的技术与架构实做起来会非常复杂。所以我们这种「个体户程序员」,提速的方向往往会以「修正前端架构」去思考。

购物网站代码重构(二)

1、用helper重写订单的created时间

在order的view页面中,出现了<%= order.created_at.to_s(:long) %>这样的、有一点艰涩的代码,我们可以对它进行优化,使它更具有可读性
在app/helpers/account/orders_helper.rb中加入

def render_order_time(order)
    order.created_at.to_s(:long)
end

在view页面中,就可以用render_order_time(order)来代替order.created_at.to_s(:long)了
修改app/views/account/orders/index.html.erb

   <td><%= link_to(order.id,order_path(order.token)) %></td>
-      <td><%= order.created_at.to_s(:long) %></td>
+      <td><%= render_order_time(order) %></td>
 </tr>

2、用partial优化app/views/products/index.html.erb页面

app/views/products/index.html.erb页面中,代码是这样写的

<div class="row">
  <% @products.each do |product| %>
    <div class="col-xs-6 col-md-3">
      <%= link_to product_path(product) do %>
        <% if product.image.present? %>
          <%= image_tag(product.image.thumb.url,class: "thumnial") %>
        <% else %>
          <%= image_tag("http://placehold.it/200x200&text=No Pic",class: "thumbnail") %>
        <% end %>
      <% end %>
      <%= product.title %> ¥ <%= product.price %>
    </div>
  <% end %>
</div>

修改它

<div class="row">
-  <% @products.each do |product| %>
-    <div class="col-xs-6 col-md-3">
-      <%= link_to product_path(product) do %>
-        <% if product.image.present? %>
-          <%= image_tag(product.image.thumb.url,class: "thumnial") %>
-        <% else %>
-          <%= image_tag("http://placehold.it/200x200&text=No Pic",class: "thumbnail") %>
-        <% end %>
-      <% end %>
-      <%= product.title %> ¥ <%= product.price %>
-    </div>
-  <% end %>
+  <%= render :partial => "product_item", :collection => @products, :as => :product %>

其中,

<%= render :partial => "product_item", :collection => @products, :as => :product %>

也可以用

<%= render: "product_item", collection: @products, as: "product" %>

来代替,两种写法,效果一样

同时创建新文件app/views/products/_product_item.html.erb,它的内容如下:

<div class="col-xs-6 col-md-3">
  <%= link_to product_path(product) do %>
    <% if product.image.present? %>
      <%= image_tag(product.image.thumb.url,class: "thumnial") %>
    <% else %>
      <%= image_tag("http://placehold.it/200x200&text=No Pic",class: "thumbnail") %>
    <% end %>
  <% end %>
  <%= product.title %> ¥ <%= product.price %>
</div>

此时我们查看rails s的log,可以看到


product_item已经被加载了

同时,我们要对app/views/admin/products/index.html.erb做同样的partial优化

<tbody>
-    <% @products.each do |product| %>
-      <tr>
-        <td>
-          <%= product.id %>
-        </td>
-        </td>
-        <td>
-          <%= link_to product_path(product) do %>
-            <% if product.image.present? %>
-              <%= image_tag(product.image.thumb.url, class: "thumbnail") %>
-            <% else %>
-              <%= image_tag("http://placehold.it/200x200&text=No Pic", class: "thumbnail") %>
-            <% end %>
-          <% end %>
-        </td>
-      <td>
-        <%= product.title %>
-      </td>
-      <td>
-        <%= product.price %>
-      </td>
-      <td>
-        <%= link_to("Edit",edit_admin_product_path(product)) %>
-      </td>
-      </tr>
-    <% end %>
+    <%= render partial: "product_item", collection: @products, as: "product" %>
   </tbody>

创新文件app/views/admin/products/_product_item.html.erb,它的内容如下:

<tr>
    <td>
      <%= product.id %>
    </td>
    <td>
      <%= link_to product_path(product) do %>
        <% if product.image.present? %>
          <%= image_tag(product.image.thumb.url, class: "thumbnail") %>
        <% else %>
          <%= image_tag("http://placehold.it/200x200&text=No Pic", class: "thumbnail") %>
        <% end %>
      <% end %>
    </td>
  <td>
    <%= product.title %>
  </td>
  <td>
    <%= product.price %>
  </td>
  <td>
    <%= link_to("Edit",edit_admin_product_path(product)) %>
  </td>
  </tr>

购物网站代码重构(一)

1、用before_action进行代码重构

在oders_controller中,发现在show、pay_with_alipay、pay_with_wechat中,都包含有@order = Order.find_by_token(params[:id])这一行代码,我们可以把@order = Order.find_by_token(params[:id])写到一个method中,并且在before_action中写入这个method,重构后的代码如下:

class OrdersController < ApplicationController
  before_action :authenticate_user!, only: [:create]
  before_action :find_order_by_token, only: [:show, :pay_with_alipay, :pay_with_wechat]
  def create
    @order = Order.new(order_params)
    @order.user = current_user
    @order.total = current_cart.total_price

    if @order.save

      current_cart.cart_items.each do |cart_item|

        product_list = ProductList.new
        product_list.order = @order
        product_list.product_name = cart_item.product.title
        product_list.product_price = cart_item.product.price
        product_list.quantity = cart_item.quantity
        product_list.save
      end

      OrderMailer.notify_order_placed(@order).deliver!
      redirect_to order_path(@order.token)
    else
      render 'carts/checkout'
    end
  end

  def show
    @product_lists = @order.product_lists
  end

  def pay_with_alipay
    @order.set_payment_with!("alipay")
    @order.make_payment!
    redirect_to order_path(@order.token), notice: "使用支付宝完成付款"
  end

  def pay_with_wechat
    @order.set_payment_with!("wechat")
    @order.make_payment!
    redirect_to order_path(@order.token), notice: "使用微信成功完成付款"
  end

  def apply_to_cancel
    @order = Order.find(params[:id])
    OrderMailer.apply_cancel(@order).deliver!
    flash[:notice] = "已提交申请"
    redirect_to :back
  end


  private

  def find_order_by_token
    @order = Order.find_by_token(params[:id])
  end

  def order_params
    params.require(:order).permit(:billing_name, :billing_address, :shipping_name, :shipping_address)
  end

end

2、利用继承重构

app/controllers/admin/orders_controller.rb和app/controllers/admin/products_controller.rb中
都包含有代码:

layout "admin"
  before_action :authenticate_user!
  before_action :admin_required

我们可以新建立一个admin_controller.rb,把这三行代码加入到这个admin_controller中

class AdminController < ApplicationController
  layout "admin"
  before_action :authenticate_user!
  before_action :admin_required
end

然后app/controllers/admin/orders_controller.rb和app/controllers/admin/products_controller.rb
继承这个admin_controller

-class Admin::OrdersController < ApplicationController
-  layout "admin"
-
-  before_action :authenticate_user!
-  before_action :admin_required
+class Admin::OrdersController < AdminController
-class Admin::ProductsController < ApplicationController
-  layout "admin"
-  before_action :authenticate_user!
-  before_action :admin_required
+class Admin::ProductsController < AdminController

3、mixin 与 ActiveSupport::Concern

在app/models/order.rb中

before_create :generate_token

  def generate_token
    self.token = SecureRandom.uuid
  end

这几行代码,其它地方也可能会用到,如果都复制、粘贴这三行,显的有些麻烦,我们可以在app/models/concerns
下边建立一个tokenable.rb文件,这个文件内容如下:

module Tokenable

    extend ActiveSupport::Concern

    included do
      before_create :generate_token
    end


    def generate_token
      self.token = SecureRandom.uuid
    end
end

现在修改app/models/order.rb,把之前的

before_create :generate_token

  def generate_token
    self.token = SecureRandom.uuid
  end

去掉,在app/models/order.rb上边添加include Tokenable,变为:

class Order < ApplicationRecord
   include AASM

+  include Tokenable
   aasm do
     state :order_placed, initial: true
     state :paid
   end

-  before_create :generate_token
-  def generate_token
-    self.token = SecureRandom.uuid
-  end

进行这三步重构后,代码看起来要清爽一些了

后端性能优化学习

一个网站,在后端方面,90%的机率是慢在数据库的操作的,10%是慢在ruby code代码,
所以我们在优化后端性能的时候,可以着重优化关于数据库的操作

我们可以从以下方面入手:
检测网站加载时间可以用:Rails 里面的测速神器:https://github.com/MiniProfiler/rack-mini-profiler

1、消灭N+1 Query

例如我们访问:


查服务器的logs信息,可以看到:

它执行了4次查询,即N+1 Query,第一个是查询groups的,后三个都是查询user表的,
如果网页数据比较多,会执行很多次查询,会大大的拖慢网站的加载速度,这时候我们可以用
includes(:class)的方式来解决N+1 Query的问题
例如在groups_controller中的

@groups = Group.all可以改为@groups = Group.includes(:user).all

这样在查询每个group的user时,执行一次sql查询就可以了

def index
    @groups = Group.includes(:user).all
  end

再找logs信息


这里它把查询user合并为查询一次了

2、消灭Table Scan

Table Scan 的意思是一般数据库,在没有加 Index (索引)的情况下,找资料是一笔一笔的循序的找
这样查询效率非常低,解决办法是
为某些经常查询的栏位,加上索引
加索引是新增一个 migration 然后打上 Index,例如

def change
   add_index :orders, :user_id
end

同样,我们也可以给布尔类型的栏位加索引,给日期栏位加索引,还可以给 state_machine 的条件加索引
索引的建立也是有效率区别的,它们按照索引效率高低分为:
boolean > integer > string > date > datetime

3、使用 counter cache

我们在查询资料有多少笔时,例如group.posts.count时,发现logs信息是执行了select count()这样的命令,
其实count(
)挺伤效能的
解决办法就是用counter cache,它的实现方法是为has_many的一方添加一个栏位(例如给group添加posts_count栏位)
然后在belogs_to的一方model中添加上counter_cache: true

4、用工具自动侦测 N+1 Query, Counter Cache

Bullet,参考地址: https://github.com/flyerhzm/bullet很好用的工具,我上一篇写过,这里不再赘述

bullet的安装及使用

bullet是一个很好的监测调试工具,它可以检测出网站中N+1 query,TableScan,Countercache问题
它的安装可以参考:
https://github.com/flyerhzm/bullet
我们可以按照下边的demo来安装,下边就是rails101为例来说明它的安装步骤以及使用方法

1、gemfile里边安装bullet

1)gem "bullet"


2)在终端执行bundle install
3)执行bundle install

2、在config/environments/development.rb中设置bullet

添加以下代码进去:

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
#  Bullet.growl = true
  Bullet.rails_logger = true
  Bullet.add_footer = true
end

如下图:

3、重启rails s

加载成功,下边我们用它检测网站


现在我们访问localhost:3000(即groups#index),网页弹出一个框


因为这个页面调用了user(group.user)
根据提示框的信息,我们修改groups_controller的index,修改如下:

此时我们观察logs,发现

我们访问my posts页面(localhost:3000/account/posts),弹出框


根据提示,我们修改account下边的posts_controller文件的index

点击弹出框的确定,然后刷新刚才的my posts页面,发现不再弹出了
此时观察logs,可以看到:

再检测group的view页面
弹出对话框,如下图:


根据提示, 我们修改groups_contrller中的show

counter cache的配置步骤

counter cache主要是用来优化数据库存查询的,例如在下边的应用场景下:

class Group < ApplicationRecord
  has_many :posts
end
class Post < ApplicationRecord
  belongs_to :group
end

在执行语句group.posts.count时,服务器log信息如下:


出现了很多个count(*),可是count(*)是很费服务器的效能的,如果数据量足够的时候,影响是非常大的

这正是counter cache要做的事情
它配置的步骤如下:

1、在post.rb中(belongs_to的一方),添加counter_cache: true

即把之前的belongs_to :group这一行修改为:
belongs_to :group, counter_cache: true

2、在groups表(即has_many的一方)中增加一个posts_count栏位

1)执行 rails g migration add_posts_count_to_group

2)在新产生的db/migrate/xxx_posts_count_to_group.rb文件中,添加

   add_column :groups, :posts_count, :integer, default: 0

    Group.pluck(:id).each do |i|
      Group.reset_counters(i, :posts) # 全部重算一次
    end

其中,后三行代码如果不加,那么counter cache对于之前的数据就没优化成功,所以需要添加上,重新计算,这样对于之前的历史数据,counter cache也可以起作用了

3)执行 rake db:migrate

注意:第1步与第2步,不能颠倒,如果先增加栏位,在rake db:migrate的时候,就会报错如下:


需要先修改post.rb,才能增加栏位posts_count

3、修改app/views/account/groups/index.html.erb页面中

把<%= group.posts.count %>修改为
<%= group.posts_count %>或者<%= group.posts.size %>
这样就配置完成了,现在log信息显示如下:

如果要查询posts_count这个栏位是否生效,可以去rails console中,用Group.all查询这个栏位的值