模拟jdc部署环境,为node项目部署提供解决方案

有部署node项目操作过jdc服务器的童鞋们,可能都会遇到线上静态资源与本地静态资源不一致的问题,主要是由于我们部署的node项目的域名是二级目录,例jdc.jd.com/svg/,这样就导致相对路径指向jdc.jd.com,而不是目标jdc.jd.com/svg。

为了解决这个问题,我们通常是通过配置process.env.NODE_ENV的值,分开发环境dev,以及发布环境production,传入不同的指向路径,让静态资源路径与开发环境一致,但这一点还不够,切换到production改变静态资源路径,会导致本地无法正常预览,那么我们还需要配置nginx+node与线上保持一致的发布预览环境,从而达到发布及稳定,减少发布过程中浪费时间,难调试的问题。

以下是大概的解决方案,构建图:

Alt text

mac下配置Nginx

Mac使用brew安装Nginx、MySQL、PHP的LAMP开发环境

Nginx配置文件详解

Nginx配置文件详解

Nginx反向代理处理静态页面

nginx反向代理处理静态页面

Nginx+Node常见问题

1.Mac 启动 nginx 失败,提示地址已被占用 (48: Address already in use)

启动Nginx

1
sudo nginx

报错

1
2
3
4
5
6
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] still could not bind()

对Nginx玩的比较少的人,可能会认为,这是一个错误,但其实是本机Nginx自启,一直再启动,没法重启,可以先nginx -s stopnginx -s reload重启。

nginx常用命令:

1
2
3
sudo nginx #打开 nginx
nginx -s reload|reopen|stop|quit #重新加载配置|重启|停止|退出 nginx
nginx -t #测试配置是否有语法错误

Nginx配置文件详解

Nginx服务器nginx.conf的配置文件说明, 部分注释收集与网络。

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
#运行用户
user www-data;
#启动进程,通常设置成和cpu的数量相等
worker_processes 1;

#全局错误日志及PID文件
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

#工作模式及连接数上限
events {
use epoll; #epoll是多路复用IO(I/O Multiplexing)中的一种方式,但是仅用于linux2.6以上内核,可以大大提高nginx的性能
worker_connections 1024;#单个后台worker process进程的最大并发链接数
# multi_accept on;
}

#设定http服务器,利用它的反向代理功能提供负载均衡支持
http {
#设定mime类型,类型由mime.type文件定义
include /etc/nginx/mime.types;
default_type application/octet-stream;
#设定日志格式
access_log /var/log/nginx/access.log;

#sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,对于普通应用,
#必须设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,以平衡磁盘与网络I/O处理速度,降低系统的uptime.
sendfile on;
#tcp_nopush on;

#连接超时时间
#keepalive_timeout 0;
keepalive_timeout 65;
tcp_nodelay on;

#开启gzip压缩
gzip on;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";

#设定请求缓冲
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

#设定负载均衡的服务器列表
upstream mysvr {
#weigth参数表示权值,权值越高被分配到的几率越大
#本机上的Squid开启3128端口
server 192.168.8.1:3128 weight=5;
server 192.168.8.2:80 weight=1;
server 192.168.8.3:80 weight=6;
}


server {
#侦听80端口
listen 80;
#定义使用www.xx.com访问
server_name www.xx.com;

#设定本虚拟主机的访问日志
access_log logs/www.xx.com.access.log main;

#默认请求
location / {
root /root; #定义服务器的默认网站根目录位置
index index.php index.html index.htm; #定义首页索引文件的名称

fastcgi_pass www.xx.com;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
include /etc/nginx/fastcgi_params;
}

# 定义错误提示页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /root;
}

#静态文件,nginx自己处理
location ~ ^/(images|javascript|js|css|flash|media|static)/ {
root /var/www/virtual/htdocs;
#过期30天,静态文件不怎么更新,过期可以设大一点,如果频繁更新,则可以设置得小一点。
expires 30d;
}
#PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置.
location ~ \.php$ {
root /root;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /home/www/www$fastcgi_script_name;
include fastcgi_params;
}
#设定查看Nginx状态的地址
location /NginxStatus {
stub_status on;
access_log on;
auth_basic "NginxStatus";
auth_basic_user_file conf/htpasswd;
}
#禁止访问 .htxxx 文件
location ~ /\.ht {
deny all;
}

}
}

