Java转码服务

记录用java实现转码服务的核心代码。功能包括:把视频转为mp4、把文档转为pdf、生成视频和文档的缩略图等。

ConvertService.java

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
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import lombok.Synchronized;

@Service
public class ConvertService {
@Autowired
ConvertConfig convertConfig;
@Autowired
ConvertTaskDao convertTaskDao;
@Autowired
ConvertHelper convertHelper;
@Autowired
RestTemplate restTemplate;

private static Thread convertThread = null;
private static Thread notifyThread = null;
private static ConcurrentLinkedQueue<ConvertTask> queueTasks = new ConcurrentLinkedQueue<>();
private static ConcurrentHashMap<String, ConvertTask> convertingTasks = new ConcurrentHashMap<>();
private static ConcurrentLinkedQueue<ConvertTask> notifyTasks = new ConcurrentLinkedQueue<>();

@Synchronized
public ConvertTask addTask(ConvertTask task) {
ConvertTask save = convertTaskDao.save(task);
queueTasks.add(save);
startConvert();
return task;
}

@Synchronized
private int doConvert() {
if (convertingTasks.size() >= 4) {
return 1;
}
ConvertTask task = queueTasks.poll();
if (task == null) {
return 10;
}
try {
if (convertingTasks.containsKey(task.getSrc())) {//重复
task.setStatus(ConvertTask.STATUS_IGNORE);
convertTaskDao.save(task);
return 0;
}
convertingTasks.put(task.getSrc(), task);
task.setStatus(ConvertTask.STATUS_START);
task.setStartAt(Instant.now());
convertTaskDao.save(task);

CompletableFuture<ConvertTask> process = convertHelper.process(task);
process.thenAccept((retTask) -> {
retTask.setStatus(ConvertTask.STATUS_FINISH);
retTask.setEndAt(Instant.now());
convertTaskDao.save(retTask);
convertingTasks.remove(task.getSrc());
notifyTasks.add(retTask);
startNotify();
System.out.println(retTask);
});
} catch(Exception e) {
e.printStackTrace();
}
return 0;
}

@Synchronized
private int doNotify() {
ConvertTask task = notifyTasks.poll();
if (task == null) {
return 10;
}
int sleep = 0;
try {
ResponseEntity<String> response = restTemplate.postForEntity(convertConfig.getNotify(), task, String.class);
String success = response.getBody();
System.out.println(success);
if (!StringUtils.isNullOrEmpty(success) && success.contains("success")) {
task.setStatus(ConvertTask.STATUS_NOTIFIED);
task.setNotify(LocalDateTime.now()+","+success);
} else {
task.setNotify(LocalDateTime.now()+",empty");
notifyTasks.add(task);
sleep = 1;
}
} catch(Exception e) {
e.printStackTrace();
task.setNotify(LocalDateTime.now()+","+e.getMessage());
notifyTasks.add(task);
sleep = 2;
}
convertTaskDao.save(task);
return sleep;
}

public void startConvert() {
if (convertThread != null) {
convertThread.interrupt();
return;
}
convertThread = new Thread(() -> {
while (convertThread == Thread.currentThread()) {
System.out.println("Convert "+convertThread + " running at " + LocalDateTime.now());
int sleep = doConvert();
try {
Thread.sleep(sleep*1000);
} catch (InterruptedException e) {
System.out.println("Convert "+Thread.currentThread() +" "+ e.getMessage());
}
}
System.out.println("Convert "+Thread.currentThread()+" stoped at " + LocalDateTime.now());
});
convertThread.start();
System.out.println("Convert "+convertThread+" started at " + LocalDateTime.now());

}

public void startNotify() {
if (notifyThread != null) {
notifyThread.interrupt();
return;
}
notifyThread = new Thread(() -> {
while (notifyThread == Thread.currentThread()) {
System.out.println("Notify "+notifyThread + " running at " + LocalDateTime.now());
int sleep = doNotify();
try {
Thread.sleep(sleep*1000);
} catch (InterruptedException e) {
System.out.println("Notify "+Thread.currentThread() +" "+ e.getMessage());
}
}
System.out.println("Notify "+Thread.currentThread()+" stoped at " + LocalDateTime.now());
});
notifyThread.start();
System.out.println("Notify "+notifyThread+" started at " + LocalDateTime.now());
}

@PostConstruct
private void init() {
List<ConvertTask> tasks = convertTaskDao.findByStatus(ConvertTask.STATUS_START);
tasks.forEach(task -> {
queueTasks.add(task);
});
tasks = convertTaskDao.findByStatus(ConvertTask.STATUS_INITIAL);
tasks.forEach(task -> {
queueTasks.add(task);
});
tasks = convertTaskDao.findByStatus(ConvertTask.STATUS_FINISH);
tasks.forEach(task -> {
notifyTasks.add(task);
});
startConvert();
startNotify();
System.out.println(convertConfig);
}

@PreDestroy
public void stop() {
if (convertThread != null) {
convertThread.interrupt();
convertThread = null;
}
if (notifyThread != null) {
notifyThread.interrupt();
notifyThread = null;
}
}
}

