dodoliu的折腾笔记

生命不息,折腾不止!

0%

前言

海康威视官方提供了Web3.0和Web3.2的SDK,

Web3.0需要浏览器支持NPAPI,但是高版本的浏览器都已经禁用了这种功能.

Web3.2需要设备支持WebSocket.

这两个Web SDK都无法适用于高版本浏览器显示海康威视老设备的视频流.

无奈只能求助官方技术支持.

官方技术支持响应还是很迅速的.描述完我的需求后,技术小哥哥(小姐姐)立马给我了一个方案(估计是被咨询了很多次):

1
您可以用RTSP协议取流,然后用第三方比如FFmpeg对数据处理成FLV或者RTMP,在谷歌网页上播放

又经过一通查询发现高版本浏览器也不支持RTMP流的显示了.只能使用flv,然后使用bilibili开源的flv.js进行展示.

方案确认了,那还愁啥,搞起来!嗨起来!

准备

因为我的项目部署环境是windows,所以所有软件都是windows版本.

ffmpeg下载地址

1
http://ffmpeg.org/download.html#build-windows

通过查询,rtsp要处理成flv需要借助nginx的nginx-http-flv-module模块.windows下编译带该模块的nginx超级麻烦,不过有人在CSDN上提供了编译好后的可用包.

下载地址:

1
https://download.csdn.net/download/KayChanGEEK/12270210

为了方便测试,建议再下载一个vlc:

1
https://www.videolan.org/

Nginx配置

配置文件参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#user  nobody;
# multiple workers works !
worker_processes 2;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;

events {
worker_connections 8192;
# max value 32768, nginx recycling connections+registry optimization =
# this.value * 20 = max concurrent connections currently tested with one worker
# C1000K should be possible depending there is enough ram/cpu power
# multi_accept on;
}

rtmp {
server {
listen 1935;
chunk_size 4000;
application live {
live on;
}
}
}

http {
#include /nginx/conf/naxsi_core.rules;
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr:$remote_port - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

# # loadbalancing PHP
# upstream myLoadBalancer {
# server 127.0.0.1:9001 weight=1 fail_timeout=5;
# server 127.0.0.1:9002 weight=1 fail_timeout=5;
# server 127.0.0.1:9003 weight=1 fail_timeout=5;
# server 127.0.0.1:9004 weight=1 fail_timeout=5;
# server 127.0.0.1:9005 weight=1 fail_timeout=5;
# server 127.0.0.1:9006 weight=1 fail_timeout=5;
# server 127.0.0.1:9007 weight=1 fail_timeout=5;
# server 127.0.0.1:9008 weight=1 fail_timeout=5;
# server 127.0.0.1:9009 weight=1 fail_timeout=5;
# server 127.0.0.1:9010 weight=1 fail_timeout=5;
# least_conn;
# }

sendfile off;
#tcp_nopush on;

server_names_hash_bucket_size 128;

## Start: Timeouts ##
client_body_timeout 10;
client_header_timeout 10;
keepalive_timeout 30;
send_timeout 10;
keepalive_requests 10;
## End: Timeouts ##

#gzip on;

server {
listen 80;
server_name localhost;


location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
root nginx-rtmp-module/;
}
location /control {
rtmp_control all;
}

# 重要 http-flv 转流配置
location /live {
flv_live on; #打开 HTTP 播放 FLV 直播流功能
chunked_transfer_encoding on; #支持 'Transfer-Encoding: chunked' 方式回复

add_header 'Access-Control-Allow-Origin' '*'; #添加额外的 HTTP 头
add_header 'Access-Control-Allow-Credentials' 'true'; #添加额外的 HTTP 头
add_header 'Cache-Control' 'no-cache';
}

#charset koi8-r;
#access_log logs/host.access.log main;

## Caching Static Files, put before first location
#location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
# expires 14d;
# add_header Vary Accept-Encoding;
#}

# For Naxsi remove the single # line for learn mode, or the ## lines for full WAF mode
location / {
add_header Cache-Control no-cache;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header 'Access-Control-Allow-Headers' 'Range';

#include /nginx/conf/mysite.rules; # see also http block naxsi include line
##SecRulesEnabled;
##DeniedUrl "/RequestDenied";
##CheckRule "$SQL >= 8" BLOCK;
##CheckRule "$RFI >= 8" BLOCK;
##CheckRule "$TRAVERSAL >= 4" BLOCK;
##CheckRule "$XSS >= 8" BLOCK;
root html;
index index.html index.htm;
}

# For Naxsi remove the ## lines for full WAF mode, redirect location block used by naxsi
##location /RequestDenied {
## return 412;
##}

## Lua examples !
# location /robots.txt {
# rewrite_by_lua '
# if ngx.var.http_host ~= "localhost" then
# return ngx.exec("/robots_disallow.txt");
# end
# ';
# }

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000; # single backend process
# fastcgi_pass myLoadBalancer; # or multiple, see example above
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}

# HTTPS server
#
#server {
# listen 443 ssl spdy;
# server_name localhost;

# ssl on;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_timeout 5m;
# ssl_prefer_server_ciphers On;
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:ECDH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!eNULL:!MD5:!DSS:!EXP:!ADH:!LOW:!MEDIUM;

