分类目录归档:默认

做个 SSO 系统

我写的东西 编程相关 Node posts 编程相关

应用场景

最近公司要统一内部管理系统的用户与权限控制,于是考虑开发一个 SSO 单点登陆系统,考虑到内部系统都是 web 服务和开发时间有限,而且内部系统都是 Node.js 写的, 使用 Koa 框架,最终决定使用最简单的解决方案。

实现方案

原理

kwt + cookie
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,这里将作为用户的登陆标识。
用户的登陆流程是这样的:

  1. 打开接入方系统,假设域名是 aaa.abc.com
  2. 校验登陆状态,查看 cookie 里是否存在 jwt 并解密校验,如果未登录,跳转到 SSO 登陆页面,SSO 域名是 sso.abc.com
  3. 在 SSO 登陆页面填写用户名和密码(未注册的先注册),登陆后 SSO 系统把 jwt 写入 cookie,cookie 的有效域名为 abc.com,然后跳跳转回原系统(跳过来的时候在 url 里带上原系统地址)
  4. 返回原系统,校验登陆状态,登陆完成

sso 系统实现

基本的用户管理功能,有:

  • 部门管理
  • 权限管理
  • 注册功能
  • 登陆功能
  • 找回密码

重点功能是登陆,需要在登陆时将 jwt 写入 cookie,上文原理中有要求各个系统要有个共同的主域名 abc.com,各个系统不同域的话,那就只能把 jwt 在 url 中返回给原系统了,原系统自己把 jwt 写入 cookie,来看看这部分的代码:

const jwtdata = {
username: user.username,
email: user.email,
role: user.role
}
// 生成 jwt
const token = jwt.sign(jwtdata, Const.TOKEN_SECRET, {expiresIn: '365d'})
// 写入 cookie
ctx.cookies.set(
'token',
token,
{
domain: '.abc.com', // 写cookie所在的域名
path: '/', // 写cookie所在的路径
maxAge: 30 * 60 * 1000, // cookie有效时长
httpOnly: false, // 是否只用于http请求中获取
overwrite: true // 是否允许重写
}
)

注意用于加密 jwt 的密匙,内部系统的话可以直接用一个密匙加密,然后接入方都用这个密匙解密就可以了。
如果安全要求高一点,那就是 sso 系统用公钥(保密)加密 jwt,然后接入方使用私钥(公开)解密。

接入方实现

考虑到接入方有多个,所以考虑直接写个 Koa 中间件给各个接入方调用是最简单的,将此中间件作为 npm 包发布,接入方使用该中间件监听所有请求,完成对 jwt 的解析和校验。

监控请求