ConvertTaskDao.java

1
2
3
4
5
6
7
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;

public interface ConvertTaskDao extends JpaRepository<ConvertTask, Integer>{
List<ConvertTask> findByStatus(@Param("status") Integer status);
}

ConvertTask实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ConvertTask implements Serializable{
private static final long serialVersionUID = 1418649194096365375L;
//状态常量
public static final Integer STATUS_IGNORE = -1;
public static final Integer STATUS_INITIAL = 0;
public static final Integer STATUS_START = 1;
public static final Integer STATUS_FINISH = 2;
public static final Integer STATUS_NOTIFIED = 3;

@Id
Integer id;
String name;
Instant createdAt;
Instant startAt;
Instant endAt;
Integer status = STATUS_INITIAL;
String src;//源文件
String out;//输出文件
String img;//缩略图
String ret;//转换结果
String notify;//通知结果
}

ConvertHelper.java

基于CmdHelper.java实现,见:https://www.zybuluo.com/TedZhou/note/1716823

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
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.CompletableFuture;

import javax.imageio.ImageIO;

import org.apache.commons.exec.CommandLine;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ConvertHelper {
static final String EXT_PDF = ".pdf";
static final String EXT_MP4 = ".mp4";
static final String EXT_AVI = ".avi";
static final String EXT_DOC_STR = ".doc,.docx,.ppt,.pptx,.xls,.xlsx,";// 文档后缀
static final String EXT_VIDEO_FFMPEG_STR = ".avi,.mpg,.wmv,.3gp,.mov,.asf,.asx,.flv,.f4v,.vob,.mkv,.ts,";// ffmpeg能解码的文件格式
static final String EXT_VIDEO_MENCODER_STR = ".wmv9,.rm,.rmvb,.mpeg,";// 需要先mencoder解码的文件格式。.mpeg无法直接截取图片,所以需要中转

@Autowired
ConvertConfig convertConfig;

public static String getExt(String filename) {
if (StringUtils.isNullOrEmpty(filename)) return "";
int pos = filename.lastIndexOf(".");
if (pos < 0) return "";
return filename.substring(pos).toLowerCase();
}

public static boolean isDoc(String ext) {
return EXT_DOC_STR.contains(ext + ',');
}

public static boolean isVideo(String ext) {
return EXT_VIDEO_FFMPEG_STR.contains(ext + ',') || needMEncoder(ext);
}

public static boolean needMEncoder(String ext) {
return EXT_VIDEO_MENCODER_STR.contains(ext + ',');
}

public CompletableFuture<ConvertTask> process(ConvertTask task) {
CompletableFuture<ConvertTask> cf = new CompletableFuture<>();
new Thread(() -> {
String srcFile = task.getSrc();
String dotExt = getExt(srcFile);
String outFile = null;
CmdHandler cmdHandler = null;
FileOutputStream logStream = null;
try {
File logFile = new File(convertConfig.getLog() + srcFile + ".log");
logStream = new FileOutputStream(logFile);
if (isDoc(dotExt)) {
outFile = srcFile + EXT_PDF;
cmdHandler = doc2pdf(convertConfig.getSrc() + srcFile, convertConfig.getOut() + outFile, logStream);
if (cmdHandler.getException() == null) {
if (pdf2png(convertConfig.getOut() + outFile, convertConfig.getOut() + task.getSrc() + ".png")) {
task.setImg(task.getSrc() + ".png");
}
}
} else if (isVideo(dotExt)) {
if (needMEncoder(dotExt)) {
outFile = srcFile + EXT_AVI;
cmdHandler = video2avi(convertConfig.getSrc() + srcFile, convertConfig.getOut() + outFile, logStream);
task.setRet(cmdHandler.resultString());
if (cmdHandler.getException() != null) {
cf.complete(task);
return;
}
srcFile = outFile;
}
cmdHandler = video2jpg(convertConfig.getSrc() + srcFile, convertConfig.getOut() + task.getSrc() + ".jpg", logStream);
if (cmdHandler.getException() == null) {
task.setImg(task.getSrc() + ".jpg");
}
outFile = task.getSrc() + EXT_MP4;
cmdHandler = video2mp4(convertConfig.getSrc() + srcFile, convertConfig.getOut() + outFile, logStream);
} else {
task.setRet("-1:unsupport");
cf.complete(task);
return;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(logStream);
}

task.setOut(outFile);
task.setRet(cmdHandler.resultString());
if (cmdHandler.getException() != null) {
task.setOut(null);
}
cf.complete(task);
}).start();
return cf;
}

CmdHandler doc2pdf(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getUnoconv());
cmd.addArgument("-f");
cmd.addArgument("pdf");
cmd.addArgument("-o");
cmd.addArgument(outFile);
cmd.addArgument(srcFile);
return CmdHelper.run(cmd, cmdOut, 0);
}

boolean pdf2png(String srcFile, String outFile) {
try {
File file = new File(srcFile);
PDDocument doc;
doc = PDDocument.load(file);
PDFRenderer renderer = new PDFRenderer(doc);
BufferedImage image = renderer.renderImageWithDPI(1, 144); // Windows native DPI
BufferedImage tag = new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
tag.getGraphics().drawImage(image, 0, 0, 64, 64, null);
ImageIO.write(tag, "PNG", new File(outFile));
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}

CmdHandler video2mp4(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getFfmpeg());
cmd.addArgument("-i");
cmd.addArgument(srcFile);
cmd.addArgument("-ar");
cmd.addArgument("22050");
cmd.addArgument("-vcodec");
cmd.addArgument("libx264");
cmd.addArgument("-q:v");
cmd.addArgument("6");
cmd.addArgument("-r");
cmd.addArgument("25");
cmd.addArgument("-flags");
cmd.addArgument("+loop");
cmd.addArgument("-crf");
cmd.addArgument("24");
cmd.addArgument("-bt");
cmd.addArgument("256k");
cmd.addArgument("-af");
cmd.addArgument("volume=2");
cmd.addArgument("-y");
cmd.addArgument(outFile);
return CmdHelper.run(cmd, cmdOut, 0);
}

CmdHandler video2jpg(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getFfmpeg());
cmd.addArgument("-i");
cmd.addArgument(srcFile);
cmd.addArgument("-f");
cmd.addArgument("image2");
cmd.addArgument("-ss");
cmd.addArgument("15");
cmd.addArgument("-t");
cmd.addArgument("0.001");
cmd.addArgument("-s");
cmd.addArgument("64x64");
cmd.addArgument("-y");
cmd.addArgument(outFile);
return CmdHelper.run(cmd, cmdOut, 0);
}