# location / {
# root html;
# index index.html index.htm;
# }
#}
}

有两个地方需要特殊说明:

第一处:这里是配置rtmp, 1935端口是默认端口.

1
2
3
4
5
6
7
8
9
rtmp {
server {
listen 1935;
chunk_size 4000;
application live {
live on;
}
}
}

第二处: flv拉流的配置

1
2
3
4
5
6
7
8
location /live {
flv_live on; #打开 HTTP 播放 FLV 直播流功能
chunked_transfer_encoding on; #支持 'Transfer-Encoding: chunked' 方式回复

add_header 'Access-Control-Allow-Origin' '*'; #添加额外的 HTTP 头
add_header 'Access-Control-Allow-Credentials' 'true'; #添加额外的 HTTP 头
add_header 'Cache-Control' 'no-cache';
}

海康威视摄像头RTSP流获取

网上一堆介绍,可自行搜索学习.

1
2
3
4
5
6
7
8
9
rtsp://admin:123456@192.168.10.54:554/Streaming/Channels/101?transportmode=unicast

rtsp:协议
admin:摄像头登录用户名
123456:摄像头登录密码
192.168.10.54:摄像头ip
554:拉rtsp流的端口
101:通道表示(主码流通道)
transportmode=unicast:传输模式

使用ffmpeg进行流转换

在命令行中输入一下命令

1
2
3
ffmpeg  -rtsp_transport tcp -i rtsp://admin:123456@192.168.10.54:554/Streaming/Channels/101?transportmode=unicast -vcodec copy -an -acodec copy -f flv rtmp://127.0.0.1:1935/live/mystream

rtmp://127.0.0.1:1935/live/mystream: 这个地址是nginx中配置的rtmp节点的地址, live是配置的app名称,mystream可以随意指定

出现下图所示效果且进程不终止,表明转换流成功了.

image-20211213174237900

有了上面的rtmp地址后,我们可以知道flv取流的地址为

1
2
3
4
5
6
http://127.0.0.1/live?port=1935&app=live&stream=mystream

这是通过nginx中http节点配置的.请注意app的值和stream的值
对应的是
rtmp://127.0.0.1:1935/live/mystream
中的 live和mystream

有了flv拉流的地址后,我们就可以在vlc中进行校验

在vlc中进行测试

点击vlc菜单中的 “媒体”,然后选择 “流”

然后选择”网络”

image-20211213175059382

然后将右下方的 “串流” 选择为 “播放”,在等待几秒后就可以看到正常的视频图像了.

image-20211213175204858

如果没有显示,请继续爬坑吧!!!

在vue中显示

我的前端项目是个vue项目.

先安装flv.js

1
npm install --save flv.js

html主要代码

1
2
3
4
5
6
<video
id="vPull"
autoplay
style="height: 100%; width: 100%; object-fit: fill"
muted
></video>

js主要代码.

参考: https://blog.csdn.net/HJFQC/article/details/109626836

感谢原作者

1
2
3
mounted() {
this.play("http://127.0.0.1/live?port=1935&app=live&stream=mystream");
}

我需要想说明一下如何调用,在mounted方法中直接调用play方法,然后传入准备好的flv流地址.

没有意外的话,vue项目启动后就可以正常查看视频了.

.net core后台动态切换摄像头地址

经过上面所有操作后,我们已经可以在前端进行视频展示了.但是如果想切换摄像头该怎么搞?

可以参考以下思路:

通过后端代码进行ffmpeg的进程创建和关闭.

我的后端是.net core项目,参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// <summary>
/// 启动ffmpeg进程
/// </summary>
/// <returns></returns>
[HttpGet("TestCmd54")]
public async Task<int> TestCmd54()
{
var psi = new System.Diagnostics.ProcessStartInfo("D:\\App\\ffmpeg\\bin\\ffmpeg.exe", "-rtsp_transport tcp -i rtsp://admin:123456@192.168.10.54:554/Streaming/Channels/101?transportmode=unicast -vcodec copy -an -acodec copy -f flv rtmp://127.0.0.1:1935/live/mystream");
var ss = System.Diagnostics.Process.Start(psi);
return await Task.FromResult(ss.Id);
}
/// <summary>
/// 关闭ffmpeg进程
/// </summary>
/// <returns></returns>
[HttpGet("TestCmd54Stop")]
public async Task<int> TestCmd54Stop(int processId)
{
var stopProcess = System.Diagnostics.Process.GetProcessById(processId);
if (!stopProcess.HasExited)
{
stopProcess.Kill();
}

return await Task.FromResult(1);
}

简介

最近做的一个项目需要在Web页面上展示视频图像信息.

项目中使用的摄像头是海康威视的.经过一番捣鼓后终于可以正常显示图像了.

于是做个记录,供其他同学爬坑.

开发准备

WEB无插件开发包 V3.2 官方下载地址:

https://open.hikvision.com/download/5cda567cf47ae80dd41a54b3?type=10&id=4c945d18fa5f49638ce517ec32e24e24

image-20211116165646268

下载完成解压后包含以下内容:

image-20211116165802938

设备网络搜索软件下载地址:

https://www.hikvision.com/cn/support/Downloads/Desktop-Application/HikvisionTools/?q=%E5%B7%A5%E5%85%B7%E8%BD%AF%E4%BB%B6%EF%BC%88hikvision%20tools%EF%BC%89&position=1

image-20211116170730350

这是海康威视官方提供的用于发现相同局域网下所有设备的工具.效果如下图:

image-20211116170905290

一个海康威视的摄像头,需要支持WebSocket.

我这边测试用的摄像头是这一款:

image-20211116170300397

==测试环境下需要保证摄像头和开发机器在同一个局域网内==

tips:

  1. 如果通过 设备网络搜索软件SADP 无法发现你需要是设备,则表明你的开发机器的网络和设备的网络不通.
  2. 将摄像头设置为使用 DHCP

image-20211116171337535

  1. Vue开发环境下无法看到正常的视频图像,需要使用Nginx进行代理

快乐的码代码

Vue代码可以借鉴这篇文章的

https://blog.csdn.net/Vslong/article/details/118517641

按照上面这篇文章码完Vue代码后如果想正常看到视频图片还需要完成以下操作:

设置Vue代理

在Vue的Config文件夹下的index.js配置文件中设置proxyTable

参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
proxyTable: {
'/ISAPI': {//配置代理地址,前端请求的所有接口都需要带的前缀
target: 'http://192.168.10.51:12345', #我本地监控的Web3.2无插件Nginx代理地址
changeOrigin: true,//是否进行跨域
secure: false,
// logLevel: 'debug',
},
'/SDK': {
target: 'http://192.168.10.51:12345',
changeOrigin: true,
secure: false,
// logLevel: 'debug',
}
},
然后编译发布Vue代码,然后修改Nginx配置.

参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157

#user nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

access_log logs/access.log main;
#access_log off;
client_max_body_size 50m;
sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 12345;
server_name 192.168.10.51;

#charset koi8-r;

access_log logs/host.access.log main;
#websocket相关配置
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

proxy_set_header 'sec-ch-ua' "";
proxy_set_header 'sec-ch-ua-mobile' "";
proxy_set_header 'sec-ch-ua-platform:' "";
proxy_set_header 'Sec-Fetch-Dest' "";
proxy_set_header 'Sec-Fetch-Mode' "";
proxy_set_header 'Sec-Fetch-Site' "";

location / {
root "../dist";
index index.html index.htm;
}

location ~ /ISAPI|SDK/ {
if ($http_cookie ~ "webVideoCtrlProxy=(.+)") {
proxy_pass http://$cookie_webVideoCtrlProxy;
break;
}
}
location ^~ /webSocketVideoCtrlProxy {
#web socket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;

if ($http_cookie ~ "webVideoCtrlProxyWs=(.+)") {
proxy_pass http://$cookie_webVideoCtrlProxyWs/$cookie_webVideoCtrlProxyWsChannel?$args;
break;
}
if ($http_cookie ~ "webVideoCtrlProxyWss=(.+)") {
proxy_pass http://$cookie_webVideoCtrlProxyWss/$cookie_webVideoCtrlProxyWsChannel?$args;
break;
}
}
#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# error_page 302 /50x.html;
# location = /50x.html {
# root html;
# }
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}

以上是无插件开发包自带的nginx配置,只简单修改了三处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
监控端口:
listen 12345;
server_name 192.168.10.51;

静态文件根目录:
location / {
root "../dist";
index index.html index.htm;
}

添加:
proxy_set_header 'sec-ch-ua' "";
proxy_set_header 'sec-ch-ua-mobile' "";
proxy_set_header 'sec-ch-ua-platform:' "";
proxy_set_header 'Sec-Fetch-Dest' "";
proxy_set_header 'Sec-Fetch-Mode' "";
proxy_set_header 'Sec-Fetch-Site' "";

然后重启nginx,访问你的项目web地址应该就能看到视频图像了.

如果还是看不到就继续爬坑吧!!!

其它可能会出现的问题

如果出现了 Too-many-header 这个错误

参考: https://www.scaugreen.cn/posts/65442/

写在最后

感谢本人在爬坑过程中翻过的所有文章.

2017要走,我拦都拦不住!
那就让他走吧!
先总结一下2017年的计划完成情况!

至少读两本文学书! (只读了一本!未完成)
学一门新技术语言! (没有学新的技术语言!未完成)
至少看两本技术书籍!(完成)
坚持写技术Blog!(未完成)
坚持看技术blog!(完成)
坚持使用默默背单词!(完成)
坚持锻炼身体!(完成)

基于以上各计划的完成情况,自我认为不是太理想.各种原因都有,但这不是借口!希望新的一年严格执行计划!

回顾这一年的点滴,生活和工作都在向着好的方向发展.印证了那句”努力就会有回报!”.我希望自己在新的一年里更加努力!因为我相信明天会美好的!

以下为新年计划:
至少读两本文学书!
至少学一门新技术!
至少看两本技术书籍!
坚持写技术Blog!
坚持看技术blog!
坚持使用默默背单词!
坚持锻炼身体!

我在研究sidekiq异步发送邮件的过程中,遇到了sidekiq无法执行队列中的任务的情况.
明明sidekiq的”已进入队列”已经包含了发送邮件的任务,并且我非常确定发送邮件的程序是没有问题的.
但是无论怎么尝试,邮件都发送不出去.
rails console中看到的入队信息是这样的

1
2
2.3.0 :015 > OrderMailer.confirm_email(1).deliver_later
Enqueued ActionMailer::DeliveryJob (Job ID: a7cda76b-1f01-4f4a-a695-3be1c6bb2640) to Sidekiq(development_mailers) with arguments: "OrderMailer", "confirm_email", "deliver_now", 1

这里很明确的指出队列的名称是 development_mailers
在我没有查明原因之前我的sidekiq.yml配置项是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
:concurrency: 5
:pidfile: tmp/pids/sidekiq.pid

:queues:
- default
- [myqueue, 2]

development:
:concurrency: 5
staging:
:concurrency: 10
production:
:concurrency: 20

因为对sidekiq研究不透,当时并没有注意 :queues 这块的含义.
在反复google后终于在这篇教程里找到了问题的原因所在.
其中有这样一句话,让我恍然大悟:
“!!! important. You may need to include new queue names in sidekiq.yml file:”
我了个大艹!
原来是我没有把 development_mailers 队列加到可执行队列中去.
添加后的sidekiq.yml配置项是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
:concurrency: 5
:pidfile: tmp/pids/sidekiq.pid

:queues:
- default
- [myqueue, 2]
- development_mailers

development:
:concurrency: 5
staging:
:concurrency: 10
production:
:concurrency: 20

重启sidekiq后,世界美好了!
ps:

1
2
3
4
:queues:
- default
- [myqueue, 2]
- development_mailers

这块的含义是sidekiq可以执行的队列集合,其中 [myqueue, 2] 这里的2 表示被分配到的权重是2.
可参考这里

在Rails项目中,scaffold提供了生成初始代码的强大功能.但任然不能满足日常开发需求.针对部分重复的代码手动ctrl+c,ctrl+v总显得太low.如果能自定义scaffold那就完美了.
庆幸的是Rails提供了这种功能.下面以我的开源项目opendoc为例,来自定义scaffold.
首先来生成generator.

1
rails generate generator opendoc_scaffold

上面的命令会生成下面这些文件

1
2
3
4
5
6
create  lib/generators/opendoc_scaffold
create lib/generators/opendoc_scaffold/opendoc_scaffold_generator.rb
create lib/generators/opendoc_scaffold/USAGE
create lib/generators/opendoc_scaffold/templates
invoke test_unit
create test/lib/generators/opendoc_scaffold_generator_test.rb

单从名字上看,我们已经大体上知道这些文件是干啥用的了.
USAGE是定义一些说明.
templates文件夹下放一些模板.
opendoc_scaffold_generator.rb是启动文件.内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class OpendocScaffoldGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__) #指定模板根目录
#创建一个启动方法,以下代码不是自动生成的
def copy_opendoc_scaffold_file
#controller
template 'controller.rb', "app/controllers/backend/#{table_name}_controller.rb"
#model
template 'model.rb', "app/models/#{singular_table_name}.rb"
#views
copy_file 'erb/new.html.erb', "app/views/backend/#{file_name}/new.html.erb"
copy_file 'erb/edit.html.erb', "app/views/backend/#{file_name}/edit.html.erb"
template 'erb/index.html.erb', "app/views/backend/#{file_name}/index.html.erb"
template 'erb/_form.html.erb', "app/views/backend/#{file_name}/_form.html.erb"
end
end

上诉代码中用到的template方法的意思是复制模板到指定的地方.
比如自动生成model.下面这些代码是我自定义的model模板.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require 'uuidtools'

class <%= singular_table_name.capitalize %> < ApplicationRecord
enum status: [:archived, :active]

#validates
validates :TODO, presence: true, length: { maximum: 50 }
validates :TODO, numericality: true

#scope
default_scope { where("status>?", <%= singular_table_name.capitalize %>.statuses[:archived]) }
scope :name_like, ->(name){ where "name like ? ", "%#{sanitize_sql_like(name)}%" } #防sql注入

#假删除
def self.delete(<%= singular_table_name %>)
<%= singular_table_name %>.status = :archived
<%= singular_table_name %>.save
end

#设置属性值
def self.set_attribute(<%= singular_table_name %>_params)
<%= singular_table_name %> = <%= singular_table_name.capitalize %>.new(<%= singular_table_name %>_params)
<%= singular_table_name %>.sid = UUIDTools::UUID.timestamp_create
<%= singular_table_name %>.status = :active
<%= singular_table_name %>
end
end

上面model模板中用到的 singular_table_name 以及 table_name 等这些方法都在 Rails::Generators::NamedBase 这个类下.
定义完模板后就可以像使用scaffold一样使用我们的自定义generator了.

1
2
rails generate opendoc_scaffold groups
rails generate opendoc_scaffold members

简介

在我们的项目中如果不对url进行模糊处理,通常的url会像下面这样子:
http://localhost:3000/backend/groups/1/edit
http://localhost:3000/backend/groups/1/interfaces/1/edit
这直接暴露了数据库里有多少条记录.为了降低风险,我们需要借助friendly_id这个gem对url进行模糊处理.
处理的结果应该是下面这样:
http://localhost:3000/backend/groups/0f7094c6-ec6e-11e6-a542-7cd1c3f0bed9/edit
http://localhost:3000/backend/groups/0f7094c6-ec6e-11e6-a542-7cd1c3f0bed9/interfaces/48e86a4e-ec6e-11e6-a542-7cd1c3f0bed9/edit

实现过程

添加friendly_id这个gem

1
2
#Gemfile
gem 'friendly_id', '~> 5.1.0'

按照官方教程进行初始化:

1
2
rails g friendly_id
rails db:migrate
先来实现编辑group的url模糊处理
1
2
3
#先为groups表添加slug字段
rails g migration add_column_slug_to_groups slug:string:uuiq
rails db:migrate

然后修改group model

1
2
3
4
#app/modles/group.rb
#添加如下代码
extend FriendlyId
friendly_id :sid, use: :slugged #使用groups中的sid字段作为slug

然后controller中所有查询都通过friendly

1
2
3
#app/controllers/backend/groups_controller.rb
#比如:
@group = Group.friendly.find(params[:id])

完成以上操作后即实现了http://localhost:3000/backend/groups/1/edit这个url的模糊处理.
查看http://localhost:3000/backend/groups的html你会发现 编辑 对应的href的值已经模糊处理,但是删除的href还是没处理的状态.这个时候修改groups/index.html.erb

1
2
3
4
#app/views/backend/groups/index.html.erb
<%= link_to '删除', backend_group_path(group.id), method: :delete, data: { confirm: '确定删除吗?' }, class: 'edit' %>

<%= link_to '删除', backend_group_path(group.sid), method: :delete, data: { confirm: '确定删除吗?' }, class: 'edit' %>
处理interface

如果现在什么都不做,直接打开interface,你会发现interface的首页无法正常显示数据了.因为用于查询的group_id由数字变成了一长串的字符串.
接下来我们修复这个问题.
先按照修改group的方式修改interface使用friendly.因为group和interface有has_many关联,所以需要一些特殊处理!

1
2
3
#先为interfaces添加groups的sid外键关联
rails g migration add_column_group_sid_to_interfaces group_sid:string:{50}
rails db:migrate

然后修改set_attribute方法传入 params[:group_id]

1
2
3
4
5
6
7
8
#app/models/intreface.rb
def self.set_attribute(interface_params, group_sid)
inter = Interface.new(interface_params)
inter.sid = UUIDTools::UUID.timestamp_create
inter.group_sid = group_sid
inter.status = 1
inter
end

这样处理后,新增interface的时候,就会把group的sid插入到interface的group_sid字段中.

其它

也可通过重写model的to_param方法模糊处理url,比如

1
2
3
def to_param
"#{id}_slfkjsdf"
end

以上就是friendly_id的简单用法.如有不对的地方欢迎指正!

错误描述:

1
Your Ruby version is 2.0.0, but your Gemfile specified 2.3.0

排错过程:

查看ruby版本

1
2
✘ ⮀ -> ⮀ openapi ⮀ ruby-2.3.0 ⮀ ⭠ master± ⮀ ruby -v
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]

查看当前使用的ruby版本

1
2
3
4
5
6
7
8
9
10
11
12
13
 -> ⮀ openapi ⮀ ruby-2.3.0 ⮀ ⭠ master± ⮀ rvm list

rvm rubies

ruby-2.0.0-p247 [ x86_64 ]
ruby-2.2.0 [ x86_64 ]
ruby-2.2.1 [ x86_64 ]
=> ruby-2.3.0 [ x86_64 ]
* ruby-2.3.1 [ x86_64 ]

# => - current
# =* - current && default
# * - default

错误解决:
重新设置当前使用的ruby版本,问题解决!

1
rvm use 2.3.0

问题原因:
不明白!为何rvm已经设置了当前使用的ruby版本是2.3.0,但是还要重新设置一下!

2016稍纵即逝!
回顾这一年有喜有悲又有无奈!好在过去的都已过去!
展望2017,新的朝阳已经升起,怎能不为自己做一个新的新年规划呢!
在新的一年里:
至少读两本文学书!
学一门新技术语言!
至少看两本技术书籍!
坚持写技术Blog!
坚持看技术blog!
坚持使用默默背单词!
坚持锻炼身体!

编写测试的指导方针

与应用代码相比,如果测试代码特别简短,倾向于先编写测试;
如果对想实现的功能不是特别清楚,倾向于先编写应用代码,然后再编写测试,并改进实现方式;
安全是头等大事,保险起见,要为安全相关的功能线编写测试;
只要发现一个问题,就编写一个测试重现这种问题,避免回归,然后再编写应用代码修正问题;
尽量不为以后可能修改的代码(例如HTML结构的细节)编写测试;
重构之前要编写测试,集中测试容易出错的代码.
在实际的开发中,根据上述方针,我们一般先编写控制器和模型测试,然后再编写集成测试(测试模型、试视图和控制器在一起时的表现)。如果应用代码很容易出错,或者经常变动(视图就是这样),我们就完全不测试。

assert_select

test中的辅助函数,用于检测html中是否有指定的html标签,这个方法有时也叫”选择符”
assert_select ‘title’,’Home I Ruby on Rails Tutorial Sample App’ #含义: 检测有没有< title >标签,以及其中的内容是不是 “Home I Ruby on Rails Tutorial Sample App”
一些用法:
| 代码 | 匹配HTML |
| ————- |:————-:|
| assert_select ‘div’ | < div>foobar< /div> |
| assert_select ‘div’,’foobar’ | < div>foorbar< /div> |
| assert_select ‘div.nav’ | < div class=’nav’>foorbar< /div> |
| assert_select ‘div#profile’ | < div id=’profile’>foorbar< /div> |
| assert_select ‘div[name=yo]’ | < div name=’yo’>hey< /div> |
| assert_select “a[href=?]”,’/‘,count:1 | < div href=’/‘>foo< /div> |
| assert_select “a[href=?]”,’/‘,text: “foo” | < div href=’/‘>foo< /div> |

选择这样一个input标签的assert_select写法为:
< input id=”email” name=”email” type=”hidden” value=”a@mail.com“ / >
assert_select “input[name=email][type=hidden][value=?]”, ‘a@mail.com

provide 帮助方法
1
2
3
4
5
6
7
<% provide(:title, "Home") %>

<html>
<head>
<title><%= yield(:title) %></title>
</head>
</html>
使用git开发的步骤

有新的需求时,创建一个分支,再分支上进行开发,开发完成后合并到主分支.

