魑魅魍魉 发布的文章

以前还没有在 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-common

修改配置文件

sudo 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 操作权限

重启 nfs 服务

sudo /etc/init.d/nfs-kernel-server restart

到此,nfs 的服务就搭建好了

客户端访问服务器

  1. 检查客户端与服务端的网络是否连通

    ping 192.168.3.3
  2. 查看服务端的共享目录设置

    showmount -e 192.168.3.3
    
    Export list for 192.168.3.3:
    /data/nfs *
  3. 将该目录挂载到本地

    mount 192.168.3.3:/data/nfs /path/to/local/folder
  4. 访问

    cd /path/to/local/folder

前几天把家里这台用了五六年的台式机的系统,从 Windows 10 换成了 Elementary.IO 了,一个 Ubuntu 的衍生版本,使用起来,更接近 Mac 的 OSX 系统一些,本文记录了一下整个电脑工作环境的配置过程。

安装系统

这个很简单了,直接下载了 Elementary 系统之后,使用官方文档里面说明的U盘刻录工具,直接将 ISO 刻录至U盘,然后在电脑上使用U盘直接安装即可,安装过程很简单。

这里面有一个小问题,安装完了之后,我使用是 TP-Link 的 WN7200 这款WIFI网卡,这个系统好像没有驱动,最后我想了一最笨的办法解决的,买了一条20米的网线直接走了一个线,到了电脑边上,然后 再加了一个无线路由器,一来提升一下自己房间的网络速度,二来,不用再去纠结网卡驱动的问题。

安装工作软件

  • Visual Studio Code
  • Chromium 浏览器
  • Git
  • Docker
  • MySQL
  • Nginx
  • MySQL Workbench

上面这些软件反正都是直接找了官方文档安装的,还是很简单的,要安装其它的软件,系统自带有一个App Store,也可以直接使用这个工具安装。

整体来讲,这个系统除了微信跟QQ外,其它的功能基本上都可以了,哦,不对,还没有找到一个很好的图片处理软件。

不过好处是,我准备把台式机的硬盘都换一下下,换成几块大硬盘,直接做成一个家庭的 NAS 服务器。

第一章 HTML 基础

第一节 初级知识

  1. HTML 是什么?

    • 基本概念
    • 发展历程
  2. 元素
  3. 属性
  4. <head>
  5. <body>
  6. 常用元素
  7. 表格
  8. 表单
  9. 列表
  10. 多媒体与嵌入内容
  11. 链接
  12. 全局属性
  13. 注释
  14. 常见 HTML 问题解答

第二节 高级应用

  1. 复杂表单

    • 多选框
    • <fieldset>
    • 文本域
    • 下拉菜单
    • <input> 类型
  2. 复杂表格

    • 合并单元格
    • <colgroup>
    • <thead><tbody 以及 <tfoot>
  3. 语义
  4. 焦点管理
  5. 预加载
  6. 多媒体
  7. CORS
  8. 内联元素与块级元素
  9. 其它HTML知识

第二章 CSS 级联样式表

第一节 基础知识

  1. CSS 是什么?

    • 基本概念
    • 发展历程
  2. 语法
  3. 选择器
  4. 值与单位
  5. 层叠和继承
  6. 盒模型
  7. 常见 CSS 属性
  8. CSS 调试

第二节 高级应用

  1. 文本样式化
  2. 盒样式化
  3. 布局
  4. 初始值、计算值、应用值和当前值
  5. 浮动
  6. 定位
  7. 弹性盒子
  8. 文档流
  9. 块级元素与内联元素的特性
  10. 函数
  11. 伪类
  12. 伪元素
  13. 背景
  14. 颜色
  15. 阴影
  16. 边框
  17. 渐变
  18. 媒体查询
  19. 响应式

第三节 过渡与动画

  1. 基础知识
  2. 2D 与 3D
  3. keyframes
  4. animation

第四节 CSS 库

  1. Bootstrap
  2. Semantic UI
  3. Magic CSS3 Animation
  4. Buttons
  5. Single Element CSS Spinner

第五节 CSS 编程

  1. SASS/SCSS
  2. LESS
  3. Stylus
  4. Postcss

第六节 移动设备

第七节 实践

  1. 企业官方网站
  2. 论坛
  3. 社交平台
  4. 新闻门户
  5. 创意单页面网站

第三章 JavaScript

第一节 基础知识

  1. 什么是编程语言?
  2. 什么是 JavaScript?
  3. 初涉 JavaScript

    • 运行环境与浏览器
    • 以程序员的方式去思考
    • 猜数游戏
  4. 存储你的数据 - 变量
  5. 数字与数学运算
  6. 字符串
  7. 数组
  8. 条件
  9. 循环
  10. 函数
  11. 事件
  12. Promise

第二节 高阶知识

  1. 对象基础
  2. 原型
  3. 继承
  4. JSON
  5. 函数

    • 参数
    • 返回值
    • 表达式
    • 数据类型
    • 声明提升
    • 递归
    • 作用域
    • 局部变量与全局变量
    • 作用域链
    • 闭包
  6. 数组

    • 遍历
    • 合并与拆分
    • 元素删除、插入与替换
    • 排序
    • 与字符串互转
    • mapreduceforEachfilter 等方法
  7. 正则

    • 概念
    • 精确匹配
    • 预定义特殊字符
    • 字符集
    • 修饰符
    • 边界
    • 预定义类
    • 量词
    • 分组
    • 操作符
    • 分组的反向引用
    • 中文
    • 常用正则表达式
  8. IIFE
  9. setIntervalsetTimeout
  10. 模块化

第三节 客户端 Web API

  1. Web API 简介
  2. 文档对象模型
  3. 基础的 DOM 操作
  4. 创建并放置节点
  5. 移动或删除节点
  6. 操作样式
  7. window 对象
  8. 客户端数据存储
  9. 事件

第四节 客户端与服务器端通信

  1. HTTP 协议
  2. XMLHttpRequest 与 Ajax
  3. Fetch

第五节 第三方 API

  1. 高德地图 API
  2. 百度地图 API
  3. Google 地图 API
  4. 微信 API

