第一次 GFX 50S + GF63mmF2.8 R WR 人像
上周末,带着 GFX 50S 去了躺内蒙,拍了几张人像,感觉全幅的细节跟中幅真的是没法比,也没怎么后期,就直接手机上面套了几个滤镜就直接上图了吧,细节基本上全部丢失。

























Polarr 转了几张黑白(其实机身直出黑白感觉更好):


上周末,带着 GFX 50S 去了躺内蒙,拍了几张人像,感觉全幅的细节跟中幅真的是没法比,也没怎么后期,就直接手机上面套了几个滤镜就直接上图了吧,细节基本上全部丢失。

























Polarr 转了几张黑白(其实机身直出黑白感觉更好):


佳能全出了,换了一个 Fujifilm GFX 机身 + 63mm/F2.8 镜头,这里记录一下。
更新几张样片,感觉得慢慢掌握了,就这一次上传原图了,五张图,100多M,以后得小小图了。





最近想着自己得静下心来搞摄影了,感觉自己这玩了近十年相机,其实根本就算不上是在摄影,只能说是在记录吧,这么久了跟大家说的是因为我只喜欢自然光,所以从来不使用闪光灯,虽然说我的闪都买了好几年了,到今天都还没真正的把玩过(对,把玩,我是按着自己的兴趣在玩相机,而不是以这个为主业)。
前几天我一口把很多镜头都出了,感觉用了这么多镜头,各种焦段、品牌的,感觉自己常用的也就那些,镜头多了还导致我有选择困难症,所以,就都出了吧,以后出门一台富士 XT-2 + 18-55/F2.8,因为我都是出户外拍得多,除了风景外,也给同行的人拍拍纪念照啥的,以前都不带闪光灯(镜头带得太多),这次决定为了更好的服务于广大人民,就买了一个富士的闪光灯。
有点扯远了,回到正题,什么是 TTL 功能呢?它的命名叫 *Through The Lens,其实,就是说,闪光灯跟相机已经关联在一起了,它能通过相机的拍摄参数设置以及现场环境光线来自动的决定光量。
如果利用闪光灯拍摄,相机不单要计算现场光的曝光量,还需要计算闪光灯闪光之后的曝光量对现场光的影响,由于需要同时进行两种光的计算,加上闪光灯只在快门开放的那一刹进行照明,如果快门速度高于同步快门,还没有开启高速同步闪光功能的话,就会出一半变量的情况。为了令相片加了闪光灯后还能提供一个正确的曝光值,于是相机就有了同步快门,摄影者需要设定相机在拍摄时,速度必须比同步快门慢,才能让闪光灯均匀的照亮整个画面。
昨天把 Samba 服务器搞定了,现在已经开始用上了,之后,我把以前的移动硬盘插在了新买的华为荣耀路由器上面,启用了路由器的 Samba 服务,配置情况如下:
接下来,在我的 Elementary 上面,连接该服务,并将里面的文件同步到本机。
继续之前,需要先安装 smbclient 工具:
sudo apt-get install smbclientsmbclient 连接smbclient 可以用于浏览 Samba 服务器上面的所有共享资源,也可以用于直接访问 Samba 服务器指定的共享资源。
浏览 Samba 服务器所有共享资源可以使用如下命令:
smbclient -L 192.168.3.13 -U username输入上面命令之后,终端会要求你输入该用户名的密码,如果想直接连同密码一起输入到命令行工具中,可以像下面这样:
smbclient -L 192.168.3.13 -U username%password终端输出如下结果:
Domain=[WORKGROUP] OS=[Unix] Server=[Samba 3.0.37]
Sharename Type Comment
--------- ---- -------
Seagate_usb1_1 Disk
Seagate_usb1_2 Disk 如果要直接访问 Samba 服务器指定共享资源,可以使用下面这行命令:
smbclient //192.168.3.13/Seagate_usb1_2 -U username%password输出:
Domain=[WORKGROUP] OS=[Unix] Server=[Samba 3.0.37]
smb: \> 在 smb 环境下,我们就可以对文件进行操作。
mount 方式使用 smbclient 访问,还是很麻烦,一般我们都会将资源挂载到本地计算机上面,这样使用起来就会方便得多:
mount -t cifs //192.168.3.13/Seagate_usb1_2 /home/pantao/CloudStorages/HonorRouter -o username=username%password如果要永久使用 Samba 服务器共享资源,可以将挂载信息写入到 /etc/fstab 文件中,在最后添加以下这行:
//192.168.3.13/Seagate_usb1_2 /home/pantao/CloudStorages/HonorRouter cifs defaults,username=username%password 0 0由于 /etc/fstab 文件的默认权限是 0644,也就是说所有用户都可以读取,如果你不希望其它用户看到你的 Samba 用户名与密码的话,那可在 /etc/samba/pwd.conf 文件中,添加以下内容:
username=username password=password然后在 /etc/fstab 中添加下面这行:
//192.168.3.13/Seagate_usb1_2 /home/pantao/CloudStorages/HonorRouter cifs defaults,credentials=/etc/samba/pwd.conf 0 0在 /etc/auto.master 文件中加入以下内容:
/home/pantao/CloudStorages/HonorRouter /etc/samba.misc--timeout=30 # timeout 值可以根据实际网络情况设置然后建立 /etc/samba.misc 文件,加入以下内容:
samba-fstype=cifs,username=username,password=password ://192.168.3.13/Seagate_usb1_2 Samba,是种用来让UNIX系列的操作系统与微软Windows操作系统的SMB/CIFS(Server Message Block/Common Internet File System)网络协议做链接的自由软件。第三版不仅可访问及分享SMB的文件夹及打印机,本身还可以集成入Windows Server的网域,扮演为网域控制站(Domain Controller)以及加入Active Directory成员。简而言之,此软件在Windows与UNIX系列操作系统之间搭起一座桥梁,让两者的资源可互通有无。
sudo apt-get install sambasudo vi /etc/samba/smb.conf在文件末尾添加以下内容:
[public]
path=/data/nfs
browseable=yes
read only=no
guest ok=yes文件夹的可读写权限我在已经设置好了,添加了匿名的文件读写权限。
sudo smbpasswd -a pantao # 添加系统用户 pantao 至 samba 在执行smbpasswd前,需要先添加好用户pantao,否则命令会执行失败。
sudo service smbd restartsmbclient -L //192.168.3.3/public可以直接访问如下地址即可:
\\192.168.3.3\public如果
public=no,则此时需要输入samba的用户名与密码,如果public=yes,则作为nobody用户直接访问。另外,在 Windows 客户端可以使用
net use * /del /y这条命令清理访问缓存。
comment=smb share test:该共享的备注信息path=/data/nfs:共享路径allow hosts=host(subnet):设置该 Samba 服务器允许的工作组或域deny hosts=host(subnet):设置该 Samba 服务器拒绝的工作组或域available=yes|no:设置该共享目录是否可用browseable=yes|no:设置该共享目录是否可显示writable=yes|no:指定该目录缺省是否可写,也可以用 readonly = no 来设置可写public=yes|no:指明该共享资源是否能给游客使用, guest ok=yes 其实与 public=yes 是一样的user=user,@group: user 设置所有可能使用该共享资源的用户,也可以用 @group 代表 group 这个组的所有成员,不同的项目之间使用空格或者英文逗号隔开valid users=user,@group:指定能够使用该共享资源的用户和组invalid users=user,@group:指定不能够使用该共享资源的用户和组read list=user,@group:指定只能读取该共享资源的用户和组write list=user,@group:指定能读取和写入该共享资源的用户和组admin list=user,@group:指定能管理该共享资源(包括读写和权限赋予等)的用户和组hide dot files=yes|no:是否像 Unix 一样,隐藏以 . 号开头的文件create mode=0755:指明新建立的文件的属性,一般是0755directory mode=0755:指明新建立的目录的属性,一般是 0755sync always=yes|no:指明对该共享资源进行写操作后是否进行同步操作short preserve case=yes|no:是否区分文件名大小写preserve case=yes|no:是否保持大小写case sensitive=yes|no:是否能大小写敏感,一般选 no,不然可能会引起错误mangle case=yes|no:指明混合大小写default case=upper|lower:指明默认的文件名是全部大写还是小写force user=testuser:强制设置新建立的文件的属主是谁,如果我有一个目录, guest 用户 可以写,那么 guest 就也可以删除文件 ,但是如果我强制把建痒痒的文件的属主设置为 testuser,同时限制 create mode=0755,那么 guest 就不能删除了wide links=yes|no:指明是否允许共享外符号连接,比如共享资源里面有一个连接指向非共享资源里面的文件或者目录,如果设置为 wide links=no,将使该链接不可用max connections=100:最大同时连接数delete readonly=yes|no:是否能删除共享资源里面已经被指定为只读的文件以前还没有在 Linux 下使用大于 2T 的硬盘,今天入的一块 8T 硬盘,按以前的 fdisk ,发现一直就只有 2T,才发现,原来还有这么个问题,先记录下今天格式化的命令:
parted # 打开 parted 程序
select /dev/sdc # 选择需要操作的硬盘
mklabel gpt # 将 MBR 硬盘格式为 GPT
mkpart primary 0% 100% # 将整块硬盘分成一个分区
print # 打印分区信息
quit # 退出完成上面的步骤之后,可以开始格式化刚才创建的分区了:
mkfs.ext4 -T largefile /dev/sdc1完成之后,可以直接 mount 刚才格式化的硬盘
sudo mkdir /data
mount /dev/sdc1 /data使用 df -h 就可以看到该磁盘的大小,如果想让系统每次启动之后自动挂载,可以编辑 /etc/fstab 文件,添加下面这一行:
/dev/sdc1 /data ext4 defaults 0 0 这么多年积累下来的照片(RAW文件都保留着)太多,西数的 NAS 有点不够用了,想着就入了一块 8T 的硬盘,扩展一下家里面的这台老台式机(安装的是 Elementary OS),搭建了一个 NFS 服务。
sudo apt-get install nfs-kernel-server nfs-commonsudo vi /etc/exports在末尾添加下面这行:
/data/nfs *(rw,sync,no_root_squash,no_subtree_check)上面这段表达的意思是:
/data/nfs : 共享的目录
* : 指定哪些用户 可以访问
* 所有可以 ping 通本主机的用户
192.168.3.* 指定网段,在该网段中的用户可以挂载
192.168.3.14 只有该 IP 的用户才能挂载
( : 共享参数
rw, : 权限
ro: 只读
rw: 读写
sync, : 同步写入硬盘
no_root_squash, : root 用户访问此目录,具有 root 操作权限
no_subtree_check : 不检查父目录权限
)常用的共享参数还有:
ro:只读访问rw:读写访问sync:同步写入硬盘async:暂存内存secure:NFS 通过 1024 以下的安全 TCP/IP 端口发送insecure:NFS 通过 1024 以上的端口发送wdelay:多个用户对共享目录进行写操作时,则按组写入数据(默认)no_wdelay:多个用户对共享目录进行写操作时,则立即写入数据hide:不共享其子目录no_hide:共享其子目录subtree_check:强制 NFS 检查父目录的权限no_subtree_check:不检查父目录权限all_squash:任何访问者,都转为匿名root_squash :root 用户访问此目录, 映射成如 anonymous 用户一样的权限(默认)no_root_squash:root 用户访问此目录,具有 root 操作权限sudo /etc/init.d/nfs-kernel-server restart到此,nfs 的服务就搭建好了
检查客户端与服务端的网络是否连通
ping 192.168.3.3查看服务端的共享目录设置
showmount -e 192.168.3.3
Export list for 192.168.3.3:
/data/nfs *将该目录挂载到本地
mount 192.168.3.3:/data/nfs /path/to/local/folder访问
cd /path/to/local/folder如题,等待
HTML 是什么?
<head><body>复杂表单
<fieldset><input> 类型复杂表格
<colgroup><thead>、<tbody 以及 <tfoot>CSS 是什么?
keyframesanimation初涉 JavaScript
函数
数组
map、reduce、forEach、filter 等方法正则
setInterval 与 setTimeoutwindow 对象XMLHttpRequest 与 Ajax模块
httpurlpathfsnpm 命令yarn 命令nvmcurl -o- https://raw.githubusercontent/creationx/nvm/v0.33.2.install.sh | bashnvm install nodenvm install node --version/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"brew install mongodb
## 修改配置文件
vi /usr/local/etc/mongod.conf
## 创建数据存储目录
mkdir -p ~/workspace/database/mongodb
## 设置读取权限
sudo chown USERNAME -u ~/workspace/database/mongodb
mkdir -p ~/Library/LaunchAgents
cd ~/Library/LaunchAgents/
cp /usr/local/Cellar/mongodb/3.4.4/homebrew.mxcl.mongodb.plist .
launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.mongodb.plistbrew install docker
brew install docker-machine
brew install docker-compose Seneca 是一个能让您快速构建基于消息的微服务系统的工具集,你不需要知道各种服务本身被部署在何处,不需要知道具体有多少服务存在,也不需要知道他们具体做什么,任何你业务逻辑之外的服务(如数据库、缓存或者第三方集成等)都被隐藏在微服务之后。
这种解耦使您的系统易于连续构建与更新,Seneca 能做到这些,原因在于它的三大核心功能:
在 Seneca 中,消息就是一个可以有任何你喜欢的内部结构的 JSON 对象,它们可以通过 HTTP/HTTPS、TCP、消息队列、发布/订阅服务或者任何能传输数据的方式进行传输,而对于作为消息生产者的你来讲,你只需要将消息发送出去即可,完全不需要关心哪些服务来接收它们。
然后,你又想告诉这个世界,你想要接收一些消息,这也很简单,你只需在 Seneca 中作一点匹配模式配置即可,匹配模式也很简单,只是一个键值对的列表,这些键值对被用于匹配 JSON 消息的极组属性。
在本文接下来的内容中,我们将一同基于 Seneca 构建一些微服务。
让我们从一点特别简单的代码开始,我们将创建两个微服务,一个会进行数学计算,另一个去调用它:
const seneca = require('seneca')();
seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: ( msg.left + msg.right )})
});
seneca.act({
role: 'math',
cmd: 'sum',
left: 1,
right: 2
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});将上面的代码,保存至一个 js 文件中,然后执行它,你可能会在 console 中看到类似下面这样的消息:
{"kind":"notice","notice":"hello seneca 4y8daxnikuxp/1483577040151/58922/3.2.2/-","level":"info","when":1483577040175}
(node:58922) DeprecationWarning: 'root' is deprecated, use 'global'
{ answer: 3 }到目前为止,所有这一切都发生在同一个进程中,没有网络流量产生,进程内的函数调用也是基于消息传输。
seneca.add 方法,添加了一个新的动作模式(_Action Pattern_)至 Seneca 实例中,它有两个参数:
pattern :用于匹配 Seneca 实例中 JSON 消息体的模式;action :当模式被匹配时执行的操作seneca.act 方法同样有两个参数:
msg :作为纯对象提供的待匹配的入站消息;respond :用于接收并处理响应信息的回调函数。让我们再把所有代码重新过一次:
seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: ( msg.left + msg.right )})
});在上面的代码中的 Action 函数,计算了匹配到的消息体中两个属性 left 与 right 的值的和,并不是所有的消息都会被创建一个响应,但是在绝大多数情况下,是需要有响应的, Seneca 提供了用于响应消息的回调函数。
在匹配模式中, role:math, cmd:sum 匹配到了下面这个消息体:
{
role: 'math',
cmd: 'sum',
left: 1,
right: 2
}并得到计自结果:
{
answer: 3
}关于 role 与 cmd 这两个属性,它们没有什么特别的,只是恰好被你用于匹配模式而已。
接着,seneca.act 方法,发送了一条消息,它有两个参数:
msg :发送的消息主体response_callback :如果该消息有任何响应,该回调函数都会被执行。响应的回调函数可接收两个参数: error 与 result ,如果有任何错误发生(比如,发送出去的消息未被任何模式匹配),则第一个参数将是一个 Error 对象,而如果程序按照我们所预期的方向执行了的话,那么,第二个参数将接收到响应结果,在我们的示例中,我们只是简单的将接收到的响应结果打印至了 console 而已。
seneca.act({
role: 'math',
cmd: 'sum',
left: 1,
right: 2
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});sum.js 示例文件,向你展示了如何定义并创建一个 Action 以及如何呼起一个 Action,但它们都发生在一个进程中,接下来,我们很快就会展示如何拆分成不同的代码和多个进程。
模式----而不是网络地址或者会话,让你可以更加容易的扩展或增强您的系统,这样做,让添加新的微服务变得更简单。
现在让我们给系统再添加一个新的功能----计算两个数字的乘积。
我们想要发送的消息看起来像下面这样的:
{
role: 'math',
cmd: 'product',
left: 3,
right: 4
}而后获得的结果看起来像下面这样的:
{
answer: 12
}知道怎么做了吧?你可以像 role: math, cmd: sum 模式这样,创建一个 role: math, cmd: product 操作:
seneca.add('role:math, cmd:product', (msg, reply) => {
reply(null, { answer: ( msg.left * msg.right )})
});然后,调用该操作:
seneca.act({
role: 'math',
cmd: 'product',
left: 3,
right: 4
}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});运行 product.js ,你将得到你想要的结果。
将这两个方法放在一起,代码像是下面这样的:
const seneca = require('seneca')();
seneca.add('role:math, cmd:sum', (msg, reply) => {
reply(null, { answer: ( msg.left + msg.right )})
});
seneca.add('role:math, cmd:product', (msg, reply) => {
reply(null, { answer: ( msg.left * msg.right )})
});
seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, console.log)
.act({role: 'math', cmd: 'product', left: 3, right: 4}, console.log)运行 sum-product.js 后,你将得到下面这样的结果:
null { answer: 3 }
null { answer: 12 }在上面合并到一起的代码中,我们发现, seneca.act 是可以进行链式调用的,Seneca 提供了一个链式API,调式调用是顺序执行的,但是不是串行,所以,返回的结果的顺序可能与调用顺序并不一样。
模式让你可以更加容易的扩展程序的功能,与 if...else... 语法不同的是,你可以通过增加更多的匹配模式以达到同样的功能。
下面让我们扩展一下 role: math, cmd: sum 操作,它只接收整型数字,那么,怎么做?
seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right)
respond(null, {answer: sum})
})现在,下面这条消息:
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}将得到下面这样的结果:
{answer: 3} // == 1 + 2,小数部分已经被移除了代码可在 sum-integer.js 中查看。
现在,你的两个模式都存在于系统中了,而且还存在交叉部分,那么 Seneca 最终会将消息匹配至哪条模式呢?原则是:更多匹配项目被匹配到的优先,被匹配到的属性越多,则优先级越高。
pattern-priority-testing.js 可以给我们更加直观的测试:
const seneca = require('seneca')()
seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
// 下面两条消息都匹配 role: math, cmd: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log)
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log)
setTimeout(() => {
seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right)
respond(null, { answer: sum })
})
// 下面这条消息同样匹配 role: math, cmd: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log)
// 但是,也匹配 role:math,cmd:sum,integer:true
// 但是因为更多属性被匹配到,所以,它的优先级更高
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log)
}, 100)输出结果应该像下面这样:
null { answer: 4 }
null { answer: 4 }
null { answer: 4 }
null { answer: 3 }在上面的代码中,因为系统中只存在 role: math, cmd: sum 模式,所以,都匹配到它,但是当 100ms 后,我们给系统中添加了一个 role: math, cmd: sum, integer: true 模式之后,结果就不一样了,匹配到更多的操作将有更高的优先级。
这种设计,可以让我们的系统可以更加简单的添加新的功能,不管是在开发环境还是在生产环境中,你都可以在不需要修改现有代码的前提下即可更新新的服务,你只需要先好新的服务,然后启动新服务即可。
模式操作还可以调用其它的操作,所以,这样我们可以达到代码复用的需求:
const seneca = require('seneca')()
seneca.add('role: math, cmd: sum', function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
})
seneca.add('role: math, cmd: sum, integer: true', function (msg, respond) {
// 复用 role:math, cmd:sum
this.act({
role: 'math',
cmd: 'sum',
left: Math.floor(msg.left),
right: Math.floor(msg.right)
}, respond)
})
// 匹配 role:math,cmd:sum
seneca.act('role: math, cmd: sum, left: 1.5, right: 2.5',console.log)
// 匹配 role:math,cmd:sum,integer:true
seneca.act('role: math, cmd: sum, left: 1.5, right: 2.5, integer: true', console.log)在上面的示例代码中,我们使用了 this.act 而不是前面的 seneca.act,那是因为,在 action 函数中,上下文关系变量 this ,引用了当前的 seneca 实例,这样你就可以在任何一个 action 函数中,访问到该 action 调用的整个上下文。
在上面的代码中,我们使用了 JSON 缩写形式来描述模式与消息, 比如,下面是对象字面量:
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5}缩写模式为:
'role: math, cmd: sum, left: 1.5, right: 2.5'jsonic 这种格式,提供了一种以字符串字面量来表达对象的简便方式,这使得我们可以创建更加简单的模式和消息。
上面的代码保存在了 sum-reuse.js 文件中。
你定义的 Action 模式都是唯一了,它们只能触发一个函数,模式的解析规则如下:
规则被设计得很简单,这使得你可以更加简单的了解到到底是哪个模式被匹配了。
下面这些示例可以让你更容易理解:
a: 1, b: 2 优先于 a: 1, 因为它有更多的属性;a: 1, b: 2 优先于 a: 1, c: 3,因为 b 在 c 字母的前面;a: 1, b: 2, d: 4 优先于 a: 1, c: 3, d:4,因为 b 在 c 字母的前面;a: 1, b:2, c:3 优先于 a:1, b: 2,因为它有更多的属性;a: 1, b:2, c:3 优先于 a:1, c:3,因为它有更多的属性。很多时间,提供一种可以让你不需要全盘修改现有 Action 函数的代码即可增加它功能的方法是很有必要的,比如,你可能想为某一个消息增加更多自定义的属性验证方法,捕获消息统计信息,添加额外的数据库结果中,或者控制消息流速等。
我下面的示例代码中,加法操作期望 left 和 right 属性是有限数,此外,为了调试目的,将原始输入参数附加到输出的结果中也是很有用的,您可以使用以下代码添加验证检查和调试信息:
const seneca = require('seneca')()
seneca
.add(
'role:math,cmd:sum',
function(msg, respond) {
var sum = msg.left + msg.right
respond(null, {
answer: sum
})
})
// 重写 role:math,cmd:sum with ,添加额外的功能
.add(
'role:math,cmd:sum',
function(msg, respond) {
// bail out early if there's a problem
if (!Number.isFinite(msg.left) ||
!Number.isFinite(msg.right)) {
return respond(new Error("left 与 right 值必须为数字。"))
}
// 调用上一个操作函数 role:math,cmd:sum
this.prior({
role: 'math',
cmd: 'sum',
left: msg.left,
right: msg.right,
}, function(err, result) {
if (err) return respond(err)
result.info = msg.left + '+' + msg.right
respond(null, result)
})
})
// 增加了的 role:math,cmd:sum
.act('role:math,cmd:sum,left:1.5,right:2.5',
console.log // 打印 { answer: 4, info: '1.5+2.5' }
)seneca 实例提供了一个名为 prior 的方法,让可以在当前的 action 方法中,调用被其重写的旧操作函数。
prior 函数接受两个参数:
msg:消息体response_callback:回调函数在上面的示例代码中,已经演示了如何修改入参与出参,修改这些参数与值是可选的,比如,可以再添加新的重写,以增加日志记录功能。
在上面的示例中,也同样演示了如何更好的进行错误处理,我们在真正进行操作之前,就验证的数据的正确性,若传入的参数本身就有错误,那么我们直接就返回错误信息,而不需要等待真正计算的时候由系统去报错了。
错误消息应该只被用于描述错误的输入或者内部失败信息等,比如,如果你执行了一些数据库的查询,返回没有任何数据,这并不是一个错误,而仅仅只是数据库的事实的反馈,但是如果连接数据库失败,那就是一个错误了。
上面的代码可以在 sum-valid.js 文件中找到。
一个 seneca 实例,其实就只是多个 Action Patterm 的集合而已,你可以使用命名空间的方式来组织操作模式,例如在前面的示例中,我们都使用了 role: math,为了帮助日志记录和调试, Seneca 还支持一个简约的插件支持。
同样,Seneca插件只是一组操作模式的集合,它可以有一个名称,用于注释日志记录条目,还可以给插件一组选项来控制它们的行为,插件还提供了以正确的顺序执行初始化函数的机制,例如,您希望在尝试从数据库读取数据之前建立数据库连接。
简单来说,Seneca插件就只是一个具有单个参数选项的函数,你将这个插件定义函数传递给 seneca.use 方法,下面这个是最小的Seneca插件(其实它什么也没做!):
function minimal_plugin(options) {
console.log(options)
}
require('seneca')()
.use(minimal_plugin, {foo: 'bar'})seneca.use 方法接受两个参数:
plugin :插件定义函数或者一个插件名称;options :插件配置选项上面的示例代码执行后,打印出来的日志看上去是这样的:
{"kind":"notice","notice":"hello seneca 3qk0ij5t2bta/1483584697034/62768/3.2.2/-","level":"info","when":1483584697057}
(node:62768) DeprecationWarning: 'root' is deprecated, use 'global'
{ foo: 'bar' }Seneca 还提供了详细日志记录功能,可以提供为开发或者生产提供更多的日志信息,通常的,日志级别被设置为 INFO,它并不会打印太多日志信息,如果想看到所有的日志信息,试试以下面这样的方式启动你的服务:
node minimal-plugin.js --seneca.log.all会不会被吓一跳?当然,你还可以过滤日志信息:
node minimal-plugin.js --seneca.log.all | grep plugin:define通过日志我们可以看到, seneca 加载了很多内置的插件,比如 basic、transport、web 以及 mem-store,这些插件为我们提供了创建微服务的基础功能,同样,你应该也可以看到 minimal_plugin 插件。
现在,让我们为这个插件添加一些操作模式:
function math(options) {
this.add('role:math,cmd:sum', function (msg, respond) {
respond(null, { answer: msg.left + msg.right })
})
this.add('role:math,cmd:product', function (msg, respond) {
respond(null, { answer: msg.left * msg.right })
})
}
require('seneca')()
.use(math)
.act('role:math,cmd:sum,left:1,right:2', console.log)运行 math-plugin.js 文件,得到下面这样的信息:
null { answer: 3 }看打印出来的一条日志:
{
"actid": "7ubgm65mcnfl/uatuklury90r",
"msg": {
"role": "math",
"cmd": "sum",
"left": 1,
"right": 2,
"meta$": {
"id": "7ubgm65mcnfl/uatuklury90r",
"tx": "uatuklury90r",
"pattern": "cmd:sum,role:math",
"action": "(bjx5u38uwyse)",
"plugin_name": "math",
"plugin_tag": "-",
"prior": {
"chain": [],
"entry": true,
"depth": 0
},
"start": 1483587274794,
"sync": true
},
"plugin$": {
"name": "math",
"tag": "-"
},
"tx$": "uatuklury90r"
},
"entry": true,
"prior": [],
"meta": {
"plugin_name": "math",
"plugin_tag": "-",
"plugin_fullname": "math",
"raw": {
"role": "math",
"cmd": "sum"
},
"sub": false,
"client": false,
"args": {
"role": "math",
"cmd": "sum"
},
"rules": {},
"id": "(bjx5u38uwyse)",
"pattern": "cmd:sum,role:math",
"msgcanon": {
"cmd": "sum",
"role": "math"
},
"priorpath": ""
},
"client": false,
"listen": false,
"transport": {},
"kind": "act",
"case": "OUT",
"duration": 35,
"result": {
"answer": 3
},
"level": "debug",
"plugin_name": "math",
"plugin_tag": "-",
"pattern": "cmd:sum,role:math",
"when": 1483587274829
}所有的该插件的日志都被自动的添加了 plugin 属性。
在 Seneca 的世界中,我们通过插件组织各种操作模式集合,这让日志与调试变得更简单,然后你还可以将多个插件合并成为各种微服务,在接下来的章节中,我们将创建一个 math 服务。
插件通过需要进行一些初始化的工作,比如连接数据库等,但是,你并不需要在插件的定义函数中去执行这些初始化,定义函数被设计为同步执行的,因为它的所有操作都是在定义一个插件,事实上,你不应该在定义函数中调用 seneca.act 方法,只调用 seneca.add 方法。
要初始化插件,你需要定义一个特殊的匹配模式 init: <plugin-name>,对于每一个插件,将按顺序调用此操作模式,init 函数必须调用其 callback 函数,并且不能有错误发生,如果插件初始化失败,则 Seneca 会立即退出 Node 进程。所以的插件初始化工作都必须在任何操作执行之前完成。
为了演示初始化,让我们向 math 插件添加简单的自定义日志记录,当插件启动时,它打开一个日志文件,并将所有操作的日志写入文件,文件需要成功打开并且可写,如果这失败,微服务启动就应该失败。
const fs = require('fs')
function math(options) {
// 日志记录函数,通过 init 函数创建
var log
// 将所有模式放在一起会上我们查找更方便
this.add('role:math,cmd:sum', sum)
this.add('role:math,cmd:product', product)
// 这就是那个特殊的初始化操作
this.add('init:math', init)
function init(msg, respond) {
// 将日志记录至一个特写的文件中
fs.open(options.logfile, 'a', function (err, fd) {
// 如果不能读取或者写入该文件,则返回错误,这会导致 Seneca 启动失败
if (err) return respond(err)
log = makeLog(fd)
respond()
})
}
function sum(msg, respond) {
var out = { answer: msg.left + msg.right }
log('sum '+msg.left+'+'+msg.right+'='+out.answer+'\n')
respond(null, out)
}
function product(msg, respond) {
var out = { answer: msg.left * msg.right }
log('product '+msg.left+'*'+msg.right+'='+out.answer+'\n')
respond(null, out)
}
function makeLog(fd) {
return function (entry) {
fs.write(fd, new Date().toISOString()+' '+entry, null, 'utf8', function (err) {
if (err) return console.log(err)
// 确保日志条目已刷新
fs.fsync(fd, function (err) {
if (err) return console.log(err)
})
})
}
}
}
require('seneca')()
.use(math, {logfile:'./math.log'})
.act('role:math,cmd:sum,left:1,right:2', console.log)在上面这个插件的代码中,匹配模式被组织在插件的顶部,以便它们更容易被看到,函数在这些模式下面一点被定义,您还可以看到如何使用选项提供自定义日志文件的位置(不言而喻,这不是生产日志!)。
初始化函数 init 执行一些异步文件系统工作,因此必须在执行任何操作之前完成。 如果失败,整个服务将无法初始化。要查看失败时的操作,可以尝试将日志文件位置更改为无效的,例如 /math.log。
以上代码可以在 math-plugin-init.js 文件中找到。
现在让我们把 math 插件变成一个真正的微服务。首先,你需要组织你的插件。 math 插件的业务逻辑 ---- 即它提供的功能,与它以何种方式与外部世界通信是分开的,你可能会暴露一个Web服务,也有可能在消息总线上监听。
将业务逻辑(即插件定义)放在其自己的文件中是有意义的。 Node.js 模块即可完美的实现,创建一个名为 math.js 的文件,内容如下:
module.exports = function math(options) {
this.add('role:math,cmd:sum', function sum(msg, respond) {
respond(null, { answer: msg.left + msg.right })
})
this.add('role:math,cmd:product', function product(msg, respond) {
respond(null, { answer: msg.left * msg.right })
})
this.wrap('role:math', function (msg, respond) {
msg.left = Number(msg.left).valueOf()
msg.right = Number(msg.right).valueOf()
this.prior(msg, respond)
})
}然后,我们可以在需要引用它的文件中像下面这样添加到我们的微服务系统中:
// 下面这两种方式都是等价的(还记得我们前面讲过的 `seneca.use` 方法的两个参数吗?)
require('seneca')()
.use(require('./math.js'))
.act('role:math,cmd:sum,left:1,right:2', console.log)
require('seneca')()
.use('math') // 在当前目录下找到 `./math.js`
.act('role:math,cmd:sum,left:1,right:2', console.log)seneca.wrap 方法可以匹配一组模式,同使用相同的动作扩展函数覆盖至所有被匹配的模式,这与为每一个组模式手动调用 seneca.add 去扩展可以得到一样的效果,它需要两个参数:
pin :模式匹配模式action :扩展的 action 函数pin 是一个可以匹配到多个模式的模式,它可以匹配到多个模式,比如 role:math 这个 pin 可以匹配到 role:math, cmd:sum 与 role:math, cmd:product。
在上面的示例中,我们在最后面的 wrap 函数中,确保了,任何传递给 role:math 的消息体中 left 与 right 值都是数字,即使我们传递了字符串,也可以被自动的转换为数字。
有时,查看 Seneca 实例中有哪些操作是被重写了是很有用的,你可以在启动应用时,加上 --seneca.print.tree 参数即可,我们先创建一个 math-tree.js 文件,填入以下内容:
require('seneca')()
.use('math')然后再执行它:
❯ node math-tree.js --seneca.print.tree
{"kind":"notice","notice":"hello seneca abs0eg4hu04h/1483589278500/65316/3.2.2/-","level":"info","when":1483589278522}
(node:65316) DeprecationWarning: 'root' is deprecated, use 'global'
Seneca action patterns for instance: abs0eg4hu04h/1483589278500/65316/3.2.2/-
├─┬ cmd:sum
│ └─┬ role:math
│ └── # math, (15fqzd54pnsp),
│ # math, (qqrze3ub5vhl), sum
└─┬ cmd:product
└─┬ role:math
└── # math, (qnh86mgin4r6),
# math, (4nrxi5f6sp69), product从上面你可以看到很多的键/值对,并且以树状结构展示了重写,所有的 Action 函数展示的格式都是 #plugin, (action-id), function-name。
但是,到现在为止,所有的操作都还存在于同一个进程中,接下来,让我们先创建一个名为 math-service.js 的文件,填入以下内容:
require('seneca')()
.use('math')
.listen()然后启动该脚本,即可启动我们的微服务,它会启动一个进程,并通过 10101 端口监听HTTP请求,它不是一个 Web 服务器,在此时, HTTP 仅仅作为消息的传输机制。
你现在可以访问 http://localhost:10101/act?role=math&cmd=sum&left=1&right=2 即可看到结果,或者使用 curl 命令:
curl -d '{"role":"math","cmd":"sum","left":1,"right":2}' http://localhost:10101/act两种方式都可以看到结果:
{"answer":3}接下来,你需要一个微服务客户端 math-client.js:
require('seneca')()
.client()
.act('role:math,cmd:sum,left:1,right:2',console.log)打开一个新的终端,执行该脚本:
null { answer: 3 } { id: '7uuptvpf8iff/9wfb26kbqx55',
accept: '043di4pxswq7/1483589685164/65429/3.2.2/-',
track: undefined,
time:
{ client_sent: '0',
listen_recv: '0',
listen_sent: '0',
client_recv: 1483589898390 } }在 Seneca 中,我们通过 seneca.listen 方法创建微服务,然后通过 seneca.client 去与微服务进行通信。在上面的示例中,我们使用的都是 Seneca 的默认配置,比如 HTTP 协议监听 10101 端口,但 seneca.listen 与 seneca.client 方法都可以接受下面这些参数,以达到定抽的功能:
port :可选的数字,表示端口号;host :可先的字符串,表示主机名或者IP地址;spec :可选的对象,完整的定制对象注意:在 Windows 系统中,如果未指定host, 默认会连接0.0.0.0,这是没有任何用处的,你可以设置host为localhost。
只要 client 与 listen 的端口号与主机一致,它们就可以进行通信:
Seneca 为你提供的 无依赖传输 特性,让你在进行业务逻辑开发时,不需要知道消息如何传输或哪些服务会得到它们,而是在服务设置代码或配置中指定,比如 math.js 插件中的代码永远不需要改变,我们就可以任意的改变传输方式。
虽然 HTTP 协议很方便,但是并不是所有时间都合适,另一个常用的协议是 TCP,我们可以很容易的使用 TCP 协议来进行数据的传输,尝试下面这两个文件:
require('seneca')()
.use('math')
.listen({type: 'tcp'})require('seneca')()
.client({type: 'tcp'})
.act('role:math,cmd:sum,left:1,right:2',console.log)默认情况下, client/listen 并未指定哪些消息将发送至哪里,只是本地定义了模式的话,会发送至本地的模式中,否则会全部发送至服务器中,我们可以通过一些配置来定义哪些消息将发送到哪些服务中,你可以使用一个 pin 参数来做这件事情。
让我们来创建一个应用,它将通过 TCP 发送所有 role:math 消息至服务,而把其它的所有消息都在发送至本地:
require('seneca')()
.use('math')
// 监听 role:math 消息
// 重要:必须匹配客户端
.listen({ type: 'tcp', pin: 'role:math' })require('seneca')()
// 本地模式
.add('say:hello', function (msg, respond){ respond(null, {text: "Hi!"}) })
// 发送 role:math 模式至服务
// 注意:必须匹配服务端
.client({ type: 'tcp', pin: 'role:math' })
// 远程操作
.act('role:math,cmd:sum,left:1,right:2',console.log)
// 本地操作
.act('say:hello',console.log)你可以通过各种过滤器来自定义日志的打印,以跟踪消息的流动,使用 --seneca... 参数,支持以下配置:
date-time: log 条目何时被创建;seneca-id: Seneca process ID;level:DEBUG、INFO、WARN、ERROR 以及 FATAL 中任何一个;type:条目编码,比如 act、plugin 等;plugin:插件名称,不是插件内的操作将表示为 root$;case: 条目的事件:IN、ADD、OUT 等action-id/transaction-id:跟踪标识符,_在网络中永远保持一致_;pin:action 匹配模式;message:入/出参消息体如果你运行上面的进程,使用了 --seneca.log.all,则会打印出所有日志,如果你只想看 math 插件打印的日志,可以像下面这样启动服务:
node math-pin-service.js --seneca.log=plugin:mathSeneca不是一个Web框架。 但是,您仍然需要将其连接到您的Web服务API,你永远要记住的是,不要将你的内部行为模式暴露在外面,这不是一个好的安全的实践,相反的,你应该定义一组API模式,比如用属性 role:api,然后你可以将它们连接到你的内部微服务。
下面是我们定义 api.js 插件。
module.exports = function api(options) {
var validOps = { sum:'sum', product:'product' }
this.add('role:api,path:calculate', function (msg, respond) {
var operation = msg.args.params.operation
var left = msg.args.query.left
var right = msg.args.query.right
this.act('role:math', {
cmd: validOps[operation],
left: left,
right: right,
}, respond)
})
this.add('init:api', function (msg, respond) {
this.act('role:web',{routes:{
prefix: '/api',
pin: 'role:api,path:*',
map: {
calculate: { GET:true, suffix:'/{operation}' }
}
}}, respond)
})
}然后,我们使用 hapi 作为Web框架,建了 hapi-app.js 应用:
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const config = {
adapter: require('seneca-web-adapter-hapi'),
context: (() => {
const server = new Hapi.Server();
server.connection({
port: 3000
});
server.route({
path: '/routes',
method: 'get',
handler: (request, reply) => {
const routes = server.table()[0].table.map(route => {
return {
path: route.path,
method: route.method.toUpperCase(),
description: route.settings.description,
tags: route.settings.tags,
vhost: route.settings.vhost,
cors: route.settings.cors,
jsonp: route.settings.jsonp,
}
})
reply(routes)
}
});
return server;
})()
};
const seneca = Seneca()
.use(SenecaWeb, config)
.use('math')
.use('api')
.ready(() => {
const server = seneca.export('web/context')();
server.start(() => {
server.log('server started on: ' + server.info.uri);
});
});启动 hapi-app.js 之后,访问 http://localhost:3000/routes,你便可以看到下面这样的信息:
[
{
"path": "/routes",
"method": "GET",
"cors": false
},
{
"path": "/api/calculate/{operation}",
"method": "GET",
"cors": false
}
]这表示,我们已经成功的将模式匹配更新至 hapi 应用的路由中。访问 http://localhost:3000/api/calculate/sum?left=1&right=2 ,将得到结果:
{"answer":3}在上面的示例中,我们直接将 math 插件也加载到了 seneca 实例中,其实我们可以更加合理的进行这种操作,如 hapi-app-client.js 文件所示:
...
const seneca = Seneca()
.use(SenecaWeb, config)
.use('api')
.client({type: 'tcp', pin: 'role:math'})
.ready(() => {
const server = seneca.export('web/context')();
server.start(() => {
server.log('server started on: ' + server.info.uri);
});
});我们不注册 math 插件,而是使用 client 方法,将 role:math 发送给 math-pin-service.js 的服务,并且使用的是 tcp 连接,没错,你的微服务就是这样成型了。
注意:永远不要使用外部输入创建操作的消息体,永远显示地在内部创建,这可以有效避免注入攻击。
在上面的的初始化函数中,调用了一个 role:web 的模式操作,并且定义了一个 routes 属性,这将定义一个URL地址与操作模式的匹配规则,它有下面这些参数:
prefix:URL 前缀pin: 需要映射的模式集map:要用作 URL Endpoint 的 pin 通配符属性列表你的URL地址将开始于 /api/。
rol:api, path:* 这个 pin 表示,映射任何有 role="api" 键值对,同时 path 属性被定义了的模式,在本例中,只有 role:api,path:calculate 符合该模式。
map 属性是一个对象,它有一个 calculate 属性,对应的URL地址开始于:/api/calculate。
按着, calculate 的值是一个对象,它表示了 HTTP 的 GET 方法是被允许的,并且URL应该有参数化的后缀(后缀就类于 hapi 的 route 规则中一样)。
所以,你的完整地址是 /api/calculate/{operation}。
然后,其它的消息属性都将从 URL query 对象或者 JSON body 中获得,在本示例中,因为使用的是 GET 方法,所以没有 body。
SenecaWeb 将会通过 msg.args 来描述一次请求,它包括:
body:HTTP 请求的 payload 部分;query:请求的 querystring;params:请求的路径参数。现在,启动前面我们创建的微服务:
node math-pin-service.js --seneca.log=plugin:math然后再启动我们的应用:
node hapi-app.js --seneca.log=plugin:web,plugin:api访问下面的地址:
{"answer":6}{"answer":5}一个真实的系统,肯定需要持久化数据,在Seneca中,你可以执行任何您喜欢的操作,使用任何类型的数据库层,但是,为什么不使用模式匹配和微服务的力量,使你的开发更轻松?
模式匹配还意味着你可以推迟有关微服务数据的争论,比如服务是否应该"拥有"数据,服务是否应该访问共享数据库等,模式匹配意味着你可以在随后的任何时间重新配置你的系统。
seneca-entity 提供了一个简单的数据抽象层(ORM),基于以下操作:
load:根据实体标识加载一个实体;save:创建或更新(如果你提供了一个标识的话)一个实体;list:列出匹配查询条件的所有实体;remove:删除一个标识指定的实体。它们的匹配模式分别是:
load: role:entity,cmd:load,name:<entity-name>save: role:entity,cmd:save,name:<entity-name>list: role:entity,cmd:list,name:<entity-name>remove: role:entity,cmd:remove,name:<entity-name>任何实现了这些模式的插件都可以被用于提供数据库(比如 MySQL)访问。
当数据的持久化与其它的一切都基于相同的机制提供时,微服务的开发将变得更容易,而这种机制,便是模式匹配消息。
由于直接使用数据持久性模式可能变得乏味,所以 seneca 实体还提供了一个更熟悉的 ActiveRecord 风格的接口,要创建记录对象,请调用 seneca.make 方法。 记录对象有方法 load$、save$、list$ 以及 remove$(所有方法都带有 $ 后缀,以防止与数据字段冲突),数据字段只是对象属性。
通过 npm 安装 seneca-entity, 然后在你的应用中使用 seneca.use() 方法加载至你的 seneca 实例。
现在让我们先创建一个简单的数据实体,它保存 book 的详情。
文件 book.js
const seneca = require('seneca')();
seneca.use('basic').use('entity');
const book = seneca.make('book');
book.title = 'Action in Seneca';
book.price = 9.99;
// 发送 role:entity,cmd:save,name:book 消息
book.save$( console.log );在上面的示例中,我们还使用了 seneca-basic,它是 seneca-entity 依赖的插件。
执行上面的代码之后,我们可以看到下面这样的日志:
❯ node book.js
null $-/-/book;id=byo81d;{title:Action in Seneca,price:9.99}Seneca 内置了 mem-store,这使得我们在本示例中,不需要使用任何其它数据库的支持也能进行完整的数据库持久操作(虽然,它并不是真正的持久化了)。
由于数据的持久化永远都是使用的同样的消息模式集,所以,你可以非常简单的交互数据库,比如,你可能在开发的过程中使用的是 MongoDB,而后,开发完成之后,在生产环境中使用 Postgres。
下面让我他创建一个简单的线上书店,我们可以通过它,快速的添加新书、获取书的详细信息以及购买一本书:
module.exports = function(options) {
// 从数据库中,查询一本ID为 `msg.id` 的书,我们使用了 `load$` 方法
this.add('role:store, get:book', function(msg, respond) {
this.make('book').load$(msg.id, respond);
});
// 向数据库中添加一本书,书的数据为 `msg.data`,我们使用了 `data$` 方法
this.add('role:store, add:book', function(msg, respond) {
this.make('book').data$(msg.data).save$(respond);
});
// 创建一条新的支付订单(在真实的系统中,经常是由商品详情布中的 *购买* 按钮触
// 发的事件),先是查询出ID为 `msg.id` 的书本,若查询出错,则直接返回错误,
// 否则,将书本的信息复制给 `purchase` 实体,并保存该订单,然后,我们发送了
// 一条 `role:store,info:purchase` 消息(但是,我们并不接收任何响应),
// 这条消息只是通知整个系统,我们现在有一条新的订单产生了,但是我并不关心谁会
// 需要它。
this.add('role:store, cmd:purchase', function(msg, respond) {
this.make('book').load$(msg.id, function(err, book) {
if (err) return respond(err);
this
.make('purchase')
.data$({
when: Date.now(),
bookId: book.id,
title: book.title,
price: book.price,
})
.save$(function(err, purchase) {
if (err) return respond(err);
this.act('role:store,info:purchase', {
purchase: purchase
});
respond(null, purchase);
});
});
});
// 最后,我们实现了 `role:store, info:purchase` 模式,就只是简单的将信息
// 打印出来, `seneca.log` 对象提供了 `debug`、`info`、`warn`、`error`、
// `fatal` 方法用于打印相应级别的日志。
this.add('role:store, info:purchase', function(msg, respond) {
this.log.info('purchase', msg.purchase);
respond();
});
};接下来,我们可以创建一个简单的单元测试,以验证我们前面创建的程序:
// 使用 Node 内置的 `assert` 模块
const assert = require('assert')
const seneca = require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
.error(assert.fail)
// 添加一本书
addBook()
function addBook() {
seneca.act(
'role:store,add:book,data:{title:Action in Seneca,price:9.99}',
function(err, savedBook) {
this.act(
'role:store,get:book', {
id: savedBook.id
},
function(err, loadedBook) {
assert.equal(loadedBook.title, savedBook.title)
purchase(loadedBook);
}
)
}
)
}
function purchase(book) {
seneca.act(
'role:store,cmd:purchase', {
id: book.id
},
function(err, purchase) {
assert.equal(purchase.bookId, book.id)
}
)
}执行该测试:
❯ node book-store-test.js
["purchase",{"entity$":"-/-/purchase","when":1483607360925,"bookId":"a2mlev","title":"Action in Seneca","price":9.99,"id":"i28xoc"}]在一个生产应用中,我们对于上面的订单数据,可能会有单独的服务进行监控,而不是像上面这样,只是打印一条日志出来,那么,我们现在来创建一个新的服务,用于收集订单数据:
const stats = {};
require('seneca')()
.add('role:store,info:purchase', function(msg, respond) {
const id = msg.purchase.bookId;
stats[id] = stats[id] || 0;
stats[id]++;
console.log(stats);
respond();
})
.listen({
port: 9003,
host: 'localhost',
pin: 'role:store,info:purchase'
});然后,更新 book-store-test.js 文件:
const seneca = require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
.client({port:9003,host: 'localhost', pin:'role:store,info:purchase'})
.error(assert.fail);此时,当有新的订单产生时,就会通知到订单监控服务了。
通过上面的所有步骤,我们现在已经有四个服务了:
book-store-stats 与 math-pin-service 我们已经有了,所以,直接启动即可:
node math-pin-service.js --seneca.log.all
node book-store-stats.js --seneca.log.all现在,我们需要一个 book-store-service :
require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
.listen({
port: 9002,
host: 'localhost',
pin: 'role:store'
})
.client({
port: 9003,
host: 'localhost',
pin: 'role:store,info:purchase'
});该服务接收任何 role:store 消息,但同时又将任何 role:store,info:purchase 消息发送至网络,永远都要记住, client 与 listen 的 pin 配置必须完全一致。
现在,我们可以启动该服务:
node book-store-service.js --seneca.log.all然后,创建我们的 app-all.js,首选,复制 api.js 文件到 api-all.js,这是我们的API。
module.exports = function api(options) {
var validOps = {
sum: 'sum',
product: 'product'
}
this.add('role:api,path:calculate', function(msg, respond) {
var operation = msg.args.params.operation
var left = msg.args.query.left
var right = msg.args.query.right
this.act('role:math', {
cmd: validOps[operation],
left: left,
right: right,
}, respond)
});
this.add('role:api,path:store', function(msg, respond) {
let id = null;
if (msg.args.query.id) id = msg.args.query.id;
if (msg.args.body.id) id = msg.args.body.id;
const operation = msg.args.params.operation;
const storeMsg = {
role: 'store',
id: id
};
if ('get' === operation) storeMsg.get = 'book';
if ('purchase' === operation) storeMsg.cmd = 'purchase';
this.act(storeMsg, respond);
});
this.add('init:api', function(msg, respond) {
this.act('role:web', {
routes: {
prefix: '/api',
pin: 'role:api,path:*',
map: {
calculate: {
GET: true,
suffix: '/{operation}'
},
store: {
GET: true,
POST: true,
suffix: '/{operation}'
}
}
}
}, respond)
})
}最后, app-all.js:
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const config = {
adapter: require('seneca-web-adapter-hapi'),
context: (() => {
const server = new Hapi.Server();
server.connection({
port: 3000
});
server.route({
path: '/routes',
method: 'get',
handler: (request, reply) => {
const routes = server.table()[0].table.map(route => {
return {
path: route.path,
method: route.method.toUpperCase(),
description: route.settings.description,
tags: route.settings.tags,
vhost: route.settings.vhost,
cors: route.settings.cors,
jsonp: route.settings.jsonp,
}
})
reply(routes)
}
});
return server;
})()
};
const seneca = Seneca()
.use(SenecaWeb, config)
.use('basic')
.use('entity')
.use('math')
.use('api-all')
.client({
type: 'tcp',
pin: 'role:math'
})
.client({
port: 9002,
host: 'localhost',
pin: 'role:store'
})
.ready(() => {
const server = seneca.export('web/context')();
server.start(() => {
server.log('server started on: ' + server.info.uri);
});
});
// 创建一本示例书籍
seneca.act(
'role:store,add:book', {
data: {
title: 'Action in Seneca',
price: 9.99
}
},
console.log
)启动该服务:
node app-all.js --seneca.log.all从控制台我们可以看到下面这样的消息:
null $-/-/book;id=0r7mg7;{title:Action in Seneca,price:9.99}这表示成功创建了一本ID为 0r7mg7 的书籍,现在,我们访问 http://localhost:3000/api/store/get?id=0r7mg7 即可查看该ID的书籍详情(ID是随机的,所以,你生成的ID可能并不是这样的)。
http://localhost:3000/routes 可以查看所有的路由。
然后我们可创建一个新的购买订单:
curl -d '{"id":"0r7mg7"}' -H "content-type:application/json" http://localhost:3000/api/store/purchase
{"when":1483609872715,"bookId":"0r7mg7","title":"Action in Seneca","price":9.99,"id":"8suhf4"}访问 http://localhost:3000/api/calculate/sum?left=2&right=3 可以得到 {"answer":5}。
使用执行脚本撰写您的应用程序,不要害怕为不同的上下文使用不同的脚本,它们看上去应该很短,比如像下面这样:
var SOME_CONFIG = process.env.SOME_CONFIG || 'some-default-value'
require('seneca')({ some_options: 123 })
// 已存在的 Seneca 插件
.use('community-plugin-0')
.use('community-plugin-1', {some_config: SOME_CONFIG})
.use('community-plugin-2')
// 业务逻辑插件
.use('project-plugin-module')
.use('../plugin-repository')
.use('./lib/local-plugin')
.listen( ... )
.client( ... )
.ready( function() {
// 当 Seneca 启动成功之后的自定义脚本
})Lodash 是一个具有一致接口、模块化、高性能等特性的 JavaScript 工具库。
<script src="path/to/lodash.js"></script>require(['lodash', function( _ ) {
// do something with lodash _;
});npm i --save lodash// 加载整个模块
const _ = require('lodash');
// 或者者只加载某个分类下的所有方法(子模块)
const array = require('lodash/array');
// 或者只加载某个特定的方法
const chunk = require('lodash/array/chunk');_.chunk(array, [size=1])将 array 拆分成多个 size 长度的块,把这些块组成一个新数组。 如果 array 无法被分割成全部等长的块,那么最后剩余的元素将组成一个块。
array:Array 类型,需要被处理的数组;[size=1]:Number 类型,每个块的长度。Array 类型,返回一个包含拆分块数组的新数组(相当于一个二维数组)。_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']] 今天一早到公司,看着用了好多年的 Mac Terminal 有点不爽,就想着给改造了一下下,具体流程记录如下,要是万一哪天我又装了系统,可以直接拿来用了。
sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"或者:
sh -c "$(wget https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)"直接访问上面的链接地址下载即可,对于 OS X Terminal 的,直接下载之后,双击安装即可,如果觉得麻烦,也可以直接下载我打包好的:
Tomorrow OS X Terminal Color Schemas.zip
npm install --global pure-prompt安装完成之后,进入到 oh-my-zsh 的 custom 目录下,我的就是默认的,然后:
ln -s /path/to/pure.zsh 之后修改 oh-my-zsh 的主题为 pure
vi ~/.zshrc修改:
ZSH_THEME="pure"`git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting修改 ~/.zshrc
plugins = (...plugins zsh-syntax-highlighting) 很早以前,我就是一名不折不扣的富士用户,但是当然我永远都不是某个品牌的死忠粉,所以后来还玩了松下,索尼等等很多品牌,这次因为看上了富士的 56mm/1.2 镜头,想试一下下这种镜头到底如何,所以就想办法整了一台 X-T2,提前两天拿到手,参数如下:
别的也不多说了,直接上几张直出 JPEG 图,都说富士的色彩调教讨喜。








要开始构建你的第一个 React App,最简单的方法莫过于使用下面这两个 JSFiddle 示例了:
Create React App 是一个新的受官方支持的用于创建 React 单页面应用的工具,它提供了一个一些无需任何配置那可拿来即用的现代化构建工具,需要 Node 4 或者更高版本的支持。
但是需要注意的是,它还是有一些使用上的限制,而且它也仅仅只适用于单页面应用,如果你更高的灵活性或者将 React 整合到现有的项目中,那你可能就需要下面这些其它的解决方案了。
如果你才刚刚开始了解 React,那么下载 Starter kit 是另一个不错的选择, Starter kit 包含了预建的 React 以及 React Dom 示例复本。
在 Starter kit 的根目录下,创建一个名为 helloworld.html 的文件,包含以下的内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="build/react.js"></script>
<script src="build/react-dom.js"></script>
<script src="https://unpkg.com/babel-core@5.8.38/browser.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
</body>
</html>如上所示,这种在JavaScript 中包含 XML 语法的实现我们称之为 JSX,你可以查看 JSX 语法说明 以了解更多关于 JSX 的使用帮助,为了将其编译为浏览器可识别的 JavaScript 代码,我们使用了 <script type="text/babel">,此时 Babel 将直接在浏览器编译它,直接在浏览器中打开该页面,你就将看到应用已经执行了。
你的 React JSX 代码,还可以被分开存储在不同的文件中,创建一个 src/helloworld.js 文件:
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);然后在 helloworld.html 代码中引入该文件:
<script type="text/babel" src="src/helloworld.js"></script>这里需要注意一点,有一些浏览器(比如 Chrome),可能只允许通过 HTTP 协议访问文件。
npm 或者 Bower 中使用 React你同样还可以使用如 npm 或者 bower 这样的包管理工具, 这在后面的文章中会详细涉及到。
今天新入手了一台刚刚发布的富士 X-T2 相机以及一个 56mm/1.2 镜头,以此记录一下自己的腐败,不小心把相机的 ISO 调到了 25600,于是,照片就成了下面这样的了:

然后,其实我还开启了 Acros 模拟,比如上面这张图,相机直出的 JPEG 图片是下面这样的:

在本文中,我们将创建一个简单的用于管理自己的任务列表的应用,要创建一个新的 Meteor 项目,打开终端命令行工具,然后输入以下命令:
meteor create meteor-todos在会在当前的工作路径下创建一个名为 meteor-todos 的文件夹,该文件夹下面将包含以下文件:
client/main.js # 客户端加载 JS 的入口
client/main.html # 定义视频的 HTML 文件
client/main.css # 定义应用样式的 CSS 文件
server/main.js # 服务器端应用的入口文件
package.json # NPM 包管理工具使用的配置文件
.meteor # Meteor 文件
.gitignore # Git 配置文件要运行该应用,执行以下命令:
cd meteor-todos
meteor npm install
meteor执行成功之后,打开浏览器访问 http://localhost:3000即可看到新的应用,在进行下一步之前,你可以尝试修改一下 client/main.html 中 h1 标签的内容,你可以看到,当你保存文件之后,浏览器中的页面会自动的刷新。
关于设计转码,有一个更新被大家所认可的名称,叫作“切图”,在本文中我没有使用“切图”二字的原因在于,在我所理解的设计转码工作中,并不只是切图这么简单。
自从出现Web这个事物开始,每一个Web页面就会经历从产品需求到设计到出图到转码的这么一个过程,虽然技术变革给我们带来了很多炫酷的工具以帮助我们更加快速、方便的进行这样的工作,但是最终工具终归是工具,人,这一动作的主体永远都无法去掉:
上面是我们最常见的工作流程,而用到的工具最多的还是 Photoshop,对于切图工作者,用到得最多的又是 Photoshop 里面的 切片工具,它能快速的将一整张PSD图切割为很小的块,然后导出,再从导出的图片里面选取我们需要的使用即可(而在以前,很多人更是直接使用了 Photoshop 导出的HTML进行修改即直接应用到最终的产品代码中去)。
按上面这种工作流程,看似很平常不过了,也特别符合我们心理预期的工作流程,但是,却有很多问题:
这种方式所带来的最大的问题,并不是最终成品的效果不好,而是,整个工期太长,尤其是在响应式布局以及移动设备流行之后,更是在绝大多数情况下,压根就无法满足生产的需要了。
说起前面这些问题的解决办法,我想最多的,还是都没有解决最终问题,因为工作流程都没有想过要去改变,还是产品-设计-转码这样的流程,那这样的流程就决定了永远都存在切图这一个工种,而解决的办法就是提升这个流程中每一个环节的工作效率,比如各种各样的在线或者线下的工具,用于将导入的PSD自动按图层、分组等导出;或者制定PSD文件的制作标准(比如图层如何分组、名称应该如何定等),但是问题依旧存在。
对于新手,可能还是先使用Photoshop切出图片,然后再一边写着HTML一边写着CSS,最后缓慢的完成一个产品,而我自己也总结了一套自己的切图方式,还算比较快速:
这个方式在技术上一般是下面这样的书写步骤:
class 而不是ID<body id="page-name-id">
<header>
<h1>网页名称</h1>
<nav>
<a href="index.html" title="网站名称">首页</a>
<a href="about.html" title="关于我们">关于</a>
</nav>
</header>
<div class="columns">
<article class="news-item">
<h2>文章标题</h2>
<p>内容</p>
</article>
<aside>
...
</aside>
</div>
</body>而后,CSS中有可能有两份
body {...}
header {...}
article {...}
h2 { ... }
article h2 { ... }
...然后对于某个单独的页面
#page-name-id article {...}这上面这些工作都完成之后,就到了切图的工作了,但是与以前的切图工作不再一样的是,不再是使用切片工具将整个图都切出来,而是按需要将元素取出来,在很多,一张设计图只需要取出图标、背景、Banner等元素即可。
我一直都是属于那种比较偷懒的人,如果想要解决切图的问题,那最好就是压根就不存在这个问题,但是如何可以让我们的工作流程中规避掉?
改变现有工作的工具以及工作方式个人感觉应该是最好不过的方式了,大概的思路是:
1.遍历数组法
最简单的去重方法, 实现思路:新建一新数组,遍历传入数组,值不在新数组就加入该新数组中;注意点:判断值是否在数组的方法“indexOf”是ECMAScript5 方法,IE8以下不支持,需多写一些兼容低版本浏览器代码,源码如下:
// 最简单数组去重法
function unique1(array){
var n = []; //一个新的临时数组
//遍历当前数组
for(var i = 0; i < array.length; i++){
//如果当前数组的第i已经保存进了临时数组,那么跳过,
//否则把当前项push到临时数组里面
if (n.indexOf(array[i]) == -1) n.push(array[i]);
}
return n;
}// 判断浏览器是否支持indexOf ,indexOf 为ecmaScript5新方法 IE8以下(包括IE8, IE8只支持部分ecma5)不支持
if (!Array.prototype.indexOf){
// 新增indexOf方法
Array.prototype.indexOf = function(item){
var result = -1, a_item = null;
if (this.length == 0){
return result;
}
for(var i = 0, len = this.length; i < len; i++){
a_item = this[i];
if (a_item === item){
result = i;
break;
}
}
return result;
}
}该方法执行的速度比其他任何方法都快, 就是占用的内存大一些;实现思路:新建一js对象以及新数组,遍历传入数组时,判断值是否为js对象的键,不是的话给对象新增该键并放入新数组。注意点: 判断是否为js对象键时,会自动对传入的键执行“toString()”,不同的键可能会被误认为一样;例如: a[1]、a["1"] 。解决上述问题还是得调用“indexOf”。
// 速度最快, 占空间最多(空间换时间)
function unique2(array){
var n = {}, r = [], len = array.length, val, type;
for (var i = 0; i < array.length; i++) {
val = array[i];
type = typeof val;
if (!n[val]) {
n[val] = [type];
r.push(val);
} else if (n[val].indexOf(type) < 0) {
n[val].push(type);
r.push(val);
}
}
return r;
}还是得调用“indexOf”性能跟方法1差不多,实现思路:如果当前数组的第i项在当前数组中第一次出现的位置不是i,那么表示第i项是重复的,忽略掉。否则存入结果数组。
function unique3(array){
var n = [array[0]]; //结果数组
//从第二项开始遍历
for(var i = 1; i < array.length; i++) {
//如果当前数组的第i项在当前数组中第一次出现的位置不是i,
//那么表示第i项是重复的,忽略掉。否则存入结果数组
if (array.indexOf(array[i]) == i) n.push(array[i]);
}
return n;
}虽然原生数组的”sort”方法排序结果不怎么靠谱,但在不注重顺序的去重里该缺点毫无影响。实现思路:给传入数组排序,排序后相同值相邻,然后遍历时新数组只加入不与前一值重复的值。
// 将相同的值相邻,然后遍历去除重复值
function unique4(array){
array.sort();
var re=[array[0]];
for(var i = 1; i < array.length; i++){
if( array[i] !== re[re.length-1])
{
re.push(array[i]);
}
}
return re;
}源自外国博文,该方法的实现代码相当酷炫;实现思路:获取没重复的最右一值放入新数组。(检测到有重复值时终止当前循环同时进入顶层循环的下一轮判断)
// 思路:获取没重复的最右一值放入新数组
function unique5(array){
var r = [];
for(var i = 0, l = array.length; i < l; i++) {
for(var j = i + 1; j < l; j++)
if (array[i] === array[j]) j = ++i;
r.push(array[i]);
}
return r;
} 最近面试比较多,收集整理了一些常见的面试题,还算不错。
这是一个非常简单的问题,如果你用过JavaScript的话。你至少得知道一种方法。但是,尽管如此,根据我的经验,也有很多自称是JavaScript程序员的人说不知道如何回答这个问题。
new 关键字来调用函数。open/close 花括号。var o = {};你也可以继续提问,“使用new关键字,什么情况下创建对象?”但是,由于我只是想淘汰一些人,所以这些问题我会等到真正面试的时候去问。
这和“如何创建对象”是相同级别的问题。然而,也有一些人回答得了第一个问题,却不能回答这个问题。
用下面的代码,简简单单就能创建一个数组:
var myArray = new Array();创建数组是一个很复杂的过程。但是我希望能从应聘者口中听到使用方括号的答案。
var myArray = [];当然,我们还可以继续问其他问题,比如如何 高效地删除JavaScript数组中的重复元素 等,但是由于我们只需要知道应聘人员是否值得进一步的观察,关于数组的问题我会到此结束。
这个问题稍微难一点,我也并不要求对方一定得回答出来。但是,通过这个问题能够快速确定应聘者的技术水平:他们是否真的像他们声明得那样理解这门编程语言?
变量提升指的是,无论是哪里的变量在一个范围内声明的,那么JavaScript引擎会将这个声明移到范围的顶部。如果在函数中间声明一个变量,例如在某一行中赋值一个变量:
function foo() {
// 此处省略若干代码
var a = "abc";
}实际上会这样运行代码:
function foo() {
var a;
// 此处省略若干代码
a = "abc";
}全局变量的危险之处在于其他人可以创建相同名称的变量,然后覆盖你正在使用的变量。这在任何语言中都是一个令人头疼的问题。
预防的方法也有很多。其中最常用的方法是创建一个包含其他所有变量的全局变量:
var applicationName = {};然后,每当你需要创建一个全局变量的时候,将其附加到对象上即可。
applicationName.myVariable = "abc";还有一种方法是将所有的代码封装到一个自动执行的函数中,这样一来,所有声明的变量都声明在该函数的范围内。
(function(){
var a = "abc";
})();在现实中,这两种方法你可能都会用到。
for(var prop in obj){
// bonus points for hasOwnProperty
if(obj.hasOwnProperty(prop)){
// do something here
}
}闭包允许一个函数定义在另一个外部函数的作用域内,即便作用域内的其他东西都消失了,它仍可以访问该外部函数内的变量。如果应聘者能够说明,在 for/next 循环中使用闭包却不声明变量来保存迭代变量当前值的一些风险,那就应该给对方加分。
关于这个问题,其实我们只是想看看应聘人员是否真的做过JavaScript单元测试。这是一个开放式问题,没有特定的正确答案,不过对方至少得能讲述进程中的一些事情。
直接上代码了,只是为了记录一个自己常用的示例:
<ul class="actions-list">
<li class="action-item facebook">
Facebook
</li>
<li class="action-item google">
GooglePlus
</li>
<li class="action-item linkedin">
LinkedIn
</li>
<li class="action-item picasa">
Picasa
</li>
<li class="action-item twitter">
Twitter
</li>
<li class="action-item wikipedia">
Wikipedia
</li>
</ul>/* Flex box define */
.actions-list {
display: -webkit-box;
display: -webkit-inline-flex;
display: -moz-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
}
.actions-list .action-item {
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
-moz-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-transition: all 300ms ease;
-moz-transition: all 300ms ease;
-o-transition: all 300ms ease;
transition: all 300ms ease;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
}
/* Design: widths, colors, borders, etc... */
.actions-list {
margin: 0;
padding: 0;
}
.actions-list .action-item {
font-family: Helvetica, Arial, sans-serif;
font-weight: lighter;
cursor: pointer;
background-color: #66bbcc;
border-left: 1px solid rgba(0, 0, 0, 0.2);
color: #000000;
padding-left: 52px;
background-repeat: no-repeat;
background-position: left 10px center;
background-size: 32px;
line-height: 52px;
height: 52px;
max-width: 50px;
}
.actions-list .action-item:hover {
max-width: 150px;
background-color: #ff9966;
padding-right: 10px;
}
.actions-list .action-item:first-child {
border: none;
}
.facebook {
background-image: url(https://pub.ofcrab.com/usr/uploads/2016/06/3022132263.png);
}
.google {
background-image: url(https://pub.ofcrab.com/usr/uploads/2016/06/3814450726.png);
}
.linkedin {
background-image: url(https://pub.ofcrab.com/usr/uploads/2016/06/650013350.png);
}
.picasa {
background-image: url(https://pub.ofcrab.com/usr/uploads/2016/06/887720659.png);
}
.twitter {
background-image: url(https://pub.ofcrab.com/usr/uploads/2016/06/1628655408.png);
}
.wikipedia {
background-image: url(https://pub.ofcrab.com/usr/uploads/2016/06/305452685.png);
} pattern用于验证表单输入的内容,通常HTML5的type属性,比如email、tel、number、data类、url等,已经自带了简单的数据格式验证功能了,加上pattern后,前端部分的验证更加简单高效了。显而易见,pattern的属性值要用正则表达式。
简单的数字验证
数字的验证有两个:
<input type="number" pattern="\d">
<input type="number" pattern="[0-9]*"> 对表单验证来说,这两个正则的作用是一样的,表现的话差异就很大:
pattern的用法都一样,这里不再啰嗦各种详细写法了,只是列出来一些常用的正则就好了:
[0-9]{13,16}^62[0-5]\d{13,16}$^4[0-9]{12}(?:[0-9]{3})?$^5[1-5][0-9]{14}$[1-9][0-9]{4,14}^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$^([0-9]){7,18}(x|X)?$^[a-zA-Z]\w{5,17}$ 字母开头,长度在6~18之间,只能包含字母、数字和下划线^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$ 包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间但很不幸,pattern的浏览器支持很惨:
翠鸟冰谷位于延庆县最北部,是北京原始地貌独立发起的原始河谷,因谷内不时传来鸟鸣而得名。尤其此谷没有其它的户外团队涉足,所以其景色还没有破坏,谷内水量很大,但是水流缓慢,所以,相比于其它河谷来讲,其谷内冬天冰层更厚更硬,很适合冬天休闲踏冰。
本文最下面提供了本次踏冰全程记录视频,有兴趣的朋友可以看看。
一直对北方这种路这种景很有感觉,还遇到一队骑行的。


下面就是一些随拍记录的照片,有兴趣的慢慢看。


