CmdHandler video2avi(String srcFile, String outFile, OutputStream cmdOut) {
CommandLine cmd = new CommandLine(convertConfig.getMencoder());
cmd.addArgument(srcFile);
cmd.addArgument("-oac");
cmd.addArgument("mp3lame");
cmd.addArgument("-lameopts");
cmd.addArgument("preset=64");
cmd.addArgument("-ovc");
cmd.addArgument("xvid");
cmd.addArgument("-xvidencopts");
cmd.addArgument("bitrate=600");
cmd.addArgument("-of");
cmd.addArgument("avi");
cmd.addArgument("-o");
cmd.addArgument(outFile);
return CmdHelper.run(cmd, cmdOut, 0);
}
}

配置项

1
2
3
4
5
6
7
convert.src=/mnt/file/upload/
convert.out=/mnt/file/download/
convert.log=/mnt/file/download/
convert.ffmpeg=/opt/ffmpeg/ffmpeg
convert.unoconv=/opt/libreoffice6.4/unoconv.py
convert.mencoder=/usr/bin/mencoder
convert.notify=http://127.0.0.1:8080/api/convert/notify
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@Configuration
@ConfigurationProperties(prefix = "convert")
public class ConvertConfig {
String src;
String out;
String log;
String ffmpeg;
String unoconv;
String mencoder;
String notify;
}