const Koa = require('koa')
const app = new Koa()
const auth = require('sso-auth')
app.use(auth.validate({
unless: [//register/, //login/, //message/],
errHandle: async function (ctx) {
console.log('授权错误')
// 跳转到SSO的登陆页面或者 返回url给前端跳转
ctx.body = {
code: 3,
message: '授权错误',
url: 'https://sso.abc.com'
}
ctx.status = 200
}
}))

中间件 sso-auth 的内部实现

// 从 cookie 取出 jwt
// 校验 jwt 是否符合

假设接入方是其他编程语言的系统,那就对该语言实现对应的上述校验逻辑即可。

总结

jwt 作为 token 可以附带额外的信息
使用 cookie 存储 token ,同域名的系统接入非常方便
使用中间件方便了接入方校验 token

Node.js 并发模型

我写的东西 编程相关 Node posts 编程相关

前言

首先,什么是并发?

并发是指程序可以同时处理多个任务,是一个web 服务必备的能力。

自从 Nodejs 出现后,js 开始涉及后端领域,因为其出色的并发模型,被很多企业用来处理高并发请求,例如淘宝已经大量使用 node 处理中间层业务。

接下来本文就分析一下 js 的并发模型,来理解 node 相对于其他语言的优势以及其最合适的应用场景

tips:并发和并行区别

异步IO

什么是异步IO?
异步 IO 具体是如何实现的呢?
异步和同步有什么区别呢?
异步就不阻塞吗?IO 阻塞又是什么概念呢?
带着这些问题,我们慢慢分析。

IO 模型

《UNIX网络编程:卷一》第六章——I/O复用。书中向我们提及了5种类UNIX下可用的I/O模型:

  • 阻塞式I/O;
  • 非阻塞式I/O;
  • I/O复用(select,poll,epoll…);
  • 信号驱动式I/O(SIGIO);
  • 异步I/O(POSIX的aio_系列函数);

总结一下阻塞,非阻塞,同步,异步

  • 阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待;
  • 同步,异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。

说人话

上面的解释太复杂我看不懂怎么办?我们把上文说到 IO 代入到生活的场景中,考虑到我们公司很多人喜欢买一点点饮料,就以买饮料为例,将四种常见 IO 模型转换为对应的买饮料流程。下面是一下设定:

  • 把买一杯一点点的流程简化为两步:下单制作和拿一点点回公司
  • 公司员工 === 线程
  • 下单制作 === 发起IO请求
  • 拿一点点回公司 === 读取数据

异步 IO 的特点就是把 IO 处理的事情都交给了操作系统(美团外卖),这样线程就不会被 IO 阻塞,可以继续处理其他请求

Node 的异步IO

Node.js 的异步 IO 由 Libuv 这个库提供实现,Libuv 是 Node.js 关键的一个组成部分,它为上层的 Node.js 提供了统一的 API 调用,使其不用考虑平台差距,隐藏了底层实现。

可以看出,它提供了非阻塞的网络 I/O,异步文件系统访问等功能,而且右下角居然还有个线程池,实际上 Libuv 收到的 IO 请求是同个多线程来实现的, 看来 Node 只是在程序层面单线程而已

事件循环

任务队列

先看看 Node.js 结构

根据上图,Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎(callback 处理结果)。

(4)V8引擎再将结果返回给用户。

异步操作都会被压入任务队列,当任务队列为空的时候,程序退出。

示例代码

总结

Libuv 使用异步IO + 线程池实现的事件循环处理机制提供的高效的 IO 处理,是 Node 能承担高并发请求的主要原因

参考文章与书籍

《深入浅出Node.js》
《Unix 网络编程》
《七周七并发模型》
并发与并行的区别
JavaScript 运行机制详解:再谈Event Loop
怎样理解阻塞非阻塞与同步异步的区别? – 大姚的回答 – 知乎
Node.js 探秘:初识单线程的 Node.js
Linux IO模式及 select、poll、epoll详解

UML

我有哪些提升编程工作效率的方法

1. 列出每日TODO

最简单也是最有效的方法,每天早上整理好今天待办事项,按照紧急和重要两个指标划分四象限,优先处理重要的事情,紧急但不重要的可次之。
这个做法的好处是做事会有计划有条理。

2. 做好工作笔记

使用云笔记记录每个 task 的相关信息,整理素材。云笔记的好处是不怕丢,手机也能方便查询,也方便日后回顾,特别是年终的时候 看着一个个 task 记录,就知道今年做了哪些事情。

3. 坚决地自动化

如果是重复简单的工作,就尽量使用程序脚本解决,可能第一次做自动化,写脚步的时间比直接做还慢,但是基础的脚本都差不多,需要用到自动化的一般就是那几类,有了积累之后,下次使用就方便很多了。珍惜自己的时间,重视自己的价值,工程师不应该把时间浪费在简单重复的事情上。

4. 熟练使用工具,节省时间和精力

人一天的精力有限,而且人能连续集中精力时间也不长,番茄工作法认为人平均能集中精力的时长为 25 分钟,并以此为周期,每个周期之间需要休息 5 分钟。所以在编码时,需要熟悉 IDE 的各种快捷功能,提高工作。

举个例子:使用 IDE 提供的 debug 功能,可以在断点处之前查看上下文变量,还可以修改上下文,也可以给短点设置变量,debug 效率非常高。

使用 console.log 来 debug 同样可以找出问题,但需要浪费时间在输入 console.log 和重试上面。即使只有几秒钟,毕竟一个周期也只有 25 分钟,而且我自己感觉一天能高效工作的时间也就 3 小时左右 LOL。

5. 先思考,再编码,多画图

不要把实现方案想个大概就动工,不然写到一半才发现某些地方没考虑完整,又得推翻部分设计,返工浪费的时间其更多。
处理简单的问题,可以脑子里过一遍流程,把可能影响的地方都一一列出来,再整理好 TODO ,编码的时候其实就只需要关注把 TODO 翻译成代码即可。
如果问题比较复杂,那就把画出流程图或者时序图,可以帮你有效地整理流程。画图还有另一个好处:当做问题,日后接收的人也轻松。这里推荐使用 plantUML 来画各种流程图,类似 markdown 只需要写好文本,渲染后就是标准的 UML 图了。

IDE 常用功能 for Node.js

我写的东西 编程相关 Node posts

什么是 IDE

集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序。使用 nodejs 开发后端应用的时候,我的常用 IDE 是 Webstorm,偶尔也用 vs code。
根据我的日常使用经验,本文列举下 Webstorm 这个 IDE 提供了哪些比较实用功能,这些功能基本上 vs code 也支持。

编辑

快捷键

导出快捷键列表,仔细过一遍,

自动补全

自动补全功能的实现一般有两种

  1. 正则匹配,自动匹配出现过的字符
  2. 语法树分析,因为 js 没有类型系统,IDE 都做不到这个,但是 typescript 是可以的,vs code 也会根据 js 库里的 d.ts 描述文件来做自动补全

Refactor 重构

常见重构支持:
重命名
Inline : 表达式替换变量
Extract :变量替换表达式

模板功能

把常见的代码块设置为模板,方便快速输入。
例如把 console.log('$message$); 这类常用语句定义为模板后定义关键字为 log ,只要输入 log 补全即可。

TODO 列表

列出项目里的所有 TODO,时刻提醒你还债~

eslint / tslint

集成 eslint / tslint 等 js 代码检查工具,错误代码直接标红

运行

build

支持 typescript flow 等语言的编译

runer

run 配置

debug

debug 支持,断点比 console.log 清晰,也不会留下一堆乱七八糟的 log 代码,而且断点的设置还支持条件过滤,非常方便

unit test

清晰的 test 结果

coverage

代码覆盖率直接显示到编辑器上,如下图,line 37-39 左侧是淡绿色,表示测试覆盖到了,下方的淡红色表明测试未覆盖

deploy

支持 ftp 上传,IDE 也是可以直接在服务器编程的
就是自动帮你执行 ftp 上传命令

terminal

当前目录开启 terminal

版本控制

支持主流的版本控制软件

git svn csv

commit

除了 GUI commit 视图

还有 before commit 和 after commit 钩子

review

review 视图帮助你清晰了解变更

revert

撤销更改

冲突解决

解决代码冲突,gui 视图,会清晰很多

shelve 暂存

临时切换任务使用,就不用提交一个 “暂存” commit 了

changelist/task

任务汇总,整理变更

history

  1. version control 的 history 查看历史变更
  2. Annotate,查锅神器,同 git blame ,可以看到每一行代码的最后编辑人
  3. local history 防丢失非常重要

集成外部软件

有名的外部软件基本都能找到集成,我常用的有

jira

直接获取 task ,根据 task 信息创建分支,自动填充 commit 信息

plantuml

有插件,将 uml 渲染成图片

总结

上文列举了后端开发中比较实用的功能,但每个人的工作习惯都不一样,如何知道有哪些功能是你很喜欢的但是你还没发现呢?

仔细过一遍 IDE 的菜单就好了

Node.js 异步编程

我写的东西 编程相关 Node posts

异步的最终解决方案

在 2017 年,Node 7.6 终于支持了 Async/Await ,async 函数就是 Generator 函数的语法糖,是 JS 异步编程的最终解决方案。

可以认为:

async 函数 == co + generator 函数

比起 co,async 有以下优点

  1. 更好的语义 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果;
  2. 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)

在此之前,JS 的异步编程经历了 Callback、Promise、Generator、Async 的进化,接下来我们过一遍异步发展历程

回调函数 Callback

在 JS 中,异步编程通过 Callback 完成,将一个函数作为另一个异步函数的参数,用于处理异步结果,一个例子:

Something.save(function(err) {
if (err) {
//error handling
return; // 没有返回值
}
console.log('success');
});

过度使用回调函数所会遇到的挑战:

  • 如果不能合理的组织代码,非常容易造成回调地狱(callback hell),这会使得你的代码很难被别人所理解。
  • 很容易遗漏错误处理代码。
  • 无法使用return语句返回值,并且也不能使用throw关键字

也正是基于这些原因,在JavaScript世界中,一直都在寻找着能够让异步JavaScript开发变得更简单的可行的方案。

一个可行的解决方案之一是async模块。如果你和回调函数打过很久的交道, 你也许会深刻地感受到,在JavaScript中如果想要让某些事并行执行,或是串行执行,甚至是使用异步函数来映射(mapping) 数组中的元素使用异步函数有多复杂。所以,感谢 Caolan McMahon写了async模块来解决这些问题。

使用async模块,你可以轻松地以下面这种方式编写代码:

async.map([1, 2, 3], AsyncSquaringLibrary.square,
function(err, result){
// result will be [1, 4, 9]
});

async模块虽然一定程度上带来了便利,但仍然不够简单,代码也不容易阅读,因此Promise出现了。

Promise 函数

Promise 的写法:

Something.save()
.then(function() {
console.log('success');
})
.catch(function() {
//error handling
})

then 和 catch 注册的回调函数分别处理下一步处理和异常处理,这样写的优点是可以链式操作:

saveSomething()
.then(updateOtherthing)
.then(deleteStuff)
.then(logResults);

只是回调函数的另一种写法,把回调函数的横向加载,改成纵向加载,缺点是代码一堆的 then。

Generator 函数

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权,注意它不是语法糖。

第一步,协程A开始执行。
第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
第三步,(一段时间后)协程B交还执行权。
第四步,协程A恢复执行。
function* gen(x){
var y = yield x + 2;
return y;
}

上面代码就是一个 Generator 函数。它不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。Generator 函数的执行方法如下。

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换错误处理机制

  1. 数据交换:g.next(data);

  2. 错误处理:g.throw('出错了');

Generator 最大的问题是要手动调用 next() 才会执行下一步,因此自动执行器 co 出现了

co 执行器

co 函数库的用法:

var co = require('co');
co(gen);

Generator 自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权
两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise对象,用 then 方法交回执行权。

co 函数的具体实现见文末参考文章,这里就不重复了

拓展阅读:其他语言的异步编程

C# 也有 async await 关键字,用于异步调用,内部实现基于线程
http://www.cnblogs.com/jesse2013/p/async-and-await.html

Java Spring 框架有 @Async 注解,用于异步调用,内部实现基于线程
https://spring.io/guides/gs/async-method/

参考文章

  1. 细说JavaScript异步函数发展历程
  2. async 函数的含义和用法
  3. Generator 函数的含义与用法
  4. co 函数库的含义和用法

Node.js 内存模型

我写的东西 编程相关 Node posts 编程相关

前言

本文尝试理清 js 内存模型的相关知识点,鉴于 js 的教程非常丰富,这里就不重复写了,只建立个知识索引就好了,详细知识看文末的参考文章即可

栈与堆

基础数据类型存在栈中,对象存储在堆中

  1. 基础数据类型

    • Undefined
    • Null
    • Boolean
    • Number
    • String
  2. 引用类型
    Object、Function、Array和自定义的对象,可以看做是指针。指针是存在栈中,但是指向的变量在堆中

下面代码表现了基础类型和引用类型的区别

// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时 a 的值是多少? // 20
// demo02.js
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// 这时 m.a 的值是多少? // 15

执行上下文

概念

每次当控制器转到ECMAScript可执行代码的时候,就会进入到一个执行上下文。可执行代码的类型包括:

  • 全局代码(Global code)
    这种类型的代码是在”程序”级处理的:例如加载外部的js文件或者本地标签内的代码。全局代码不包括任何function体内的代码。 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
  • 函数代码(Function code)
  • Eval代码(Eval code)

执行栈 demo

建立的细节

1、创建阶段【当函数被调用,但未执行任何其内部代码之前】

  • 创建作用域链(Scope Chain)

  • 创建变量,函数和参数。

  • 求”this“的值

2、执行阶段
初始化变量的值和函数的引用,解释/执行代码。

我们可以将每个执行上下文抽象为一个对象,这个对象具有三个属性

ECObj: {
scopeChain: { /* 变量对象(variableObject)+ 所有父级执行上下文的变量对象*/ },
variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ },
this: {}
}