第六节 Web 端图形绘制

Canvas

SVG

MathML

第七节 Web 端多媒体 API

  1. 视频 API
  2. 音频 API

第八节 Web 端设备 API

  1. 地理位置
  2. 网络信息
  3. 抖动
  4. 摄像头

第九节 JavaScript 库

  1. jQuery
  2. Lodash

第十节 ECMAScript 6

第十一节 JavaScript 框架

基础知识

  1. 基本概念
  2. 发展与趋势

AngularJS

ReactJS

VueJS

第四章 NodeJS

第一节 基础知识

  1. NodeJS 是什么?
  2. 开发环境配置
  3. 单线程
  4. 非阻塞I/O
  5. 事件驱动
  6. 高并发、IO密集型
  7. 模块

    • 基本概念
    • 内置模块与自定义模块
    • http
    • url
    • path
    • fs
  8. npm 命令
  9. yarn 命令

第二节 高阶应用

  1. WebSocket

第三节 NodeJS 框架

  1. ExpressJS
  2. KoaJS
  3. HapiJS

第四节 实现自己的 NodeJS 框架

第五章 TypeScript

第六章 工程化

第一节 Gulp

第二节 Webpack

第三节 Fis3

安装 nvm

curl -o- https://raw.githubusercontent/creationx/nvm/v0.33.2.install.sh | bash

安装默认版本NODE

nvm install node

安装其它版本node

nvm install node --version

安装 brew

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装 mongodb

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.plist

安装 docker

brew install docker
brew install docker-machine
brew install docker-compose

Seneca :NodeJS 微服务框架入门指南

Seneca 是一个能让您快速构建基于消息的微服务系统的工具集,你不需要知道各种服务本身被部署在何处,不需要知道具体有多少服务存在,也不需要知道他们具体做什么,任何你业务逻辑之外的服务(如数据库、缓存或者第三方集成等)都被隐藏在微服务之后。

这种解耦使您的系统易于连续构建与更新,Seneca 能做到这些,原因在于它的三大核心功能:

  1. 模式匹配:不同于脆弱的服务发现,模式匹配旨在告诉这个世界你真正关心的消息是什么;
  2. 无依赖传输:你可以以多种方式在服务之间发送消息,所有这些都隐藏至你的业务逻辑之后;
  3. 组件化:功能被表示为一组可以一起组成微服务的插件。

在 Seneca 中,消息就是一个可以有任何你喜欢的内部结构的 JSON 对象,它们可以通过 HTTP/HTTPS、TCP、消息队列、发布/订阅服务或者任何能传输数据的方式进行传输,而对于作为消息生产者的你来讲,你只需要将消息发送出去即可,完全不需要关心哪些服务来接收它们。

然后,你又想告诉这个世界,你想要接收一些消息,这也很简单,你只需在 Seneca 中作一点匹配模式配置即可,匹配模式也很简单,只是一个键值对的列表,这些键值对被用于匹配 JSON 消息的极组属性。

在本文接下来的内容中,我们将一同基于 Seneca 构建一些微服务。