1
2
git checkout master #切换到master分支
git merge static-pages #把static-pages分支merge到master分支
ps 命令
1
2
ps aux  #查看系统进程
ps aux | grep spring #查找spring
Guard的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 # Defines the matching rules for Guard.
guard :minitest, spring: true, all_on_start: false do
watch(%r{^test/(.*)/?(.*)_test\.rb$})
watch('test/test_helper.rb') { 'test' }
watch('config/routes.rb') { integration_tests }
watch(%r{^app/models/(.*?)\.rb$}) do |matches|
"test/models/#{matches[1]}_test.rb"
end
watch(%r{^app/controllers/(.*?)_controller\.rb$}) do |matches|
resource_tests(matches[1])
end
watch(%r{^app/views/([^/]*?)/.*\.html\.erb$}) do |matches|
["test/controllers/#{matches[1]}_controller_test.rb"] +
integration_tests(matches[1])
end
watch(%r{^app/helpers/(.*?)_helper\.rb$}) do |matches|
integration_tests(matches[1])
end
watch('app/views/layouts/application.html.erb') do
'test/integration/site_layout_test.rb'
end
watch('app/helpers/sessions_helper.rb') do
integration_tests << 'test/helpers/sessions_helper_test.rb'
end
watch('app/controllers/sessions_controller.rb') do ['test/controllers/sessions_controller_test.rb','test/integration/users_login_test.rb']
end
watch('app/controllers/account_activations_controller.rb') do 'test/integration/users_signup_test.rb'
end
watch(%r{app/views/users/*}) do
resource_tests('users') +
['test/integration/microposts_interface_test.rb']
end
end

# Returns the integration tests corresponding to the given resource.
def integration_tests(resource = :all) if resource == :all
Dir["test/integration/*"] else
Dir["test/integration/#{resource}_*.rb"] end
end

# Returns the controller tests corresponding to the given resource.
def controller_test(resource) "test/controllers/#{resource}_controller_test.rb"
end
# Returns all tests for the given resource.
def resource_tests(resource)
integration_tests(resource) << controller_test(resource)
end
#下面这行会让 Guard 使用 Rails 提供的 Spring 服务器减少加载时间,而且启动时不运行整个测试组件。
guard :minitest, spring: true, all_on_start: false do
#使用 Guard 时,为了避免 Spring 和 Git 发生冲突,应该把 spring/ 文件夹加到 .gitignore 文件中,让 Git 忽 略这个文件夹。
bang方法约定

方法名后加一个感叹号的方法称为”炸弹”(bang)方法.bang方法的含义是 方法处理的结果会改变对象的值.比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2.3.1 :001 > a = %w(2 3 5)
=> ["2", "3", "5"]
2.3.1 :002 > a.sort
=> ["2", "3", "5"]
2.3.1 :003 > a = %w(5 9 2)
=> ["5", "9", "2"]
2.3.1 :004 > a.sort
=> ["2", "5", "9"]
2.3.1 :005 > a
=> ["5", "9", "2"]
2.3.1 :006 > a.sort!
=> ["2", "5", "9"]
2.3.1 :007 > a
=> ["2", "5", "9"]
数组的 push | <<

为数组添加元素可以使用 push方法,也可以使用 << 运算符.

1
2
2.3.1 :008 > a << 'bbb' << 'dddd'
=> ["2", "5", "9", "bbb", "dddd"]
map块变量调用方法简写形式 &
1
2
3
4
2.3.1 :011 > a.map {|item| item.upcase }
=> ["2", "5", "9", "BBB", "DDDD"]
2.3.1 :012 > a.map(&:downcase)
=> ["2", "5", "9", "bbb", "dddd"]
hash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#几种写法 及 多层嵌套后的hash取值方式
2.3.1 :001 > a = { "a1" => 1, "a2" => 2 }
=> {"a1"=>1, "a2"=>2}
2.3.1 :002 > b = { :b1 => 1, :b2 => 2 }
=> {:b1=>1, :b2=>2}
2.3.1 :003 > c = { c1: 1, c2: 2 }
=> {:c1=>1, :c2=>2}

#多层嵌套后的hash取值
2.3.1 :004 > d = { d1: { dd1: { ddd1: 6, ddd2: 7}, dd2: 4 }, d2: 3 }
=> {:d1=>{:dd1=>{:ddd1=>6, :ddd2=>7}, :dd2=>4}, :d2=>3}

2.3.1 :011 > d[:d1]
=> {:dd1=>{:ddd1=>6, :ddd2=>7}, :dd2=>4}
2.3.1 :012 > d[:d1][:dd1]
=> {:ddd1=>6, :ddd2=>7}
2.3.1 :013 > d[:d1][:dd1][:ddd2]
=> 7

#hash的merge方法
2.3.1 :018 > a
=> {"a1"=>1, "a2"=>2}
2.3.1 :019 > b
=> {:b1=>1, :b2=>2}
2.3.1 :020 > a.merge b
=> {"a1"=>1, "a2"=>2, :b1=>1, :b2=>2}

#函数调用时,如果哈希是最后一个参数,可以省略花括号.比如:
stylesheet_link_tag 'application', { media: 'all', 'data-turbolinks-track': 'reload'}
stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
字符串、数组、hash、Range的声明
1
2
3
4
5
6
7
8
a = ''
a = String.new
a = String.new('dd')
b = []
b = Array.new
c = {}
c = Hash.new
d = Range.new(0,2) #声明一个范围
预处理引擎的执行顺序

从右向左执行
foobar.js.erb.coffee #先通过coffee处理,再通过ruby处理

路由的 _path 和 _url

root_path -> ‘/‘
root_url -> ‘http://www.example.com/'
约定 只有重定向使用_url形式,其余都使用_path形式.(因为HTTP标准严格要求重定向的URL必须完整.不过在大多数浏览器中,两种形式都可以正常使用)

测试方法 assert_template

检查路由是否使用正确的视图渲染
get root_path
assert_template ‘static_pages/home’
#请求root路由,看root路由是否使用了static_pages下的home视图渲染.

沙盒模式

rails console –sandbox
在沙盒模式下做的所有修改都不会影响实际数据.
在控制台下可以使用 reload方法重新加载环境

ActiveRecord的一些方法

user.new #在内存中创建对象
user.save #保存到数据库,save方法会返回true和false
以上两步和
user.create #create方法会返回对象的实例
等效.
user.valid? #只检测对象是否有效
user.destroy #create方法的逆操作,也会返回对象的实例.(销毁的对象还在内存中)
user.update_attributes(name: ‘The Dude’, email: ‘dude@abides.org‘) #更新name和email属性,返回ture和false.该方法无法跳过验证
user.update_attribute(:name, ‘El Duderino’) #更新单个属性,该方法可以跳过验证
User.find(params[:id]).destroy #可以再查询到的结果上直接调用destroy方法删除对象,物理删除…

Model的验证

validate :email, format: { with: // } #validate可以有format参数,提供正则验证

1
2
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: {maximum: 255 }, format: {with: VALID_EMAIL_REGEX }
正则表达式 /i,/g,/m,/gi,/ig 区别

/i (忽略大小写)
/g (全文查找出现的所有匹配字符)
/m (多行查找)
/gi(全文查找、忽略大小写)
/ig(全文查找、忽略大小写)

产生7位随机的字符

(‘a’..’z’).to_a.shuffle[0..7].join

dup方法复制一个对象

duplicate_user = @user.dup

model的 before_save 方法
1
2
3
4
class User < ApplicationRecord
before_save { self.email = email.downcase } #在执行save方法前,先把email转换为小写
validates :name, presence: true, length: { maximum: 50 }
end
has_secure_password 方法
1
2
3
calss User < ApplicationRecord
has_secure_password
end

在模型中调用了该方法后,会自动添加以下功能:
在数据库中的password_digest列存储安全的密码哈希值;
获得一对虚拟属性,password和password_confirmation,而且创建用户对象时会执行存在性验证和匹配验证;
获得authenticate方法,如果密码正确,返回对应的用户对象,否则返回false;
这个方法需要表有 password_digest字段.

可以根据环境的不同展示debug信息
1
2
3
<div>
<% debug(params) if Rails.env.development? % >
</div>
在指定的环境下执行命令
1
2
3
4
rails console test #在test环境下启用rails console
rails server --enviroment production #在production环境下运行服务
rails db:migrate RAILS_ENV=production #迁移production环境数据库
#控制台,服务器,迁移命令 这几个命令中都可以使用RAILS_ENV=<env>,比如: RAILS_ENV=production rails server
健壮参数
1
params.require(:user).permit(:name,:email,:password,:password_confirmation)
model的erros对象
1
2
3
user.errors.full_messages #查看所有错误信息,是一个数组
user.errors.empty? #判断是否存在错误信息
user.errors.any? #判断是否存在错误信息,可以empty?含义相反
helper的pluralize方法

该方法返回正确的单复数形式

1
2
helper.pluralize(1,"error") # 1 error
helper.pluralize(2,"error") # 2 errors
redirect_to

redirect_to @user
redirect_to user_url(@user)
以上两种形式的含义相同

||= 或等运算符

a ||= b
相当于 a = a || b
当a为真时返回a的值,当a为假时返回b的值

if(a=b)
1
2
3
4
5
6
7
a = 1
b = nil
if a = b
puts a
else
puts 3
end

上面的代码意思是当b存在时,把b赋给a,然后打印a,当b不存在时,打印3

activerecord的new_record?方法

User.new.new_record? # true
User.first.new_record? #false
用于检测对象是新创建的还是存在于数据库中.

model的validates允许为空的验证
1
validates :password, presence: true, length: {minimum: 6}, allow_nil: true
权限验证使用过滤器实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ApplicationController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]

private
def loggent_in_user
unless logged_in?
flash[:danger] = '.....'
redirect_to login_url
end
end
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless @user == current_user
end
end

删除用户的链接只有管理员才能看到

1
2
3
<% if current_user.admin? && !current_user?(user) % >
<% link_to "delete", user, method: :delete, data: {confirm: 'You sure?'} % >
<% end % >

为了限制只有管理员才能删除用户信息,需要这样做

1
2
3
4
5
6
7
8
class UserController < ApplicationController
before_action :admin_user, only: :destroy

private
def admin_user
redirect_to(root_url) unless current_user.admin?
end
end

在数据保存,创建之前都有相应的过滤器
比如: before_create, before_save 等

send方法
1
2
3
4
5
6
7
8
2.3.1 :004 > a = [1,2,3]
=> [1, 2, 3]
2.3.1 :005 > a.length
=> 3
2.3.1 :006 > a.send(:length)
=> 3
2.3.1 :007 > a.send("length")
=> 3

可见send方法可以在对象上调用以字符串或符号方式传入的参数的方法

flash

falsh[:info] = “111” #用在redirect_to中,数据存在session中
falsh.now[:danger] = “111” #用在render中,数据存在request中

has_many 和 belongs_to

当micropost模型和 user模型进行 belongs_to/has_many关联后会获得这些方法:
micropost.user #返回与微博关联的用户对象
user.microposts #返回用户发布的所有微博
user.microposts.create(arg) #创建一篇user发布的微博
user.microposts.create!(arg) #创建一篇user发布的微博(失败时抛出异常)
user.microposts.build(arg) #返回一个user发布的新微博对象
user.microposts.find_by(id:1) #查找user发布的一篇微博,而且微博的id为1

1
2
3
4
5
6
class Micropost < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :microposts, dependent: :destroy #dependent: :destroy 的作用是再用户被删除的时候,把这个用户发布的微博也删除.
end
scope
1
2
3
4
5
class Micropost < ApplicationRecord
belongs_to :user
default_scope -> { order(create_ar: :desc) } #默认的scope
scope :tmp, -> { }
end
Proc(procedure) 匿名函数

-> 接受一个代码块,返回一个Proc,然后再这个Proc上调用call方法执行其中代码.

1
2
3
4
5
6
7
8
2.3.1 :002 > a = -> { puts 'b' }
=> #<Proc:0x007f9f338bfc98@(irb):2 (lambda)>
2.3.1 :003 > a.call
b
=> nil
2.3.1 :004 > -> { puts 'c' }.call
c
=> nil

render传递参数

1
2
3
4
5
6
7
8
<%= for_for(@micropost) do |f|  % >
<%= render 'shared/error_mesages', object: f.object % >
<% end % >

#app/views/shared/_error_messages.html
<% if object.errors.any? % >
<div id="error" >1 <\/ div >
< % end % >

在启动hexo服务时遇到该问题.

1
2
3
4
5
6
hexo s
[Error: Module did not self-register.]
{ [Error: Cannot find module './build/default/DTraceProviderBindings'] code:'MODULE_NOT_FOUND' }
{ [Error: Cannot find module './build/Debug/DTraceProviderBindings'] code:'MODULE_NOT_FOUND' }
INFO Start processing
INFO Hexo is running at http://localhost:4000/. Press Ctrl+C to stop.

分析原因,应该是昨天安装了iTerm后使用了zsh,然后好多命令都找不到了…然后各种幺蛾子问题都出来了!!!
这是因为mac默认使用的是bash,现在设置成zsh后,好多命令都不存在了…最简单的方式是就重装一次!!!!
我再尝试 升级 nvm

1
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.4/install.sh | bash

然后重装 node和hexo后解决了该问题

1
2
nvm install node
npm install hexo --no-optional