变量对象

变量对象(Variable object)是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上。

代码示例

var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();

闭包概念

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1;
function foo() {
console.log(a);
}
foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 就构成了一个闭包

js 不会销毁被闭包引用的对象

GC 垃圾回收

Garbage Collection 垃圾回收是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理,称为垃圾回收。

新生代和老生代内存分区

为什么要分区?为了 GC 效率

新生代的 GC 算法

Scavenge 算法,它将堆内存一分为二,将存活对象在从空间 1 复制到空间 2,其他对象被回收。特点是速度快。新生代内存的对象过大或者存活时间过长就会去到老生代内存。

老生代的 GC 算法

Mark-Sweep 标记清除算法,标记清除回收之后,内存会变得碎片化。
Mark-Compact 标记整理算法,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

内存泄露

本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。

排除方法

  1. 抓下内存快照,使用 chrome 分析,使用框架和各种库的时候干扰项非常多
  2. alinode

参考文章

重要

  1. 深入理解闭包(五)——作用域、作用域链和执行上下文
  2. 深入理解JavaScript闭包【译】
  3. 深入理解JavaScript执行上下文、函数堆栈、提升的概念
  4. MDN 函数
  5. JavaScript深入之闭包
  6. 轻松排查线上Node内存泄漏问题
  7. 4类 JavaScript 内存泄漏及如何避免 介绍了如何使用 chrome dev tool 排查内存泄露

不重要

  1. 解读 V8 GC Log(二): 堆内外内存的划分与 GC 算法
    1. Node 性能优化
  2. 解读 V8 GC Log(一): Node.js 应用背景与 GC 基础知识
  3. NodeJS中被忽略的内存
  4. 前端基础进阶(一):内存空间详细图解
  5. 前端基础进阶(二):执行上下文详细图解
  6. JavaScript 内存模型

Swagger

什么是 Swagger

官网的解释:

Swagger is the world’s largest framework of API developer tools for
the OpenAPI Specification(OAS), enabling development across the entire
API lifecycle, from design and documentation, to test and deployment.

Swagger 提供了对 API 的设计、文档、测试、部署等阶段的支持。
Web 开发的本质是 API 开发,Swagger 使用 swagger.json/yaml 文件来定义 API,语法基于 OpenAPI Specification(OAS) 规范。

OpenAPI 语法入门:https://www.gitbook.com/book/huangwenchao/swagger/details

一个在线的 demo: https://editor.swagger.io//?_ga=2.267808907.403623983.1505817850-1482371638.1504594291#/

Swagger 可以做什么

围绕这个 API 定义,Swagger 生态提供了以下主要的服务:

  1. API 编辑:Swagger Editor
  2. API 文档:Swagger UI
  3. Mock 服务:Easy Mock
  4. 代码生成器:Swagger Codegen