以上是一些基本的配置,使用Nginx最大的好处就是负载均衡

如果要使用负载均衡的话,可以修改配置http节点如下:

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
#设定http服务器,利用它的反向代理功能提供负载均衡支持
http {
#设定mime类型,类型由mime.type文件定义
include /etc/nginx/mime.types;
default_type application/octet-stream;
#设定日志格式
access_log /var/log/nginx/access.log;

#省略上文有的一些配置节点

#。。。。。。。。。。

#设定负载均衡的服务器列表
upstream mysvr {
#weigth参数表示权值,权值越高被分配到的几率越大
server 192.168.8.1x:3128 weight=5;#本机上的Squid开启3128端口
server 192.168.8.2x:80 weight=1;
server 192.168.8.3x:80 weight=6;
}

upstream mysvr2 {
#weigth参数表示权值,权值越高被分配到的几率越大

server 192.168.8.x:80 weight=1;
server 192.168.8.x:80 weight=6;
}

#第一个虚拟服务器
server {
#侦听192.168.8.x的80端口
listen 80;
server_name 192.168.8.x;

#对aspx后缀的进行负载均衡请求
location ~ .*\.aspx$ {

root /root; #定义服务器的默认网站根目录位置
index index.php index.html index.htm; #定义首页索引文件的名称

proxy_pass http://mysvr ;#请求转向mysvr 定义的服务器列表

#以下是一些反向代理的配置可删除.

proxy_redirect off;

#后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m; #允许客户端请求的最大单文件字节数
client_body_buffer_size 128k; #缓冲区代理缓冲用户端请求的最大字节数,
proxy_connect_timeout 90; #nginx跟后端服务器连接超时时间(代理连接超时)
proxy_send_timeout 90; #后端服务器数据回传时间(代理发送超时)
proxy_read_timeout 90; #连接成功后,后端服务器响应时间(代理接收超时)
proxy_buffer_size 4k; #设置代理服务器(nginx)保存用户头信息的缓冲区大小
proxy_buffers 4 32k; #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置
proxy_busy_buffers_size 64k; #高负荷下缓冲大小(proxy_buffers*2)
proxy_temp_file_write_size 64k; #设定缓存文件夹大小,大于这个值,将从upstream服务器传

}

}
}

参考资料:

Mongoose 参考手册 -- 转载

Mongoose 是什么?

一般我们不直接用MongoDB的函数来操作MongoDB数据库 Mongose就是一套操作MongoDB数据库的接口.

Schema

一种以文件形式存储的数据库模型骨架,无法直接通往数据库端,也就是说它不具备对数据库的操作能力.可以说是数据属性模型(传统意义的表结构),又或着是“集合”的模型骨架

1
2
3
4
5
6
7
8
9
/* 定义一个 Schema */
var mongoose = require("mongoose");

var TestSchema = new mongoose.Schema({
name : { type:String },//属性name,类型为String
age : { type:Number, default:0 },//属性age,类型为Number,默认为0
time : { type:Date, default:Date.now },
email: { type:String,default:''}
});

上面这个 TestSchema包含4个属性 [name, age, time, email]

Model

由Schema构造生成的模型,除了Schema定义的数据库骨架以外,还具有数据库操作的行为,类似于管理数据库属性、行为的类

1
2
3
4
var db = mongoose.connect("mongodb://127.0.0.1:27017/test");

// 创建Model
var TestModel = db.model("test1", TestSchema);

test1 数据库中的集合名称, 不存在会创建.

Entity

由Model创建的实体,使用save方法保存数据,Model和Entity都有能影响数据库的操作,但Model比Entity更具操作性

1
2
3
4
5
6
7
var TestEntity = new TestModel({
name : "Lenka",
age : 36,
email: "lenka@qq.com"
});
console.log(TestEntity.name); // Lenka
console.log(TestEntity.age); // 36

游标

MongoDB 使用游标返回find的执行结果.客户端对游标的实现通常能够对最终结果进行有效的控制。可以限制结果的数量,略过部分结果,根据任意键按任意顺序的组合对结果进行各种排序,或者是执行其他一些强的操作。

ObjectId

存储在mongodb集合中的每个文档(document)都有一个默认的主键_id,这个主键名称是固定的,它可以是mongodb支持的任何数据类型,默认是ObjectId。