依赖

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

ffmpeg && mencoder

1
2
3
4
5
wget https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
yum install epel-release
rpm -Uvh rpmfusion-free-release-7.noarch.rpm
wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz
yum install mencoder

libreoffice && unoconv

1
2
https://www.libreoffice.org/donate/dl/rpm-x86_64/6.4.4/zh-CN/LibreOffice_6.4.4_Linux_x86-64_rpm.tar.gz
https://codeload.github.com/unoconv/unoconv/zip/0.8.2

简单实现Web页面元素拖放功能(Web Drag & Drop)

之前,想在浏览器中实现拖放似乎很困难,现在有了HTML5就简单了。下面实现一个例子:简单的拖拽排序。

拖放测试页面(DragDrop.html)

页面使用bootstrap展示几张图片(缩略图)
注:省略号部分表示你可以自己在div.row下复制更多个div.thumbnail

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drag & Drop sample</title>
<link rel="stylesheet" href="http://cdn.staticfile.org/twitter-bootstrap/3.1.1/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="thumbnail col-sm-6 col-md-4">
<img class="img-responsive" src="img1.png">
<div class="caption text-center"><h5>Image1</h5></div>
</div>
<div class="thumbnail col-sm-6 col-md-4">
<img class="img-responsive" src="img2.png">
<div class="caption text-center"><h5>Image2</h5></div>
</div>
<!-- ...... -->
</div>
</div>
<script src="http://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script src="DragDrop.js"></script>
</body>
</html>

拖放测试脚本(DragDrop.js)

注:脚本依赖的jquery已经在页面里引入。

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
$(function(){
//拖拽的对象是这些缩略图
var dragable = '.thumbnail';
var $dragable = $(dragable);
//设置可拖拽属性
$dragable.attr('draggable', true);
//响应开始拖拽事件
$dragable.on('dragstart', setDragData);
//拖放的目标也是这些缩略图
var $dropable = $dragable;
//让目标允许拖放
$dropable.on('dragover', allowDrop);
//响应拖放事件
$dropable.on('drop', dropDragData);

//开始拖拽事件处理
function setDragData(e){
var $target = getDragable(e.target);
//传递被拖拽对象的位置索引
e.originalEvent.dataTransfer.setData("text",$target.index());
}

function allowDrop(e){
e.preventDefault();//这样就能允许拖放
}

//拖放事件处理:把被拖拽对象放到目标对象所在的位置,即实现拖拽排序。
function dropDragData(e){
e.preventDefault();//阻止默认的放置行为
var $target = getDragable(e.target);
var dragIdx = e.originalEvent.dataTransfer.getData("text");
if (dragIdx !== $target.index()){
var $draged = $(dragable).eq(dragIdx);
if ($draged.index() > $target.index()){
$target.before($draged);//向前拖:放在目标对象的前面
}else{
$target.after($draged);//向后拖:放在目标对象的后面
}
}
}
//获取可拖拽对象
function getDragable(dom){
var $dragable = $(dom);
if (!$dragable.is(dragable)){
//拖拽子元素时定位到父元素
$dragable = $dragable.closest(dragable);
}
return $dragable;
}
});

总结

上例的拖放效果在最新的ChromeIEQQ浏览器下测试了都很正常,但在FireFox下拖放图片会激活一个新的页签(有人说使用e.preventDefault()可以解决,但我试了没有效果)。

让table固定表头并冻结列的一种简单实现方法

标题已经说明需求,就不说废话了,直接上code;)

.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--把table放到div容器里。-->
<div class="table-container">
<table class="table-fixed">
<thead>
<tr>
<th class="col-fixed">fixed0</th>
<th>column0</th>
...省略其它th...
</tr>
</thead>
<tbody>
<tr>
<td class="col-fixed">fixed1</td>
<td>column1</td>
....省略其它td...
</tr>
....省略其它tr...
</tbody>
</table>
<div>

.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
/*限制容器宽高并允许滚动*/
.table-container{
width:100%;
max-height:200px;
overflow:auto;
}
/*表头cell设为相对定位*/
.table-fixed>thead>tr>th{
position:relative;
z-index:100;
background-color:#eee;
}
/*冻结列设为相对定位*/
.table-fixed>tbody>tr>td.col-fixed{
position:relative;
background-color:#eee;
}
/*设置z-index避免被覆盖*/
.table-fixed>thead>tr>th.col-fixed{
z-index:110;
}
</style>

.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
//需先引入jquery
$(function(){
var $table = $('.table-fixed');
var $topFixes = $table.find('thead>tr>th');
var $colFixes = $table.find('.col-fixed');
//容器滚动时,为实现fixed冻结效果:
//1、把表头cell的top设为scrollTop
//2、冻结列的left设为scrollLeft
$table.closest('.table-container').scroll(function(){
$topFixes.css('top',this.scrollTop);
$colFixes.css('left',this.scrollLeft);
});
});
</script>

##总结
相较于表头和内容分离的做法,这种实现方式不会出现对不齐的问题,而且比较简单(:不想写js的请忽略)

##已知问题

  1. 桌面Chrome下的效果最好,其他浏览器fixed内容有点闪烁。
  2. 移动端浏览器下的延迟比较严重,只能改为用多个table实现。
  3. 如果冻结列超出容器(.table-container)的范围,会导致水平滚动条滚不到头(无限滚动)。

Mongodb MapReduce 介绍、示例及特别说明

介绍

Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。
MongoDB提供的Map-Reduce非常灵活,可以高效的进行大规模数据的统计分析。

语法

格式一

1
2
3
4
5
6
7
8
9
10
11
12
13
db.collection.mapReduce(
function() {emit(key,value)},//map 函数
function(key,values) {return reduced}, //reduce 函数
<collection>//out collection
)
//第三个参数也可以传入更多选项:
{
finalize: function(key, reduced){return finalized}
out: <collection>|{inline:true},
query: <document>,
sort: <document>,
limit: <number>,
}

格式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.runCommand({
mapReduce: <collection>,
map: <function>,
reduce: <function>,
finalize: <function>,
out: <output>,
query: <document>,
sort: <document>,
limit: <number>,
scope: <document>,
jsMode: <boolean>,
verbose: <boolean>,
bypassDocumentValidation: <boolean>,
collation: <document>
})

示例

示例集合person存储各省市居民的姓名、性别等记录

1
2
3
4
5
6
7
8
9
10
11
12
13
1.{
"name" : "姓名1",
"gender" : "男",
"city" : "城市a",
"province" : "省份A"
}
2.{
"name" : "姓名2",
"gender" : "女",
"city" : "城市b",
"province" : "省份B"
}
3...

现用mapReduce统计各省市人口性别比例:

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
db.person.mapReduce(
function(){//map
var key = {province:this.province, city:this.city}
var value = {total: 1}
if (this.gender == '男'){
value.male = 1
}else if (this.gender == '女'){
value.female = 1
}else{
value.unknown = 1
}
emit(key, value)
},
function(key, values){//reduce
var value = {}//累计各性别的数量
values.forEach(function(item){
//item里可能是单个值,也可能已经是累计值
for (var k in item){
value[k] = (value[k]||0) + item[k]
}
})
return value//reduce返回值不支持数组,若需多值请用对象格式
},{
query:{},//指定过滤源数据的查询条件
sort:{province:1, city:1},//按key排序可减少reduce的次数,加快执行速度
finalize: function(key, rValue){
for (var k in rValue){
if (k !== 'total'){//数量转为比例
rValue[k] = rValue[k]/rValue.total
}
}
return rValue
},
out:{inline:true},
}
).find()

执行结果如下:

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
[
{
"_id" : {
"province" : "省份A",
"city" : "城市a"
},
"value" : {
"total" : 40.0,
"male" : 0.425,
"female" : 0.575
}
},
{
"_id" : {
"province" : "省份A",
"city" : "城市b"
},
"value" : {
"total" : 1.0,
"male" : 1.0
}
},
{
"_id" : {
"province" : "省份A",
"city" : "城市c"
},
"value" : {
"total" : 150.0,
"male" : 0.526,
"female" : 0.473
}
},
...
]

特别说明

  1. 当key对应的value只有一组时不会触发reduce。所以,map函数emit的value值的格式最好和reduce函数return值一致,避免最终的结果不一致。
  2. 同一个key可能会触发多次reduce,即reduce接收的values中有些item元素可能是已经reduced的,累积时要考虑这种情况(即values包含直接emit的value,也包含reduce返回的value)。
  3. reduce返回值不支持数组,若需多值请用对象格式。
  4. 按key排序可以减少reduce的次数,提高处理效率。
  5. out可以指定输出到一个collection集合,但这样会导致写操作;指定为{inline:true}时mongodb会用runReadCommand代替runCommand——if inline output is specified, we need to apply readPreference on the command as it could be run on a secondary.

JS的“缺陷”

JS从诞生到现在已经二十几年了,在其演进中难免引入了一些bug,且积习难改。理解这些缺陷有助于避开那些新手陷阱。

typeof相关

Null和Undefined

他们都是基本类型,也都只对应一个常量值(分别是null和undefined)。

1
2
3
4
5
6
//已知:
typeof undefined === 'undefined'
//你可能会以为:
typeof null === 'null'//但,不是这样
//真相是:
typeof null === 'object'//null是指空对象,空对象也是对象!

Object、Array、Function、RegExp、Date、Error、…

这些本质上都是对象类型。

1
2
3
4
5
6
7
//已知:
typeof {} === 'object'
typeof function(){} === 'function'
//你可能会以为:
typeof [] === 'array'//但,不是这样的
//真相是:
typeof [] === 'object'//数组也是对象,基本类型外的除了函数都是对象。

typeof一览表

类型 Type typeof
布尔 Boolean true ‘boolean’
数值 Number 1 ‘number’
字符串 String ‘’ ‘string’
符号 Symbol Symbol() ‘symbol’
未定义 Undefined undefined ‘undefined’
空* Null null ‘object’
对象 Object {} ‘object’
下列都属于 对象的派生类型
数组 Array [] ‘object’
函数* Function function(){} ‘function’
Function class{} ‘function’

相等比较

宽松比较==

相等操作符比较两个值是否可能相等,即如果类型不同则进行可能的类型转换。

1
2
3
4
5
6
7
8
9
0==''//true
0=='0'//true
0==false//true
[1,2]=='1,2'//true
null==undefined//true
!null && !undefined//true
//注意:null和undefined既不等于true也不等于false
null==true || null==false//false
undefined==true || undefined==false//false

严格比较===

全等操作符比较两个值是否严格相等,即要求类型一样,也要求值相同。

1
2
3
4
5
6
null===null//true
undefined===undefined//true
null===undefined//false
0==='0'//false
0===-0//true
NaN===NaN//false?--这个比较有性格

同值相等Object.is()比较

Object.js()比较的结果同全等操作符,除了下面这两个相反:

1
2
Objecct.is(NaN,NaN)//true
Objecct.is(0,-0)//false

A链接打开的新窗口无法显示内容?

通常,我们想要一个A链接跳转到新窗口的话,会这样写:

1
<a href="http://www.zybuluo.com" target="_blank">link to a new window</a>

如果href的url需要动态的话,需要用到javascript,会改写成这样:

1
<a href="javascript:openUrl();" target="_blank">link to a new window</a>

当然还需要额外的script定义openUrl()函数:

1
2
3
4
function openUrl(){
var url = 'http://www.zybuluo.com';
window.open(url, '_blank');
}

似乎一切OK。——我是在chrome下测试的,确实没问题。
但发现,在IE和Firefox下试不行:新窗口无法显示内容。

原因是忽视了a标签里的target="_blank",去掉它就行了。

也就是说,借用A链接执行js的最好不要加target属性(虽然Chrome支持但其它浏览器不一定支持)。所以,还是在脚本里决定是否打开新窗口。

感想

  • 前端测试需顾及所有主流浏览器;
  • Chrome(google)的兼容性做得太好了:)

JS对象的克隆Clone

浅克隆[Plain Clone]:

1
2
3
4
5
6
var obj1 = {foo: "foo", bar: "bar"};
var obj2 = {foo: "foo2", bar: "bar2"};
var copy1 = {...obj1}; // Object {foo: "foo", bar: "bar"}
var copy2 = Object.assign({}, obj); //Object {foo: "foo2", bar: "bar2"}
var copySpread = {...obj1, ...obj2}; //Object {foo: "foo2", bar: "bar2"}
var copyAssign = Object.assign({}, obj2, obj1); //Object {foo: "foo", bar: "bar"}

JSON克隆[Json Clone]

1
2
var obj = { a: 0, b: { c: 0 } };
var copy = JSON.parse(JSON.stringify(obj));

深度克隆[Deep Clone]

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
function deepClone(obj) {
var copy;
// Handle the 3 simple types, and null or undefined
if (null == obj || "object" != typeof obj) return obj;
// Handle Date
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
// Handle Array
if (obj instanceof Array) {
copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = deepClone(obj[i]);
}
return copy;
}
// Handle Function
if (obj instanceof Function) {
copy = function() {
return obj.apply(this, arguments);
}
return copy;
}
// Handle Object
if (obj instanceof Object) {
copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name);
}

使用键盘事件(keypress)对输入框(input)做输入限制

需求

为避免用户输入非法数据,希望能对某些输入框的输入做限制。这里用价格输入框(只允许输入最多2位小数的非负数值)来做例子:

1
<input type="text" id="price" name="price" placeholder="请输入价格(最多2位小数)" required>

键盘事件处理代码(javascript)

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
//用了jQuery
$(function(){
//事件绑定
$('#price').keypress(onPriceKeypress).keyup(onPriceKeyup);
//按键处理
function onPriceKeypress(e){
var allowKeys = '0123456789+.';//只允许输入这些字符
var keyCode = e.which;//键盘输入的字符码

if (code<32 || (code>126 && code<160)){
//在有些浏览器(如Firefox)下控制字符也会触发该事件
return true;//允许输入控制字符,如退格、删除、tab等键
}
if (allowKeys.indexOf(String.fromCharCode(code))<0){//若是不允许的字符
return false;//禁止输入
}
return true;
}
//起键处理
function onPriceKeyup(e){
var $ipt = $(this), val = $ipt.val();//取当前值
if (!/^[+\d]?\d*\.?\d{0,2}$/.test(val)){//若当前值不是合法价格
$ipt.val(/[+\d]?\d*\.?\d{0,2}/.exec(val));//去除不合法的内容
}
}
}

注:

  1. 只在input使用pattern属性(如:pattern="[+\d]?\d*\.?\d{0,2}")只能限制最后的结果,不能限制输入不当的字符。
  2. 把input的type设为number可限制只能输入数字,但无法输入小数。
  3. 在有些浏览器(如Firefox)下控制字符也会触发keypress事件,待textinput事件普及后改用该事件就不用做特殊处理了。

使用electron开发桌面应用(笔记)

开发环境

  • nodejs + npm/cnpm
    1
    安装node
  • electron
  • 开发工具
    • atom

      第三方(vendors)

  • jQuery
  • Bootstrap
  • Nedb

Q&A

  • 如何防止用户重复打开应用?
  • 执行Nedb操作有时无响应(callback不被调用)?
    • 上一次操作参数有误导致后面的操作都被挂起(参看BUG)
  • 路径问题:相对路径 v.s. 绝对路径?数据文件路径?
    • 界面(ui组件)载入使用相路径(相对于对于起始页index.html)
    • 业务(mdl&&dao)对象require使用绝对路径(appPath+对象.js)
    • 数据文件存放在userData路径下:app.getPath(‘userData’)