模式( Patterns

让我们从一点特别简单的代码开始,我们将创建两个微服务,一个会进行数学计算,另一个去调用它:

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 实例中,它有两个参数:

  1. pattern :用于匹配 Seneca 实例中 JSON 消息体的模式;
  2. action :当模式被匹配时执行的操作

seneca.act 方法同样有两个参数:

  1. msg :作为纯对象提供的待匹配的入站消息;
  2. respond :用于接收并处理响应信息的回调函数。

让我们再把所有代码重新过一次:

seneca.add('role:math, cmd:sum', (msg, reply) => {
  reply(null, { answer: ( msg.left + msg.right )})
});

在上面的代码中的 Action 函数,计算了匹配到的消息体中两个属性 leftright 的值的和,并不是所有的消息都会被创建一个响应,但是在绝大多数情况下,是需要有响应的, Seneca 提供了用于响应消息的回调函数。

在匹配模式中, role:math, cmd:sum 匹配到了下面这个消息体:

{
  role: 'math',
  cmd: 'sum',
  left: 1,
  right: 2
}

并得到计自结果:

{
  answer: 3
}

关于 rolecmd 这两个属性,它们没有什么特别的,只是恰好被你用于匹配模式而已。

接着,seneca.act 方法,发送了一条消息,它有两个参数:

  1. msg :发送的消息主体
  2. response_callback :如果该消息有任何响应,该回调函数都会被执行。

响应的回调函数可接收两个参数: errorresult ,如果有任何错误发生(比如,发送出去的消息未被任何模式匹配),则第一个参数将是一个 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,因为 bc 字母的前面;
  • a: 1, b: 2, d: 4 优先于 a: 1, c: 3, d:4,因为 bc 字母的前面;
  • a: 1, b:2, c:3 优先于 a:1, b: 2,因为它有更多的属性;
  • a: 1, b:2, c:3 优先于 a:1, c:3,因为它有更多的属性。

很多时间,提供一种可以让你不需要全盘修改现有 Action 函数的代码即可增加它功能的方法是很有必要的,比如,你可能想为某一个消息增加更多自定义的属性验证方法,捕获消息统计信息,添加额外的数据库结果中,或者控制消息流速等。

我下面的示例代码中,加法操作期望 leftright 属性是有限数,此外,为了调试目的,将原始输入参数附加到输出的结果中也是很有用的,您可以使用以下代码添加验证检查和调试信息:

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 函数接受两个参数:

  1. msg:消息体
  2. 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 方法接受两个参数:

  1. plugin :插件定义函数或者一个插件名称;
  2. 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 加载了很多内置的插件,比如 basictransportweb 以及 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 去扩展可以得到一样的效果,它需要两个参数:

  1. pin :模式匹配模式
  2. action :扩展的 action 函数

pin 是一个可以匹配到多个模式的模式,它可以匹配到多个模式,比如 role:math 这个 pin 可以匹配到 role:math, cmd:sumrole:math, cmd:product

在上面的示例中,我们在最后面的 wrap 函数中,确保了,任何传递给 role:math 的消息体中 leftright 值都是数字,即使我们传递了字符串,也可以被自动的转换为数字。

有时,查看 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.listenseneca.client 方法都可以接受下面这些参数,以达到定抽的功能:

  • port :可选的数字,表示端口号;
  • host :可先的字符串,表示主机名或者IP地址;
  • spec :可选的对象,完整的定制对象
注意:在 Windows 系统中,如果未指定 host, 默认会连接 0.0.0.0,这是没有任何用处的,你可以设置 hostlocalhost

只要 clientlisten 的端口号与主机一致,它们就可以进行通信:

  • seneca.client(8080) → seneca.listen(8080)
  • seneca.client(8080, '192.168.0.2') → seneca.listen(8080, '192.168.0.2')
  • seneca.client({ port: 8080, host: '192.168.0.2' }) → seneca.listen({ port: 8080, host: '192.168.0.2' })

Seneca 为你提供的 无依赖传输 特性,让你在进行业务逻辑开发时,不需要知道消息如何传输或哪些服务会得到它们,而是在服务设置代码或配置中指定,比如 math.js 插件中的代码永远不需要改变,我们就可以任意的改变传输方式。

虽然 HTTP 协议很方便,但是并不是所有时间都合适,另一个常用的协议是 TCP,我们可以很容易的使用 TCP 协议来进行数据的传输,尝试下面这两个文件:

math-service-tcp.js :

require('seneca')()
  .use('math')
  .listen({type: 'tcp'})

math-client-tcp.js

require('seneca')()
  .client({type: 'tcp'})
  .act('role:math,cmd:sum,left:1,right:2',console.log)

默认情况下, client/listen 并未指定哪些消息将发送至哪里,只是本地定义了模式的话,会发送至本地的模式中,否则会全部发送至服务器中,我们可以通过一些配置来定义哪些消息将发送到哪些服务中,你可以使用一个 pin 参数来做这件事情。

让我们来创建一个应用,它将通过 TCP 发送所有 role:math 消息至服务,而把其它的所有消息都在发送至本地:

math-pin-service.js

require('seneca')()

  .use('math')

  // 监听 role:math 消息
  // 重要:必须匹配客户端
  .listen({ type: 'tcp', pin: 'role:math' })

math-pin-client.js

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;
  • levelDEBUGINFOWARNERROR 以及 FATAL 中任何一个;
  • type:条目编码,比如 actplugin 等;
  • plugin:插件名称,不是插件内的操作将表示为 root$
  • case: 条目的事件:INADDOUT
  • action-id/transaction-id:跟踪标识符,_在网络中永远保持一致_;
  • pinaction 匹配模式;
  • message:入/出参消息体

如果你运行上面的进程,使用了 --seneca.log.all,则会打印出所有日志,如果你只想看 math 插件打印的日志,可以像下面这样启动服务:

node math-pin-service.js --seneca.log=plugin:math

Web 服务集成

Seneca不是一个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 的值是一个对象,它表示了 HTTPGET 方法是被允许的,并且URL应该有参数化的后缀(后缀就类于 hapiroute 规则中一样)。

所以,你的完整地址是 /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

访问下面的地址:

数据持久化

一个真实的系统,肯定需要持久化数据,在Seneca中,你可以执行任何您喜欢的操作,使用任何类型的数据库层,但是,为什么不使用模式匹配和微服务的力量,使你的开发更轻松?

模式匹配还意味着你可以推迟有关微服务数据的争论,比如服务是否应该"拥有"数据,服务是否应该访问共享数据库等,模式匹配意味着你可以在随后的任何时间重新配置你的系统。

seneca-entity 提供了一个简单的数据抽象层(ORM),基于以下操作:

  • load:根据实体标识加载一个实体;
  • save:创建或更新(如果你提供了一个标识的话)一个实体;
  • list:列出匹配查询条件的所有实体;
  • remove:删除一个标识指定的实体。

它们的匹配模式分别是:

  • loadrole:entity,cmd:load,name:<entity-name>
  • saverole:entity,cmd:save,name:<entity-name>
  • listrole:entity,cmd:list,name:<entity-name>
  • removerole: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

下面让我他创建一个简单的线上书店,我们可以通过它,快速的添加新书、获取书的详细信息以及购买一本书:

book-store.js

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();
  });
};

接下来,我们可以创建一个简单的单元测试,以验证我们前面创建的程序:

boot-store-test.js

// 使用 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"}]

在一个生产应用中,我们对于上面的订单数据,可能会有单独的服务进行监控,而不是像上面这样,只是打印一条日志出来,那么,我们现在来创建一个新的服务,用于收集订单数据:

book-store-stats.js

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-statsmath-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}

最佳 Seneca 应用结构实践

推荐你这样做

  • 将业务逻辑与执行分开,放在单独的插件中,比如不同的Node模块、不同的项目甚至同一个项目下不同的文件都是可以的;
  • 使用执行脚本撰写您的应用程序,不要害怕为不同的上下文使用不同的脚本,它们看上去应该很短,比如像下面这样:

    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 启动成功之后的自定义脚本
      })
  • 插件加载顺序很重要,这当然是一件好事,可以主上你对消息的成有绝对的控制权。

不推荐你这样做

  • 将 Seneca 应用的启动与初始化同其它框架的启动与初始化放在一起了,永远记住,保持事务的简单;
  • 将 Seneca 实例当做变量到处传递。

Lodash 是一个具有一致接口、模块化、高性能等特性的 JavaScript 工具库。

官网

https://lodash.com

安装

浏览器

<script src="path/to/lodash.js"></script>

AMD Loader

require(['lodash', function( _ ) {
  // do something with lodash _;
});

使用 NPM

npm i --save lodash

NodeJS/IOJS

// 加载整个模块
const _ = require('lodash');
// 或者者只加载某个分类下的所有方法(子模块)
const array = require('lodash/array');
// 或者只加载某个特定的方法
const chunk = require('lodash/array/chunk');

功能、方法介绍以及使用示例

_.chunk(array, [size=1])

array 拆分成多个 size 长度的块,把这些块组成一个新数组。 如果 array 无法被分割成全部等长的块,那么最后剩余的元素将组成一个块。

参数

  1. arrayArray 类型,需要被处理的数组;
  2. [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 有点不爽,就想着给改造了一下下,具体流程记录如下,要是万一哪天我又装了系统,可以直接拿来用了。

1. 安装 zsh + oh-my zsh

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 -)"

2. 安装 Tomorrow OS X Terminal Color Schemas

直接访问上面的链接地址下载即可,对于 OS X Terminal 的,直接下载之后,双击安装即可,如果觉得麻烦,也可以直接下载我打包好的:

Tomorrow OS X Terminal Color Schemas.zip

3. 安装 Pure

npm install --global pure-prompt

安装完成之后,进入到 oh-my-zshcustom 目录下,我的就是默认的,然后:

ln -s /path/to/pure.zsh 

之后修改 oh-my-zsh 的主题为 pure

vi ~/.zshrc

修改:

ZSH_THEME="pure"`

4. 安装 zsh-syntax-highlighting

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)

眼看着这一个月就可过完了,发现这个月的博客都还没有写,为了不让Month Archive断,所以,就还是赶紧着来发篇文章,这个月回了个家,去了几次户外,然后出了个差,感觉就是很忙,现在还在加班中。

很早以前,我就是一名不折不扣的富士用户,但是当然我永远都不是某个品牌的死忠粉,所以后来还玩了松下,索尼等等很多品牌,这次因为看上了富士的 56mm/1.2 镜头,想试一下下这种镜头到底如何,所以就想办法整了一台 X-T2,提前两天拿到手,参数如下:

  • X-Trans™* CMOS III传感器与X-Processor Pro
  • 更优秀的自动对焦性能表现
  • 全新AF-C自定义设置
  • 全新EVF(0.77倍放大倍率,0.005秒快门时滞,100帧/秒刷新率实时取景)
  • 全新4K视频拍摄
  • 全新增能竖拍手柄(另购)
  • 全天候机身

别的也不多说了,直接上几张直出 JPEG 图,都说富士的色彩调教讨喜。

_DSF0084.jpg

_DSF0087.jpg

_DSF0090.jpg

_DSF0094.jpg

_DSF0099.jpg

_DSF0106.jpg

_DSF0111.jpg

_DSF0114.jpg

要开始构建你的第一个 React App,最简单的方法莫过于使用下面这两个 JSFiddle 示例了:

Create React App

Create React App 是一个新的受官方支持的用于创建 React 单页面应用的工具,它提供了一个一些无需任何配置那可拿来即用的现代化构建工具,需要 Node 4 或者更高版本的支持。

但是需要注意的是,它还是有一些使用上的限制,而且它也仅仅只适用于单页面应用,如果你更高的灵活性或者将 React 整合到现有的项目中,那你可能就需要下面这些其它的解决方案了。

Starter Pack

如果你才刚刚开始了解 React,那么下载 Starter kit 是另一个不错的选择, Starter kit 包含了预建的 React 以及 React Dom 示例复本。

下载 Starter kit 15.3.1

在 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,于是,照片就成了下面这样的了:

DSCF0013.jpg

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

DSCF0013.jpg

西峰河谷,位于河北省保定市涞水县,未经任何商业开发,风景秀丽,是一条长约 20 余里的原始峡谷,2016 年 9 月 3 日,随原始地貌登山队再次穿越徒步(上次穿越还是 2016 年 1 月 30 号),与上次不一样的是,此次正值夏末秋初,河水未结冰,槙被也还生机盎然,天气也不是!很冷,所以,这次是玩水。

_1020184.jpg

下车入山后,大约是一条两公里的道路。

_1020189.jpg

有商业头脑的当地村民直接在入口处摆上了一个小摊位,这里离镇区近 1.5 公里,就这一家,生意肯定也还是很不错的,不过这也预示这这条河谷即将被破坏,我的猜想最后也没有错,这是一条原始地貌登山队独立开发的原始路线,直到我们上次穿越的时候,这条河谷还几乎就只有我们自己队走,但是这次听说,在当天我们都已经是第三批人了。

_DSC9040.jpg

_DSC9254.jpg

_DSC9272.jpg

创建项目

在本文中,我们将创建一个简单的用于管理自己的任务列表的应用,要创建一个新的 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.htmlh1 标签的内容,你可以看到,当你保存文件之后,浏览器中的页面会自动的刷新。

LRM_EXPORT_20160821_195900.jpg

这是 2016 年 8 月 21 日,带着表弟去的一躺山西太白巍山,太白巍山位于山西省大同市灵丘县红石塄乡与武灵镇交界处,是太行山支脉,海拔2234米,周长60.5公里。由于海拔较高,云霄也只是山的围裙,登上山顶,灵丘县城却在云层之下。山顶从秋末到次年初春都覆盖着皑皑白雪,蔚为壮观。即使盛夏登顶也需穿着厚外套。由于主峰形状酷似于一个躺着的睡美人而名闻天下。

太白巍山距离北岳恒山和佛教名山五台山的直线距离都在70公里范围内。晴天登顶即拿起望远镜,即可望见五台山。山北便是灵丘县城和唐河公园。山下原有军用机场一座,飞机训练不断,现在仅剩下空旷机场。

太白巍山原谓之"太白驿山",为古代县衙武官放马之牧地,系太行山脉。古时灵丘曾有过太白金星庙,据县志补志,也载有"太白书院",可见巳名留千古,但至今对该山名称来历无法追溯。太白巍山,属土石山,山上草木丛生,松柏翠绿, 桦蔡树繁茂。整个山体巍然屹立,山势特点为东西展示,南北倾斜,是全县最大的一座山。山顶较为平缓,好胜者登巅极目,广灵,灵丘、 涞源、繁峙等地尽收眼底。夏季:全山碧绿,百花尽卉,芳香烩口,清凉爽人,实为避暑胜地;冬季:冰封雪飘,银装素裹,一派北国风光,古人称”杰出群峰表,南瞻太白峰;云来悬锦幔,雨过濯美蓉;飞瀑流寒玉,虚岩响石钟;移情晴倚栏, 翠色入重重"。山顶北侧有一神泉,过去泉旁有古寺一座,素称“有影无踪天堂寺",为灵丘九景之一。该山还是一座万宝俱全的天然宝库,自然资源丰富,地下矿藏较多,贮有大量的云母,石英、铁、煤等。

LRM_EXPORT_20160821_212937.jpg

一行人早上 5 点从灵丘县城酒店出发,车程大约 40 分钟即到达登山起点,上图是所有参与人员,全队 21 人。

LRM_EXPORT_20160821_212713.jpg

太早,月亮都还没有下山,美丽的格桑花又入了我的眼,喜欢这样的花,喜欢这样望着月亮的花

LRM_EXPORT_20160821_212620.jpg

LRM_EXPORT_20160821_212601.jpg

大家都在修整自己的装备,由于此山比较原始,所有,司机师傅还会在出发点等待两小时,以防有人回退。

LRM_EXPORT_20160821_212547.jpg

透过格桑花,已经可以隐隐约约看到,东方的太阳即将出来,预示着今天又是一个好天气。

LRM_EXPORT_20160821_212456.jpg

虫儿也出来散步了。

LRM_EXPORT_20160821_212441.jpg

LRM_EXPORT_20160821_212322.jpg

LRM_EXPORT_20160821_212046.jpg

许多年没有见过的牵牛花,这次也有幸看到了。

LRM_EXPORT_20160821_212024.jpg

收拾完行装之后,一行人身着大山出发。

进山之路

跋山总是伴着涉水,如果去到一座有水的山,总是会让我幸福感爆棚,比如说,太白巍山。

LRM_EXPORT_20160821_211648.jpg

太白巍山进山之后,会走过很长的一段涉水路,在小溪流中来来回回,别有一翻滋味。

LRM_EXPORT_20160821_210959.jpg

整个流水都不是很深,基本上登山鞋都可以直接涉水而过。

LRM_EXPORT_20160821_210950.jpg

风哥这是第二次来了,听说,上次来这里,最后是次日的早上五点才到家,想想也是,这种大强度的登山活动,又是风扇兄带队,晚八九个小时到家很正常嘛。

LRM_EXPORT_20160821_210911.jpg

LRM_EXPORT_20160821_210837.jpg

LRM_EXPORT_20160821_210715.jpg

这里的水跟上个月去的西藏的水是不一样的,西藏那边的水几乎都是冰川或者雪山融化之后流下来的,而这里的水都是山体积养的水。

LRM_EXPORT_20160821_210702.jpg

LRM_EXPORT_20160821_210617.jpg

路上偶遇一位干完农活回来的农夫,让我想起自己当年也是一样,一双夹板拖鞋就能上山入水的,而今,登个小山都得穿上几千块的登山鞋,唉,这也是一个直接原因,导致了我后来去天云山的时候直接上夹板拖了。

LRM_EXPORT_20160821_210606.jpg

LRM_EXPORT_20160821_210235.jpg

LRM_EXPORT_20160821_210146.jpg

上面这位就是我的亲表弟,小鲜肉一枚

LRM_EXPORT_20160821_210130.jpg

这位就是人见人爱的风哥。

LRM_EXPORT_20160821_205657.jpg

LRM_EXPORT_20160821_205433.jpg

一弯水

LRM_EXPORT_20160821_205228.jpg

LRM_EXPORT_20160821_205136.jpg

收队,和尚,总是这么逗 B。

LRM_EXPORT_20160821_204957.jpg

而友谊的小船说启航就可以启航了。

LRM_EXPORT_20160821_204828.jpg

LRM_EXPORT_20160821_204052.jpg

已经进山了,来张兄弟合影。

LRM_EXPORT_20160821_204027.jpg

LRM_EXPORT_20160821_203959.jpg

上面这位就是我们的领队,风扇兄。

LRM_EXPORT_20160821_203947.jpg

LRM_EXPORT_20160821_203857.jpg

LRM_EXPORT_20160821_203821.jpg

LRM_EXPORT_20160821_203803.jpg

LRM_EXPORT_20160821_203556.jpg

过了这个小瀑布之后,水路结束,接下来的路程,就是第一个小高潮了。

攀登

LRM_EXPORT_20160821_203522.jpg

攀登路段应该说是整个这次登山行程中最危险的,几乎都是在十几二十米左右高度(有的地方会更高)的悬崖边上走过,当然了,登山的过程中,越危险的地方,其实更加安全,因为会让人的注意力更加集中,像这种不是纯石头的悬崖,最大的危险不在于它的高,而在于长在悬崖边上的好地些植物,很容易让人踩空,这个时候,每一次下脚都不要全力,给自己留一手。

LRM_EXPORT_20160821_203403.jpg

最美的风景,总是需要你付出更多,随着高度的上升,我们的视野也越来越开阔,现在还是早上,城市里的人可能还在睡梦之中,而大山也还没有完全苏醒,身着有太阳的方向望去,层峦叠嶂,好一幅山水画。

LRM_EXPORT_20160821_203250.jpg

LRM_EXPORT_20160821_203143.jpg

LRM_EXPORT_20160821_203037.jpg

在半山腰上,有一块小地方,稍稍平缓一些,前队一行人在这里临时休憩片刻。

LRM_EXPORT_20160821_203009.jpg

谁又能想到,她的身后就是悬崖呢。

LRM_EXPORT_20160821_202917.jpg

我们前队继续赶路之后,后队由和尚带着也在同一个地方休憩,从这个角度,应该就可以看到当时的位置了,如果没有那些树木,我相信应该没有几个人愿意在那里多待一秒钟吧。

LRM_EXPORT_20160821_202854.jpg

LRM_EXPORT_20160821_202827.jpg

LRM_EXPORT_20160821_202649.jpg

LRM_EXPORT_20160821_202633.jpg

LRM_EXPORT_20160821_202621.jpg

LRM_EXPORT_20160821_202609.jpg

LRM_EXPORT_20160821_202302.jpg

LRM_EXPORT_20160821_202333.jpg

同样的景,越往上走,看到的越壮观。

LRM_EXPORT_20160821_201932.jpg

到达一个小平台,从这里开始,我们将进入今天的第三段行程。

LRM_EXPORT_20160821_201854.jpg

穿越灌木林

在此行的一开始,领队就说,灌木林中有一段需要放绳的路,大约是一个六米左右高度的悬崖,需要有绳才能下去,不过,感觉还是很不错的,最后却被我们自己走着走着,绕过去了。

LRM_EXPORT_20160821_201729.jpg

LRM_EXPORT_20160821_201836.jpg

LRM_EXPORT_20160821_201711.jpg

LRM_EXPORT_20160821_201539.jpg

LRM_EXPORT_20160821_201423.jpg

LRM_EXPORT_20160821_201302.jpg

LRM_EXPORT_20160821_201005.jpg

灌木林里,已经有一条羊倌们走出来的路了,所以,没有任何难度的穿过了,以一支小水流和一个小断崖结束。

LRM_EXPORT_20160821_200818.jpg

表弟还满满的都是笑容,身后的那一个大斜坡就是本日行程的第四段了。

LRM_EXPORT_20160821_200700.jpg

400 米干拔

第四段之后,就可以到达顶峰了,这是一段特别奇幻的路程,直线距离看过去应该不足 300 米,我们走了近两公里,走之字形路,坡度却 60 度,上升 400 多米。

LRM_EXPORT_20160821_200635.jpg

LRM_EXPORT_20160821_200536.jpg

LRM_EXPORT_20160821_200524.jpg

LRM_EXPORT_20160821_200512.jpg

LRM_EXPORT_20160821_200451.jpg

难度大不大呢?可以从各位的笑容中看出一二。

LRM_EXPORT_20160821_200442.jpg

LRM_EXPORT_20160821_200245.jpg

LRM_EXPORT_20160821_200207.jpg

LRM_EXPORT_20160821_200057.jpg

LRM_EXPORT_20160821_195953.jpg

上升到这个位置之前,我们是看不到身后的山的,本以为我们在下面看到的顶就是山顶,可是当我们到达这块平地之后,绝望啊,后面的那个越更长,更高,看上去似乎十几分钟就能到顶,最终,最长的硬生生的走了一个半小时。

LRM_EXPORT_20160821_195931.jpg

此时已经可以看到灵丘县城了,我们今天早上就是从那个地方过来的。

LRM_EXPORT_20160821_195900.jpg

视野更大了。

LRM_EXPORT_20160821_195736.jpg

排掉一点垃圾之后,我们继续前进。

LRM_EXPORT_20160821_195648.jpg

LRM_EXPORT_20160821_195632.jpg

继续往上的路,也就是最后的两百米上升是最难受的,由于体能的原因,最先到顶的人与最后到顶的人,时间差了近一个半小时,而且,越往上,坡度越大,同时这是一个高山草甸,基本上都是踩不实的地儿。

LRM_EXPORT_20160821_195602.jpg

LRM_EXPORT_20160821_195552.jpg

登山才能望远,越来越接近山顶,看到的风景也越来越壮观。

LRM_EXPORT_20160821_195422.jpg

LRM_EXPORT_20160821_195407.jpg

LRM_EXPORT_20160821_195314.jpg

不要看着他一脸笑容,我可能有人格担保,那都是装出来了,照片拍完之后,立马就开始大口喘气。

LRM_EXPORT_20160821_195209.jpg

这位大神也不例外 ,哈哈。

LRM_EXPORT_20160821_194953.jpg

LRM_EXPORT_20160821_194941.jpg

LRM_EXPORT_20160821_194804.jpg

LRM_EXPORT_20160821_194645.jpg

LRM_EXPORT_20160821_194103.jpg

看背后的山,我们就要登顶。

LRM_EXPORT_20160821_194021.jpg

LRM_EXPORT_20160821_194012.jpg

LRM_EXPORT_20160821_193807.jpg

LRM_EXPORT_20160821_193756.jpg

LRM_EXPORT_20160821_193647.jpg

上顶之后,必须来一张潘式时间定格照。

LRM_EXPORT_20160821_193624.jpg

而这位和尚大人,听说拉了五天肚子,所以,今天最后一位登顶的就是他了,虽然以前他也都是最后一个登顶的,但是以前的原因是,他是收队啊,今天的原因是,他真的不行哪。

LRM_EXPORT_20160821_193528.jpg

LRM_EXPORT_20160821_193508.jpg

LRM_EXPORT_20160821_193450.jpg

LRM_EXPORT_20160821_193440.jpg

LRM_EXPORT_20160821_193407.jpg

LRM_EXPORT_20160821_193316.jpg

LRM_EXPORT_20160821_193210.jpg

LRM_EXPORT_20160821_193345.jpg

全队合照。

下山

上山不容易,下山其实更容易出现事故,也不是一条容易的道儿,吃完休息好之后,打个包,下山。

LRM_EXPORT_20160821_192948.jpg

LRM_EXPORT_20160821_192934.jpg

LRM_EXPORT_20160821_192700.jpg

LRM_EXPORT_20160821_192636.jpg

LRM_EXPORT_20160821_192540.jpg

从山顶下往下 100 米时,还都是高山草甸,基本上没有什么难度,而再往下之后,问题就来了,全是半人高的杂草,没有路,全靠瞎踩出来,后面的人就跟着一直走,期间总是会有松动的石头,或者被草盖住了的坑,一脚下去,很容易摔倒。

LRM_EXPORT_20160821_192520.jpg

LRM_EXPORT_20160821_192509.jpg

LRM_EXPORT_20160821_192458.jpg

LRM_EXPORT_20160821_192306.jpg

LRM_EXPORT_20160821_192040.jpg

下山走的是另一条道,风景照样很不错。

LRM_EXPORT_20160821_192014.jpg

LRM_EXPORT_20160821_191925.jpg

LRM_EXPORT_20160821_191913.jpg

LRM_EXPORT_20160821_191644.jpg

LRM_EXPORT_20160821_191609.jpg

LRM_EXPORT_20160821_191534.jpg

LRM_EXPORT_20160821_191518.jpg

LRM_EXPORT_20160821_191358.jpg

LRM_EXPORT_20160821_191327.jpg

LRM_EXPORT_20160821_191240.jpg

LRM_EXPORT_20160821_191224.jpg

LRM_EXPORT_20160821_191132.jpg

LRM_EXPORT_20160821_191110.jpg

LRM_EXPORT_20160821_191050.jpg

LRM_EXPORT_20160821_191018.jpg

LRM_EXPORT_20160821_191018.jpg

LRM_EXPORT_20160821_190944.jpg

下午约四点左右,到达山脚下,花了不到一个半小时,此时又有了流水,赶紧脱鞋,让脚丫子洗个澡,水很凉,不过洗完之后,那叫一个舒服哪。

出山

从山底到达中巴车的位置,大约还有六公里的路程,不过这段路程已经完全没有难度了,就是延着小溪流一直往外走即可,有防火道。

LRM_EXPORT_20160821_190918.jpg

LRM_EXPORT_20160821_190851.jpg

LRM_EXPORT_20160821_190813.jpg

LRM_EXPORT_20160821_190750.jpg

LRM_EXPORT_20160821_190735.jpg

LRM_EXPORT_20160821_190715.jpg

LRM_EXPORT_20160821_190706.jpg

LRM_EXPORT_20160821_190645.jpg

恩,你没看错,我跟表弟两人一直是走在最前面的,因为一天的时间手机都没有信号,所以,只想着一路赶到车边,然后发个朋友圈哪,但是一路上看到这样的水潭太多太多,实在是没有忍住,所以,就下了一躺水,很爽。

LRM_EXPORT_20160821_190631.jpg

下午六点整到达沟掌村,全程 15.80 公里,上升 1193 米,历时 12 个小时整,期间休息时间大约占用了三个小时。

篇外

LRM_EXPORT_20160821_194922.jpg

LRM_EXPORT_20160821_194909.jpg

LRM_EXPORT_20160821_194605.jpg

LRM_EXPORT_20160821_194543.jpg

LRM_EXPORT_20160821_194534.jpg

LRM_EXPORT_20160821_194128.jpg

LRM_EXPORT_20160821_194031.jpg

LRM_EXPORT_20160821_193201.jpg

LRM_EXPORT_20160821_193038.jpg

LRM_EXPORT_20160821_192529.jpg

LRM_EXPORT_20160821_191344.jpg

LRM_EXPORT_20160821_191426.jpg

LRM_EXPORT_20160821_193819.jpg

LRM_EXPORT_20160821_194835.jpg

LRM_EXPORT_20160821_195005.jpg

LRM_EXPORT_20160821_195036.jpg

LRM_EXPORT_20160821_195107.jpg

LRM_EXPORT_20160821_195137.jpg

LRM_EXPORT_20160821_195152.jpg

LRM_EXPORT_20160821_195620.jpg

LRM_EXPORT_20160821_200116.jpg

LRM_EXPORT_20160821_200348.jpg

LRM_EXPORT_20160821_190631.jpg

全程回顾

关于设计转码,有一个更新被大家所认可的名称,叫作“切图”,在本文中我没有使用“切图”二字的原因在于,在我所理解的设计转码工作中,并不只是切图这么简单。

长久以来我们怎么做?

自从出现Web这个事物开始,每一个Web页面就会经历从产品需求到设计到出图到转码的这么一个过程,虽然技术变革给我们带来了很多炫酷的工具以帮助我们更加快速、方便的进行这样的工作,但是最终工具终归是工具,人,这一动作的主体永远都无法去掉:

  1. 产品提出需求,制作产品交互原型;
  2. 设计师根据产品的交互原型制作精美的设计图;
  3. 切图工作者将设计图转换成HTML静态页面。

上面是我们最常见的工作流程,而用到的工具最多的还是 Photoshop,对于切图工作者,用到得最多的又是 Photoshop 里面的 切片工具,它能快速的将一整张PSD图切割为很小的块,然后导出,再从导出的图片里面选取我们需要的使用即可(而在以前,很多人更是直接使用了 Photoshop 导出的HTML进行修改即直接应用到最终的产品代码中去)。

长久以来我们又有什么样的问题?

按上面这种工作流程,看似很平常不过了,也特别符合我们心理预期的工作流程,但是,却有很多问题:

  1. 交互原型在很大程度上已经给设计定型了,比如交互原型里面会指出哪里有按钮,哪里是一个列表,哪里有一个表格,哪里放一个Banner等;
  2. 设计师很多时候仅仅只是美化交互原型,比如将按钮改个配色,调整一下元素的尺寸,画一个漂亮的Banner,精细的设计师会让自己的设计图精确到一个像素,这就是我们常说的像素级精确的设计;
  3. 而切图工作者在将设计转码的过程中,为了保证还原度,很多时候也很无奈的只能使用切图工具将设计图导出为一些图片切片,然后再用代码拼接到整个页面中,硬性的将Photoshop里面的图层对应到CSS里面的Layers中,然后定位也像设计图版的像素级的去调整。

这种方式所带来的最大的问题,并不是最终成品的效果不好,而是,整个工期太长,尤其是在响应式布局以及移动设备流行之后,更是在绝大多数情况下,压根就无法满足生产的需要了。

后来大家都又想过什么样的解决办法?

说起前面这些问题的解决办法,我想最多的,还是都没有解决最终问题,因为工作流程都没有想过要去改变,还是产品-设计-转码这样的流程,那这样的流程就决定了永远都存在切图这一个工种,而解决的办法就是提升这个流程中每一个环节的工作效率,比如各种各样的在线或者线下的工具,用于将导入的PSD自动按图层、分组等导出;或者制定PSD文件的制作标准(比如图层如何分组、名称应该如何定等),但是问题依旧存在。

对于新手,可能还是先使用Photoshop切出图片,然后再一边写着HTML一边写着CSS,最后缓慢的完成一个产品,而我自己也总结了一套自己的切图方式,还算比较快速:

  1. 先浏览所有设计图,找出设计图中通用的布局与元素样式,先将这些基础的元素转为代码(此时基本上不需要切图,因为绝大多数情况下都能直接使用CSS样式化出来同样或者特别接近的效果);
  2. 使用上一步完成的基础的样式库一般都完成所有页面的布局与样式工作;
  3. 再对每一页的特殊元素进行特殊处理。

这个方式在技术上一般是下面这样的书写步骤:

  1. 先看图直接完成所有元素与布局的HTML结构;
  2. 完成HTML结构之后,再完成CSS样式;
  3. HTML中,基础元素全部使用 class 而不是ID
  4. ID 直接应用到整个Page。

关于上面所用方式的一段代码示例

<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. 版本管理: Git/Gitlab
  2. 文档编写: Markdown / Office Word
  3. 交互原型与设计: Sketch / Photoshop

基本工作流程

  1. 产品人员出产品需求文档,详细描述产品功能细节;
  2. 交互设计使用Sketch制作交互原型;
  3. 设计师使用直接美化Sketch交互原型,若有需要细化设计的图片,使用Photoshop单独制作并导入Sketch文档;
  4. 前端开发人员直接按照Sketch进行开发,不需要切图流程,直接使用Sketch导出所有资源文档,设计中关于颜色、尺寸的测量等直接以Sketch中的样式标注为准即可。

2016年7月9日-10日,参加了由北京野狼户外徒步登山队组织的北京首次夜穿库布齐沙漠的活动,包括来回车程在内,总共用了近36个小时,这是我人生中第一次进入沙漠,第一次穿越沙漠,还是夜穿,感觉很带劲儿。

我们是9日早上七点四十从北京西直门出发,经河北到内蒙古,大巴于9日下午6:40到达库布齐沙漠西侧,一直往东穿越至响沙湾景区,总路程18公里,历时12个小时后,于早上七点到达等候我们的大巴上回程。

_DSC0629.jpg

_DSC0618.jpg

_DSC0598.jpg

_DSC0697.jpg

_DSC0867.jpg

还人生中第一次拍下了自己的银河照,感觉还过得去吧。

_DSC0845.jpg

_DSC0846.jpg

_DSC0901.jpg

_DSC0889.jpg

全程视频记录

直接在下方难看,或者进入视频网站 http://v.qq.com/x/page/y0312n9l2c5.html 观看.

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;
  }
}

2.对象键值对法

该方法执行的速度比其他任何方法都快, 就是占用的内存大一些;实现思路:新建一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;
}

3.数组下标判断法

还是得调用“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;
}

4.排序后相邻去除法

虽然原生数组的”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;
}

5.优化遍历数组法

源自外国博文,该方法的实现代码相当酷炫;实现思路:获取没重复的最右一值放入新数组。(检测到有重复值时终止当前循环同时进入顶层循环的下一轮判断)

// 思路:获取没重复的最右一值放入新数组
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;
}

最近面试比较多,收集整理了一些常见的面试题,还算不错。

1.创建JavaScript对象的两种方法是什么?

这是一个非常简单的问题,如果你用过JavaScript的话。你至少得知道一种方法。但是,尽管如此,根据我的经验,也有很多自称是JavaScript程序员的人说不知道如何回答这个问题。

  • 使用 new 关键字来调用函数。
  • open/close 花括号。
var o = {};

你也可以继续提问,“使用new关键字,什么情况下创建对象?”但是,由于我只是想淘汰一些人,所以这些问题我会等到真正面试的时候去问。

2.如何创建数组?

这和“如何创建对象”是相同级别的问题。然而,也有一些人回答得了第一个问题,却不能回答这个问题。

用下面的代码,简简单单就能创建一个数组:

var myArray = new Array();

创建数组是一个很复杂的过程。但是我希望能从应聘者口中听到使用方括号的答案。

var myArray = [];

当然,我们还可以继续问其他问题,比如如何 高效地删除JavaScript数组中的重复元素 等,但是由于我们只需要知道应聘人员是否值得进一步的观察,关于数组的问题我会到此结束。

3.什么是变量提升(Variable Hoisting)?

这个问题稍微难一点,我也并不要求对方一定得回答出来。但是,通过这个问题能够快速确定应聘者的技术水平:他们是否真的像他们声明得那样理解这门编程语言?

变量提升指的是,无论是哪里的变量在一个范围内声明的,那么JavaScript引擎会将这个声明移到范围的顶部。如果在函数中间声明一个变量,例如在某一行中赋值一个变量:

function foo() {
    // 此处省略若干代码
    var a = "abc";
}

实际上会这样运行代码:

function foo() {
    var a;
    // 此处省略若干代码
    a = "abc";
}

4.全局变量有什么风险,以及如何保护代码不受干扰?

全局变量的危险之处在于其他人可以创建相同名称的变量,然后覆盖你正在使用的变量。这在任何语言中都是一个令人头疼的问题。

预防的方法也有很多。其中最常用的方法是创建一个包含其他所有变量的全局变量:

var applicationName = {};

然后,每当你需要创建一个全局变量的时候,将其附加到对象上即可。

applicationName.myVariable = "abc";

还有一种方法是将所有的代码封装到一个自动执行的函数中,这样一来,所有声明的变量都声明在该函数的范围内。

(function(){
   var a = "abc";
})();

在现实中,这两种方法你可能都会用到。

5.如何通过JavaScript对象中的成员变量迭代?

for(var prop in obj){
    // bonus points for hasOwnProperty
    if(obj.hasOwnProperty(prop)){
        // do something here
    }
}

6.什么是闭包(Closure)?

闭包允许一个函数定义在另一个外部函数的作用域内,即便作用域内的其他东西都消失了,它仍可以访问该外部函数内的变量。如果应聘者能够说明,在 for/next 循环中使用闭包却不声明变量来保存迭代变量当前值的一些风险,那就应该给对方加分。

7.请描述你经历过的JavaScript单元测试。

关于这个问题,其实我们只是想看看应聘人员是否真的做过JavaScript单元测试。这是一个开放式问题,没有特定的正确答案,不过对方至少得能讲述进程中的一些事情。