Swagger Hub 全家桶对上述功能提供了整套服务打包。
理论上是可用,但是 mock 和 codegen 服务不够灵活。

接下来重点介绍 Mock 和 代码生成器这两个部分的内容。

Mock 服务

Swagger Hub 的 Auto Mock

Swagger Hub 提供的 Mock 服务, 只能区分 String 和 Number,不能提供更灵活的 Mock。

Easy Mock

Easy Mock 是一个可视化,并且能快速生成 模拟数据 的持久化服务,支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等,支持扩展更多数据类型,支持自定义函数和正则。

重点是它可以导入 Swagger 配置,

Codegen

Swagger Hub 的 Codegen

官方宣称提供几十个 server 端和 client 端的代码生成服务,但是实际上用处非常有限,以 nodejs-server 为例,这个模板使用 connect 框架,使用 express 和 koa 框架的就不适用了,而且编码风格差距也非常大。

  • 官方支持的 Codegen 模板:

  • nodejs-server 模板生成的代码:

自定义模板

在代码生成器这一块,往往是半自动的工具更加灵活,自定义模板,根据 swagger.json 生成填充值,一键生成重复内容。
在 Node 这一块,半自动生成器做得比较好的是 swagger-js-codegen
这个工具使用 mustache 模板工具,还提供了 gulp 工具。

根据这个思路我们可以自定义好模板,然后一键生成代码。

npm 上的 Swagger 工具

  1. 根据 Swagger 生成 Joi https://github.com/zaaack/koa-joi-swagger
  2. 根据 Swagger 生成 Mongoose https://www.npmjs.com/package/swagger-mongoose

番外

apidoc to swagge

使用 apidoc-swagger 把现在的 apidock 转为 swagger
https://www.npmjs.com/package/apidoc-swagger

参考

http://blog.just4fun.site/swagger-note.html

 

使用 Docker 搭建 WordPress 支持 https

最近把 WordPress 迁移到了腾讯云,为了配置方便使用了 docker 来运行,这里记录下配置过程

准备 compose 文件

WordPress 的 docker compose 文件网上有很多,需要一个 mysql 的镜像,还有 WordPress 的镜像,大概长这样:

version: '3'


services:

db:

image: mysql:5.7

volumes:

- db_data:/var/lib/mysql

restart: always

environment:

MYSQL_ROOT_PASSWORD: somewordpress

MYSQL_DATABASE: wordpress

MYSQL_USER: wordpress

MYSQL_PASSWORD: wordpress


wordpress:

depends_on:

- db

image: wordpress:latest

ports:

- "8000:80"

restart: always

environment:

WORDPRESS_DB_HOST: db:3306

WORDPRESS_DB_USER: wordpress

WORDPRESS_DB_PASSWORD: wordpress

volumes:

db_data:

定制 Dockerfile 添加 https 支持

借助于 letsencrypt 这个项目, 给个人网站添加 letsencrypt 变得十分容易,详细见这篇文章:
如何免费的让网站启用HTTPS

大概流程就是安装一个软件包 letsencrypt ,然后配置你的网站信息即可,但是我们的 WordPress 是安装在 docker 里面,所以我们要想办法把这个软件包打进镜像里面。

接下来我们要对 WordPress 这个镜像进行自定义,参考这篇文章:
docker + wordpress + letsencrypt

先定制 Dockerfile,集成 letsencrypt
新建文件夹 wordpress_tls 添加 Dockerfile

FROM wordpress:php7.1


RUN echo "export TERM=xterm LANG=en_US.UTF-8" >> ~/.bashrc

&& apt-get update && apt-get -y install git

&& rm -rf "/opt/letsencrypt"

&& git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

&& cd /opt/letsencrypt

&& ./letsencrypt-auto --version

wordpress 官方镜像使用的 ubuntu 源是国外源,打包镜像的速度会让你怀疑人生。可以把宿主机的 ubuntu 源放进 docker 镜像里。
$cp /etc/apt/sources.list ./

修改 Dockerfile

FROM wordpress:php7.1


ADD sources.list /etc/apt/sources.list


RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys

&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 // 改成你的 key


RUN echo "export TERM=xterm LANG=en_US.UTF-8" >> ~/.bashrc

&& apt-get update && apt-get -y install git

&& rm -rf "/opt/letsencrypt"

&& git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

&& cd /opt/letsencrypt

&& ./letsencrypt-auto --version

添加新的源会有认证的问题,可以参考 http://naveenubuntu.blogspot.com/2011/08/fixing-gpg-keys-in-ubuntu.html 解决

配置 https

启动容器:

$docker-compose up -d

然后配置 https
$docker-compose exec wordpress_tls /opt/letsencrypt/certbot-auto --apache -d your.domain.com --agree-tos -n -m you@your.domain.com

Let’s Encrypt 的证书90天就过期了,过期后执行

$ docker-compose exec wordpress_tls /opt/letsencrypt/certbot-auto renew

来更新,可以把更新脚本写进 crontab
$crontab -e

0 0 1 * * docker-compose exec wordpress_tls /opt/letsencrypt/certbot-auto renew

完整示例

https://github.com/myfjdthink/docker-wordpress

Typescript 与 Mongoose 的最佳实践

前言

mongoose 是 node.js 里操作 mongodb 的最好的 orm 工具。
typescript 则是带了 type 的 js 超集。
在开发过程中经常会碰到写错字段名的问题,只有到了运行阶段才能发现(或许也没发现。。。),使用 typescript 可以达到以下目的:

  1. 智能提示,不会输错字段名啦,当然这个取决于你的 IDE 是否支持 typescript。
  2. 类型检查,错误的类型定义可以在编译期发现。

接下来就看看这两个东西怎么配合吧。

完整的 Express + Typescript + Mongoose 的 Demo 可以参考之前的文章 :使用 typescript 做后端应用开发

安装准备

  1. typescript
    typescript 的安装配置这里不赘述,可以看 typescript 的官方文档。

  2. mongoose

下载 mongoose 的类型定义文件

$ npm install mongoose --save
$ npm install @types/mongoose --save-dev
$ npm install @types/mongodb --save-dev

typescript 的 tsd 的组织方式一直在变化,目前的方式算是比较简单的,typescript 官方把常见的库 Definition 都放在 npm 上了。

Model 的定义

以一个 User Model 为例。

先定义一个接口

src/interfaces/User.ts:

export interface IUser {
createdAt?: Date;
email?: string;
firstName?: string;
lastName?: string;
}

Schema + Model

src/model/User.ts:

import {Document, Schema, Model, model} from "mongoose";
import {IUser} from "../interfaces/User";

export interface IUserModel extends IUser, Document {
fullName(): string;
}

export const UserSchema: Schema = new Schema({
createdAt: Date,
email: String,
firstName: {type: String, required: true},
lastName: {type: String, required: true}
});
UserSchema.pre("save", function (next) {
let now = new Date();
if (!this.createdAt) {
this.createdAt = now;
}
next();
});
UserSchema.methods.fullName = function (): string {
return (this.firstName.trim() + " " + this.lastName.trim());
};

export const User: Model<IUserModel> = model<IUserModel>("User", UserSchema);

Mongoose 的 tsd 中是使用泛型来实现对具体 Model 的定义的,我们自己定义的 Model 是 IUser(包含数据库字段),Mongoose 提供的基础的 Model 定义是 Document(包括 find findOne 等操作) ,继承这个两个接口就是最后的 Model 啦。

有个比较尴尬的地方是:Mongoose 的 Schema 定义和 IUser 的定义是非常相似却又不是同一个东西,你需要写两遍属性定义。而且应该会经常改了一处忘了另一处。

这里提供一个解决方案是:用代码生成器根据 Schema 来生成 Interface 。
只提供思路,不给实现。

RUN 起来

引入 User Model。
src/app.js:

import * as mongoose from 'mongoose'
// mongoose.Promise = global.Promise;
import {User} from './model/User'
mongoose.connect('mongodb://localhost:57017/test');

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', async function () {
console.log('we're connected!');
try {
await User.remove({})
const user = new User({firstName: 'feng', lastName: 'nick', email: 'jfeng@kalengo.com'})
await user.save()
const fuser = await User.findOne({})
console.log('email', user.email);
console.log('email err', user.email2);
console.log('email', fuser.email);
console.log('fullName', fuser.fullName());
console.log('lastName', fuser.lastName);
console.log('createdAt', fuser.createdAt);
} catch (err) {
console.log('err ', err.stack);
}
});

一切正常。

得到了什么

智能提示

IDE 会根据 IUser 的结构来智能提示,终于不用怕很长的字段名啦

类型检查

属性不存在

类型不匹配

OK, 这样就实现文章开头提到的目的啦。

源码

https://github.com/myfjdthink/typescript-mongoose

参考文章

TypeScript: Declaring Mongoose Schema + Model
mongoose 文档

Node 后端开发的几种测试方式

后端开发中,只需要针对接口(无 GUI)做自动化测试,还是比较容易实现的,所以在开发过程中尽量写好测试。在这里总结下我在工作中用到的测试方法。这里以 Nodejs 的后端开发为例。

单元测试

单元测试的重要性不言而喻,算是业界共识了。
在有完善的单元测试的基础上,可以很方便地做到持续集成。
提到单元测试,大家很容易想到 TDD,我个人认为 TDD 并不是一件很容易做到的事情,特别是小团队需求多变的时候,往往会追求先出功能后补测试。

集成测试

考拉里的集成测试一般是通过一个接口对 N 个方法进行集成,这个接口会调用 N 个方法。这种集成测试是对单元测试的一种补充。
因为集成测试的粒度比较大,在做测试的时候往往会碰到个很麻烦的问题:怎么准备测试数据?

准备测试数据

测试数据存储位置
使用 js 来存储数据,相对于 json 来说 js 可以编程,例如需要定义一个日期是昨天,json 是做不到的。

在测试开始前载入测试数据

准备测试数据是件比较繁琐的事情,建议直接从生产环境拉取用户数据。脱敏后作为测试数据用。

写个脚本去线上拉取数据

GenTextUserData.test.js
关键点:
mongodump with query
tosource

平行测试

重构用,新写一个方法,新旧方法一起执行,判断结果是否一致。把不一致的结果收集起来。
效果卓群,可以发现很多意想不到的 BUG。
但是比较使用范围有限,只能测试只读的方法。

灰度测试

最实用,成本很低的方案。
灰度发布,只有部分人可以使用新功能,观察一段时间。

全量覆盖测试

使用大量真实数据来测试,真实数据可以提供足够的样本。
例如开发一个新功能,把所有用户的数据 load 进来,模拟使用这个新功能,看看是否有报错。
但是这个做法成本毕竟高,只有重要的功能才需要这样做。
而且用户数据大于百万级别时,整个测试耗时将非常漫长,可以考虑过滤部分用户把量控制在10万以内。

人工测试

在测试环境,作为用户,体验一遍此次开发的功能,看看前面的测试是否有遗漏。
辅助工具
HDC
POSTMAN