ObjectId是一个12字节的 BSON 类型字符串。按照字节顺序,依次代表:
4字节:UNIX时间戳
3字节:表示运行MongoDB的机器
2字节:表示生成此_id的进程
3字节:由一个随机数开始的计数器生成的值

Node.js 中

package.json 中加入”mongoose”: “*” 字段
npm install 安装依赖.

var mongoose = require(“mongoose”);
var db = mongoose.connect(“mongodb://localhost:27017/test”);
然后引用

API

var mongoose = require(“mongoose”);
var db = mongoose.connect(“mongodb://localhost:27017/test”);

db - 数据库操作

1.挂接数据库连接事件,参数1: 也可以是error.

db.connection.on(‘open’, callback);

Schema - 表结构

1.构造函数

new mongoose.Schema( { name:{type:String}, age:{type:Number, default:10} } )

2.添加属性

Schema.add( { name: ‘String’, email: ‘String’, age: ‘Number’ } )

3.有时候Schema不仅要为后面的Model和Entity提供公共的属性,还要提供公共的方法

Schema.method( ‘say’, function(){console.log(‘hello’);} )
//这样Model和Entity的实例就能使用这个方法了

4.添加静态方法

Schema.static( ‘say’, function(){console.log(‘hello’);} )
//静态方法,只限于在Model层就能使用

5.追加方法

Schema.methods.say = function(){console.log(‘hello’);};
//静态方法,只限于在Model层就能使用

model - 文档操作

1.构造函数, 参数1:集合名称, 参数2:Schema实例

db.model(“test1”, TestSchema );

2.查询, 参数1忽略,或为空对象则返回所有集合文档

model.find({}, callback);

model.find({},field,callback);
过滤查询,参数2: {‘name’:1, ‘age’:0} 查询文档的返回结果包含name , 不包含age.(_id默认是1)

model.find({},null,{limit:20});
过滤查询,参数3: 游标操作 limit限制返回结果数量为20个,如不足20个则返回所有.

model.findOne({}, callback);
查询找到的第一个文档

model.findById(‘obj._id’, callback);
查询找到的第一个文档,同上. 但是只接受 __id 的值查询

3.创建, 在集合中创建一个文档

Model.create(文档数据, callback))

4.更新,参数1:查询条件, 参数2:更新对象,可以使用MondoDB的更新修改器

Model.update(conditions, update, function(error)

5.删除, 参数1:查询条件

Model.remove(conditions,callback);

Entity - 文档操作

1.构造函数, 其实就是model的实例

new TestModel( { name:‘xueyou’, age:21 } );

2.创建, 在集合中创建一个文档.

Entity.save(callback);

修改器和更新器

更新修改器:

‘$inc’ 增减修改器,只对数字有效.下面的实例: 找到 age=22的文档,修改文档的age值自增1

Model.update({‘age’:22}, {’$inc’:{‘age’:1} } );
执行后: age=23

‘$set’ 指定一个键的值,这个键不存在就创建它.可以是任何MondoDB支持的类型.

Model.update({‘age’:22}, {’$set’:{‘age’:‘haha’} } );
执行后: age=‘haha’

‘$unset’ 同上取反,删除一个键

Model.update({‘age’:22}, {’$unset’:{‘age’:‘haha’} } );
执行后: age键不存在

数组修改器:

‘$push’ 给一个键push一个数组成员,键不存在会创建

Model.update({‘age’:22}, {’$push’:{‘array’:10} } );
执行后: 增加一个 array 键,类型为数组, 有一个成员 10

‘$addToSet’ 向数组中添加一个元素,如果存在就不添加

Model.update({‘age’:22}, {’$addToSet’:{‘array’:10} } );
执行后: array中有10所以不会添加

‘$each’ 遍历数组, 和 $push 修改器配合可以插入多个值

Model.update({‘age’:22}, {’$push’:{‘array’:{’$each’: [1,2,3,4,5]}} } );
执行后: array : [10,1,2,3,4,5]

‘$pop’ 向数组中尾部删除一个元素

Model.update({‘age’:22}, {’$pop’:{‘array’:1} } );
执行后: array : [10,1,2,3,4] tips: 将1改成-1可以删除数组首部元素

‘$pull’ 向数组中删除指定元素

Model.update({‘age’:22}, {’$pull’:{‘array’:10} } );
执行后: array : [1,2,3,4] 匹配到array中的10后将其删除

条件查询:

  • “$lt” 小于
  • “$lte” 小于等于
  • “$gt” 大于
  • “$gte” 大于等于
  • “$ne” 不等于

Model.find({“age”:{ “$get”:18 , “$lte”:30 } } );
查询 age 大于等于18并小于等于30的文档

或查询 OR:

  • ‘$in’ 一个键对应多个值
  • ‘$nin’ 同上取反, 一个键不对应指定值
  • “$or” 多个条件匹配, 可以嵌套 $in 使用
  • “$not” 同上取反, 查询与特定模式不匹配的文档

    Model.find({“age”:{ “$in”:[20,21,22.‘haha’]} } );
    查询 age等于20或21或21或’haha’的文档

    Model.find({“$or” : [ {‘age’:18} , {‘name’:‘xueyou’} ] });
    查询 age等于18 或 name等于’xueyou’ 的文档

类型查询:

null 能匹配自身和不存在的值, 想要匹配键的值 为null, 就要通过 “$exists” 条件判定键值已经存在
“$exists” (表示是否存在的意思)

Model.find(“age” : { “$in” : [null] , “exists” : true } );
查询 age值为null的文档

1
2
3
4
5
6
7
Model.find({name: {$exists: true}},function(error,docs){
//查询所有存在name属性的文档
});

Model.find({telephone: {$exists: false}},function(error,docs){
//查询所有不存在telephone属性的文档
});

正则表达式:

MongoDb 使用 Prel兼容的正则表达式库来匹配正则表达式

find( {“name” : /joe/i } )
查询name为 joe 的文档, 并忽略大小写

find( {“name” : /joe?/i } )
查询匹配各种大小写组合

查询数组:

Model.find({“array”:10} );
查询 array(数组类型)键中有10的文档, array : [1,2,3,4,5,10] 会匹配到

Model.find({“array[5]”:10} );
查询 array(数组类型)键中下标5对应的值是10, array : [1,2,3,4,5,10] 会匹配到

‘$all’ 匹配数组中多个元素

Model.find({“array”:[5,10]} );
查询 匹配array数组中 既有5又有10的文档

‘$size’ 匹配数组长度

Model.find({“array”:{“$size” : 3} } );
查询 匹配array数组长度为3 的文档

‘$slice’ 查询子集合返回

Model.find({“array”:{“$skice” : 10} } );
查询 匹配array数组的前10个元素

Model.find({“array”:{“$skice” : [5,10] } } );
查询 匹配array数组的第5个到第10个元素

where

用它可以执行任意javacript语句作为查询的一部分,如果回调函数返回 true 文档就作为结果的一部分返回

1
2
3
4
5
6
7
8
9
10
11
12
13
	find( {"$where" : function(){
for( var x in this ){
//这个函数中的 this 就是文档
}

if(this.x !== null && this.y !== null){
return this.x + this.y === 10 ? true : false;
}else{
return true;
}


} } )

简化版本

1
2
find( {"$where" :  "this.x + this.y === 10" } )
find( {"$where" : " function(){ return this.x + this.y ===10; } " } )

游标:

  • limit(3) 限制返回结果的数量,
  • skip(3) 跳过前3个文档,返回其余的
  • sort( {“username”:1 , “age”:-1 } ) 排序 键对应文档的键名, 值代表排序方向, 1 升序, -1降序

参考资料:

.gitignore 文件使用说明

我们在使用 Git 进行版本控制的时候,有些文件是无需纳入 Git 管理的,通常都是些自动生成的文件,像日志或者编译过程中创建的文件。我们可以创建一个名为 .gitignore的文件,列出要忽略的文件来解决这个问题。

来看一个简单的例子

1
2
3
# cat .gitignore
*.[oa]
*~

第一行告诉 Git 忽略所有以 .o 或 .a结尾的文件。一般这类对象文件和存档文件都是编译过程中出现的,我们用不着跟踪它们的版本。
第二行告诉 Git 忽略所有以波浪符(~)结尾的文件,许多文本编辑软件(比如 Emacs)都用这样的文件名保存副本。此外,你可能还需要忽略 log,tmp 或者 pid 目录,以及自动生成的文档等等。要养成一开始就设置好 .gitignore 文件的习惯,以免将来误提交这类无用的文件。

文件 .gitignore 的格式规范如下:
所有空行或者以注释符号 # 开头的行都会被 Git 忽略。
可以使用标准的 glob 模式匹配。
匹配模式最后跟反斜杠(/)说明要忽略的是目录。
要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。
星号(*)匹配零个或多个任意字符;
[abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);
问号(?)只匹配一个任意字符;
如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。

我们再看一个 .gitignore 文件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 忽略所有 .a 结尾的文件
*.a

# 但 lib.a 除外
!lib.a

# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO

# 忽略 build/ 目录下的所有文件
build/

# 会忽略 doc/notes.txt 但不包括 doc/server/notes.txt
doc/notes.txt

其他信息可以直接 man gitignore 查看。

此外推荐一个自动生成 .gitignore 文件的网址:http://www.gitignore.io/

对于iOS开发者,推荐一个 gist :https://gist.github.com/mmorey/6931793

买车前必备知识

买车文章大纲:

1.车型简介

2.买车前必备知识
油耗-百公里多少油耗(雪铁龙车型 8.3L/百公里,6~7左右最省油,控制在每公里5毛以内),性能保养

3.各车型品牌对比

准备资料:

通过.htaccess文件配置二级域名绑定子目录

一直使用万网的虚拟主机,但是发现只能绑定域名到根目录,因此想要添加一个新的网站只能使用 abc.com/blog 而不能用 blog.abc.com 这样的二级域名。可以通过 .htaccess 文件来解决该问题,那下面具体讲一下万网通过.htaccess文件配置二级域名绑定子目录。

第一步,增加.htaccess配置

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
# 开启功能  
RewriteEngine on

# 你要绑定的二级域名
RewriteCond %{HTTP_HOST} ^blog.abc.com$

# 把那个子目录指向要绑定的二级域名
# 这里以子目录blog目录为例
RewriteCond %{REQUEST_URI} !^/blog/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /blog/$1

#这里改成要绑定的二级域名和要绑定的子目录
RewriteCond %{HTTP_HOST} ^blog.abc.com$
RewriteRule ^(/)?$ blog/index.php [L]

# 你要绑定的二级域名
RewriteCond %{HTTP_HOST} ^case.abc.com$

# 配置case.abc.com域名
RewriteCond %{REQUEST_URI} !^/case/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /case/$1

RewriteCond %{HTTP_HOST} ^case.abc.com$
RewriteRule ^(/)?$ case/index.php [L]

第二步,增加域名解析

增加域名解析

第三步,绑定域名

增加域名解析

经过,上面三个步骤,就可以访问绑定的二级域名了。

参考资料:

webp实践探究

最近组内流行一股探讨webp的风潮,抱着实事求是的态度,本码农也开始了webp探究实践。先扫一下盲,提提WebP的优势。

WebP 的优势

WebP 格式是 Google 于2010年发布的一种支持有损压缩和无损压缩的图片文件格式,派生自图像编码格式 VP8。它具有较优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都非常优秀、稳定和统一。目前,知名网站 Youtube 、Facebook、Ebay 等均有使用 WebP格式。

WebP 集合了多种图片文件格式的特点,JPEG 适合压缩照片和其他细节丰富的图片,GIF 可以显示动态图片,PNG 支持透明图像,图片色彩非常丰富,而 WebP 则兼具上述优点,且较于它们还有更出色的地方。

据 Google 测试,无损压缩后的 WebP 比 PNG 文件少了 45% 的文件大小,即使 PNG 文件经过其他压缩工具压缩后,WebP 还是可以减少 28% 的文件大小。此外,与 JPEG 相比,在质量相同的情况下,WebP 格式图像的体积要比 JPEG 格式图像小 40%,而 WebP 在压缩方面比 JPEG 格式更优越。

Webp 转换工具

看了我组恩鑫大大的png 转 webp 的正确姿势,知道了WebP Converter工具转换webp的效率是比较高的。

webP Converter工具安装是这样的:

1
sudo brew install webp

转换webp命令是这样的:

1
cwebp -q 80 image.png -o image.webp

知道了,google提供的工具转换效率高,以及使用方式,鉴于工程化、批量化、敏捷开发的原则,我进行了如下猜想使其达到批量生产的目的:

猜测一: cwebp命令后面匹配多个参数进行批量转换

1
cwebp -q 80 image.png -o image.webp -q 80 logo.png -o logo.webp

抱着大大的希望,结果发现,只能转换最后一个图片,只能转换最后一个图片,只能转换最后一个图片,重要的事情说三遍,整个人不好了……

猜测二:通过child_process模块,开子进程,来执行格式webp转换

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
const child_process  = require("child_process");
const fs = require("fs");
const path = require("path");
const curPath = process.cwd();

function convertWep(opt){
var src = opt.src, quality = opt.quality || 60;

if(fs.existsSync(src)){
fs.readdir(src,(err,files) => {
if(err){console.log(err);return;}
files.forEach(function(filename){

var url = path.join(src,filename);

fs.stat(url,(err, stats) => {
if (err) throw err;

if(stats.isFile()){
//不支持bmp格式
if(/.*\.png|jpg|gif$/.test(url)){

var webpsrc = url.split('.')[0]+".webp";
childWebp(path.join(curPath,url),quality,path.join(curPath,webpsrc))
console.log(filename+" 转换webp格式成功");
}

}else if(stats.isDirectory()){
convertWep({
"src":url,
"quality":quality
})
}
});
});
});
}else{
console.log("给定的目录不存,读取不到文件");
return;
}
}

function childWebp(src , quality,webpsrc){
child_process.exec("cwebp -q "+quality+" "+src+" -o "+webpsrc,(error, stdout, stderr) => {
if (error) {
console.log(error.stack);
console.log('Error code: ' + error.code);
return ;
}
});
}

convertWep({
src:"./images/",
quality:60
})

转换结果:
child_process开进程转换结果

方法原理就是给每个图片单独开子进程进行转换,看结果可以达到批量转换不同质量的webp格式的目的,但是不够优雅,无法达到监测每个图片转换情况,无法监测图片转换完成,以及无法关闭所开的子进程。

gulp-webp转换工具

本着码农朋友不作死、不服输的精神,反复查阅资料找寻其他自动化转换webp工具,我发现了gulp-webp插件也可以达到批量转换,支持 PNGJPEGTIFF格式转换,并且转换率基本一致。

使用示例:

1
2
3
4
5
6
7
8
const gulp = require('gulp');
const webp = require('gulp-webp');

gulp.task('default', () =>
gulp.src('./images/*.{png,jpg,jpeg}')
.pipe(webp({quality: 60}))
.pipe(gulp.dest('./dist'))
);

简单看看,示例代码,感觉还是很优雅的,但使用时发现无法提供转换webp具体提示,这点糟透了:
执行结果:
child_process开进程转换结果

gulp-webp与WebP Converter转换率对比:
WebP Converter: 0.4.3
gulp-webp: 2.3.0
原图: bg.jpg (34.176K),test.png(141.071K)

类型 质量 gulp-webp WebP Converter
bg.jpg 90% 21.344K 21.344K
80% 10.250K 10.250K
70% 7.550K 7.550K
60% 6.856K 6.856K
50% 6.034K 6.034K
test.png 90% 48.390K 48.710K
80% 29.584K 29.584K
70% 23.240K 23.240K
60% 20.734K 20.734K
50% 18.332K 18.332K

对比结果得出,gulp-webp与WebP Converter转换率基本是一致的,于是乎猜想难道它们使用额转换原理一样。抱着这个猜想,开始了分析gulp-webp转换原理。

gulp-webp中 API 提到:

webp([options])

See the imagemin-webp options.

查看imagemin-webp模块中发现,其中转换模块最终是利用cwebp-bin实现,其原理也是对图片单开子进程实现转换:

1
2
3
4
5
6
7
8
9
10
var execFile = require('child_process').execFile;
var cwebp = require('cwebp-bin');

execFile(cwebp, ['input.png', '-o', 'output.webp'], function (err) {
if (err) {
throw err;
}

console.log('Image is converted!');
});

结合webp官网给出解释:

WebP includes the lightweight encoding and decoding library libwebp and the command line tools cwebp and dwebp for converting images to and from the WebP format, as well as tools for viewing, muxing and animating WebP images. The full source code is available on the download page.

最终结论,WebP Convertergulp-webp转换webp的核心模块都是cwebp-bin,实现原理、转换效率基本都是一样,所以在使用webp批量转换上,使用gulp-webp插件或前面我提到的第二种猜想方法,都可以达到目的,具体使用可结合个人喜好选用。

Webp 实例使用

本码农再次本着实事求是的心态,深挖总结webp在项目中实际使用。具体使用方案如下:

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
//注意此脚本需放在样式前面
checkWebp();

function checkWebp(){
var img = new Image();
img.onload = function(){
if(img.width > 0 && img.height > 0){
document.documentElement.className = "webp";
}
}
img.onerror = function(){}
img.src = "data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA"
}

<link rel="stylesheet" type="text/css" href="/201607/sns808/css/style.css">

<div class="wrapper"></div>

//样式代码片段
.wrapper{
position: relative;
width:pxTorem(750px);
overflow: hidden;
margin:0 auto;
background:image-url("bg.jpg") no-repeat;
background-size:cover;
}
.webp{
.wrapper{
background:image-url("bg.webp") no-repeat;
background-size:cover;
}
}

通过chrome控制台观察结果:

实践结果

通过浏览器的自动运算判断,只加载了对应使用的资源,从而达到减少加载资源、提高加载效率的目的。

参考资料:

Canvas实时处理Video预研

video简介

HTML5里引入的新标记 <video> 实现了HTML对视频播放和音频播放的原生支持,有了这种原生的HTML5视频播放器/音频播放器,我们不再需要flash技术,而直接能将视频/音频嵌入到了网页中。

由于部分浏览器存在不支持<video> 标签,可以尝试使用video.js.

如何嵌入视频

方式一:

1
2
3
<video src="http://www.webhek.com/~j/theora_testsuite/320x240.ogg" controls autoplay loop>
Your browser does not support the <code>video</code> element.
</video>

方式二:

1
2
3
4
5
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
您的浏览器不支持Video标签。
</video>

video常用属性

属性 描述
autoplay 如果出现该属性,则视频在就绪后马上播放。
controls 如果出现该属性,则向用户显示控件,比如播放按钮。
loop 如果出现该属性,则当媒介文件完成播放后再次开始播放。
muted 规定视频的音频输出应该被静音。
poster 规定视频下载时显示的图像,或者在用户点击播放按钮前显示的图像。
preload 如果出现该属性,则视频在页面加载时进行加载,并预备播放。如果使用 “autoplay”,则忽略该属性。
webkit-playsinline IOS允许内嵌播放在网页,因浏览器默认,全局打开。

video在IOS和Andriod使用情况

1.video在IOS环境下微信、sq以及uc使用情况
默认,全屏播放video,通过webkit-playsinline属性可以达到内嵌入网页播放,添加controls属性会导致出现video控制条.

2.video在Andriod环境下微信、sq以及uc使用情况
但凡检测到页面可播放的video元素,都会调用浏览器封装的播放器,全屏播放,不支持webkit-playsinline属性,发现qq域名拥有腾讯x5白名单,可以内嵌播放,默认不会调用全屏播放,但会自动添加浮动全屏按钮。

video转canvas

canvas中 drawImage() 方法可以在画布上绘制视频。想通过canvas绘制video视频的方式,来全兼容IOS和Andriod,发现还是无法解决Andriod中存在检测页面中video全屏播发的问题,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- video视频在ios中wx和sq里面可以通过drawImage实现截取视频,andriod中wx和sq,不管你禁用还是隐藏video都会自动启用系统自带播放器-->
<!-- 这里ios会有控制条,是因为加了controls,不加就可以取消-->
<video id="video" controls width="270" autoplay webkit-playsinline="true">
<source src="http://www.w3school.com.cn/example/html5/mov_bbb.mp4" type='video/mp4'>
<source src="http://www.w3school.com.cn/example/html5/mov_bbb.ogg" type='video/ogg'>
<source src="http://www.w3school.com.cn/example/html5/mov_bbb.webm" type='video/webm'>
</video>

<p>画布(每 20 毫秒,代码就会绘制视频的当前帧):</p>

<canvas id="myCanvas" width="540" height="270" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.
</canvas>

<script>
var video=document.getElementById("video");
var canvas=document.getElementById("myCanvas");
ctx=canvas.getContext('2d');

video.addEventListener('play', function() {var i=window.setInterval(function() {ctx.drawImage(video,0,0,540,270)},20);},false);
video.addEventListener('pause',function() {window.clearInterval(i);},false);
video.addEventListener('ended',function() {clearInterval(i);},false);

</script>

参考资料: