Quantcast
Channel: CodeSky 代码之空
Viewing all 195 articles
Browse latest View live

Node.js 异步原理

$
0
0

写文章写得实在生无可恋,依旧看起了书——《深入浅出 Node.js》,觉得自己确实有错,之后如果有人问我 Node.js 入门怎么入门我可能还是会推荐他网上的教程,但是说到推荐书,这确实是一本很棒的书。

——安利完毕,之后进入正题。

在面试时可能真的会遇到这样的情况,面试官问:请问一下异步是怎么实现的。

之前在听小伙伴说到这个问题的时候一脸黑人问号,毕竟仿佛是一知半解,不得其意,今天总算可以好好聊一聊异步了,也算是一个章节的笔记。不过掌握的还不是很透彻,如果有错误请指出。

为什么会有异步

  1. 单线程同步模型系统资源利用率低下
  2. 多线程模型线程切换开销大,多线程编程中的同步问题让人头大

异步让单线程远离阻塞,同时规避了线程切换(恢复现场)的开销,让单一线程在执行 I/O 操作后立即进行其他操作。

异步 I/O 与非阻塞 I/O

尽管异步与非阻塞我们常常一起提及,异步也确实解决了非阻塞的问题,但是在计算机内核的角度,这并不是同一回事。

我们知道,阻塞 I/O 造成了 CPU 等待,浪费了系统资源,而非阻塞 I/O 会在执行完毕后立即返回,如果需要获得数据,需要再次读取。系统通过轮询来获取非阻塞 I/O 的结果,对 CPU 资源又存在浪费。

现在主要的轮询技术有:read / select / poll / epoll

其中 epoll 在进入轮询后没有检测到 I/O 事件将会休眠,直到事件发生,利用率较高,但仍然需要花费资源去等待与确认。

我们理想中的异步应该是可以在进行一次 I/O 之后不闲置 CPU,不去关心,直到完成之后执行回调。

现实中也确实有好几种异步方案,在 Node.js 和 Windows 下的 IOCP 中都使用线程池完成异步 I/O。Node.js 中利用 libuv 封装,来判断平台进行兼容。

based-libuv-1.png

事件循环

Node.js 中的异步 I/O 当然得提到大名鼎鼎的事件循环了,如果面试只面试到这里,相信每个人都能说出来:

事件循环就是执行一次循环(一个 Tick),就检测是否有事件未处理,如果有,就取出事件及相关回调函数,如果存在关联的回调函数,就执行它们,然后进入下一循环,如果没有,就退出进程。

事件循环就是一个典型的生产者消费者模型,由请求生产,事件循环消费,这个循环由 IOCP / 多线程创建。

首先我们引入请求对象的概念,JavaScript 层传入的参数和方法都被封装在请求对象中,当有可用线程时,我们就会调用对象底层对应的方法。

组装好请求对象,送入线程池等待执行,这就完成了我们的第一步。

执行结束后,将会将结果存储,并且调用方法通知 IOCP 将线程交还给线程池。

之后事件循环观察到执行完的请求,进行处理即可。

整个流程如图(摘自深入浅出 Node.js):

14964161584392.jpg

非 I/O 的异步 API

这部分介绍了一下 setTimeout, process.nextTick, setImmediate 的异同,可以从这里理解为什么 setTimeout 或者 setInterval 实现计划任务是个不靠谱的行为。

setTimeout 时创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次 Tick 执行时,就会从该红黑树中迭代取出定时器对象,检测是否超时,如果超时就立即执行。

由此我们知道,如果两个定时 1ms 的任务,但是一个任务占用了 4 ms 的时间片,那么下一个任务就定然是不精准的。

行为图(摘自深入浅出 Node.js):

14964164304499.jpg

如果你需要立即执行一个任务,就应该使用 process.nextTick() 他可以规定和保证在下一个循环中执行。

此外,Node.js 还有一个 setImmediate() 方法,与 process.nextTick() 的区别是,process.nextTick 的回调函数保存在一个数组中,而 setImmediate 保存在链表中,在同一个 Tick 中会清空数组,但是只执行链表中的一项。此外,process.nextTick 在事件循环的检查中(idle 观察者)高于 setImmediate(check 观察者)。


ECMAScript 标准的十万个为什么 - Symbol 的隐式转换

$
0
0

今天苏老师发了一篇:Symbol Polyfill 填坑之旅,文章中提到 Symbol 隐式转换的问题,作为一个只在文章中看过 Symbol 的萌豚,我也想知道究竟为什么 Symbol 可以有 toString() 方法,却不能进行 '' + Symbol('word') 的操作。

标准

MDN - Symbol 中有一些规则,它告诉我们当 Symbol 尝试隐式类型转换时会报错,但是为什么会这样,底层是是怎么实现的,我们还是不太清楚。

Symbol 类型转换

当使用 symbol 值进行类型转换时需要注意一些事情:

尝试将一个 symbol 值转换为一个 number 值时,会抛出一个 TypeError 错误 (e.g. +sym or sym | 0).

使用宽松相等时, Object(sym) == sym returns true.
这会阻止你从一个 symbol 值隐式地创建一个新的 string 类型的属性名。例如,Symbol("foo") + "bar" 将抛出一个 TypeError (can't convert symbol to string).

"safer" String(sym) conversion works like a call to Symbol.prototype.toString() with symbols,但是注意 new String(sym) 将抛出异常。

标准实现

我们从底层标准看起,首先看 + 时发生了什么:

Let lref be the result of evaluating AdditiveExpression.
Let lval be ? GetValue(lref).
Let rref be the result of evaluating MultiplicativeExpression.
Let rval be ? GetValue(rref).
Let lprim be ? ToPrimitive(lval).
Let rprim be ? ToPrimitive(rval).
If Type(lprim) is String or Type(rprim) is String, then
Let lstr be ? ToString(lprim).
Let rstr be ? ToString(rprim).
Return the String that is the result of concatenating lstr and rstr.
Let lnum be ? ToNumber(lprim).
Let rnum be ? ToNumber(rprim).

会在左值和右值赋值之后依次执行 GetValueToPrimitive,对于 String 和 Symbol 而言,会返回原来的值。接下来,假设操作为 '' + Symbol('word'),左值为 String ,执行 ToString 操作,此处的 ToString 并不是我们所说的原型链上的 toString 方法,我们需要接着往下看:

14969339026287.jpg

换句话说,在 ToString 方法内部判断了值类型,根据类型进行后续不同的操作,而不是简单的调用 toString() 方法,对于 Symbol 类型,它的处理就是抛出异常。

如果我们需要 polyfill,需要进行一系列重载或者复写底层定义,不太好搞。

相关文档

CSS 揭秘笔记 - 文字效果

$
0
0

CSS 其实还是有很多学问的,上次正好说到了文字的一些效果,干脆就把 CSS Secret 中的文字效果都做一下笔记,还是有很多自己原来不知道的黑科技的。

连字符断行

在 Word 中我们经常会用两端对齐的效果,但是在浏览器渲染时会形成文字孤岛的效果,而在打印媒介中,两端对齐往往会伴随连字符的使用,所以在处理对齐时需要调整的间距就少很多,下图展示了 CSS 默认效果:

14970731555270.jpg

要想实现连字符,常见的方法有:

  1. 服务端预处理
  2. JavaScript 后期生成
  3. 在线生成器处理
  4. 手工插入软连字符­

怎么想想都觉得不如不用……

于是 CSS3 中引入了一个新的属性 hyphens,只要使用 hyphens: auto 即可实现我们想要的效果,为了确保奏效,你需要在 HTML 标签内插入 lang,当然,实际上这个特性还在草案中,兼容性并不是很好,你可以通过 MDN 看到它的兼容性表:https://developer.mozilla.org/en-US/docs/Web/CSS/hyphens

插入换行

如果我们要定义列表,在考虑语义的情况下,应该使用 <dt><dd>。我们最终所期待的效果如下:

<dl>
    <dt>Name:</dt>
    <dd>Lea Verou</dd>

    <dt>Email:</dt>
    <dd>lea@verou.me</dd>
    <dd>leaverou@mit.edu</dd>

    <dt>Location:</dt>
    <dd>Earth</dd>
</dl>

当然实际上并不会如我们所想的这么顺利<dt><dd> 都是块级元素,因此如果要实现以下效果,我们可能会开始修改 display 之旅。

加了 display: inline 之后我们的 Name 和 Email 都变成了同行的,看着不太靠谱,尝试打了个换行的补丁(<br>),破坏了原本的语义,所以我们考虑用 CSS 换行,Unicode 中有一个字符专门代表换行符:0x000A,在 CSS 中,这个字符可以写作 \A,我们把它插入到伪元素中,并且加入 white-space: pre 避免空白符和换行符合并:

dd::after {
  content: "\A";
  white-space: pre;
}

看上去我们即将达成效果:

接下来我们只要排除前面的 dd 的影响,只选择最后一个就可以了,可行的方法有许多种,这里我们换个思路,再通过相似的方法给与 dd 相邻的 dd 添加逗号:

dd + dt::before {
  content: '\A';
  white-space: pre;
}

dd + dd::before {
  content: ', ';
  font-weight: normal;
}

如果你的 dd 之间有未加注释的空白符,那么逗号前面会有一个空格,我们这里使用负外边距解决这个问题:

dd + dd::before {
  content: ', ';
  font-weight: normal;
  margin-left: -.25em;
}

这种方法也有一种 hack 的味道,实际上也不是那么刚好,不过对于大多数字体,这个空隙宽度的不同导致的误差可以忽略,就达成了最终的效果:

文字行的斑马线

在平时我们可能最常用的地方就是代码高亮的渲染上,虽然我博客的代码高亮并没有用

然而如果使用:

tr:nth-child(even) { background: rgba(0,0,0,.2); }

虽然确实能起到隔行换行的左右我们却不得不为每行都添加标签,破坏了语义化。不够理想。

之后我们考虑,可以通过创造条纹背景,调整间距的方法来实现:

pre {
  padding: .5em;
  line-height: 1.5;
  background: beige;
  background-image: linear-gradient(rgba(0,0,0,.2) 50%, transparent 0);
  background-size: auto 3em;
}

加入条纹背景后大致的效果是这样的:

这和我们预期的效果有一定差距,不过距离成功只差一步,background-origin 可以修复这个问题,他告诉浏览器在解析 background-position 时以 content-box 的外沿做基准,而不是 padding-box。

最终我们做到了,如果需要改变 line-height 时,需要同时改变 background-size

调整 Tab 宽度

Tab 宽度默认是 8 字符,非常尴尬,即使是在 GitHub 我们也经常会看到八空白符的 Tab 带来的尴尬。

不过由于有 CSS3,一切都变得美好起来,CSS3 有一个 tab-size 属性可以设置为一个数字(表示字符数)或者长度值,以便我们添加 Tab 空白符,看上去太棒了:

pre {
  tab-size: 2;
}

连字

由于字体设计时不同字形不一定会和谐共处,有时候会发生冲突。

所以有就有了连字的存在,有时设计师会设计一些额外的字体,但是浏览器默认并不会使用连字,如果需要使用连字,过去往往使用 Unicode 强制,这样会造成很大的问题:

  1. 结构不可辨识
  2. 当前字体可能不包含连字
  3. 并不是所有连字效果都有对应的 Unicode
  4. 破坏了文本的可访问性,包括复制粘贴和语言处理等等

在 CSS3 中引入了 font-variant-ligatures 属性,它可以帮助你启用连字效果,默认为 normal 而不是 none,需要启动所有可能的连字需要开启:

font-variant-ligatures: common-ligatures discretionary-ligatures historical-ligatures;

如果只需要通用连字,那么只要设置为

font-variant-ligatures: common-ligatures;

可以显式的把其他两种连字关闭:

font-variant-ligatures: common-ligatures no-discretionary-ligatures no-historical-ligatures;

我们可以看下最终的效果:

华丽的 &

有时候我们要指定部分字用非常酷炫的效果,有一种方法是手动规定样式,但是这样需要把每一个字都插入标签,非常麻烦,我们期待在自然的情况下用 CSS 去解决这样的问题。

可以使用仅带这个字的字体,这样似乎就能搞定了,@font-face 会在解析不到其他字体时选择之后的字体方案,而我们需要的字体就用首选方案渲染好了。但是实际上我们还需要对字体本身进行处理,如果是自带的字体,看着就不太靠谱了。

实际上我们可以试用 unicode-range,我们可以通过 JavaScript 获得我们的 Unicode 码位。

&".charCodeAt(0).toString(16); // 返回26

如果你想指定一个字符区间,还是要加上 U+ 前缀,比如 U+400-4FF。 实际上对于这个区间来说,你还可以使用通配符,以这样的方式来写: U+4??。同时指定多个字符或多个区间也是允许的,把它们用逗号隔开即可, 比如 U+26, U+4??, U+2665-2670

最终就可以实现了:

自定义下划线

text-decoration: underline; 默认的功能是在让人捉急,通常我们使用边框来模拟下划线,但是下划线的位置让人觉得尴尬,和文本的空隙很大,并且会受到文本正常换行的干扰,使用 box-shadow 也是一样。

于是我们又想到了 background 的神奇魔法,还可以调整线条,你只需要进行一些简单的设置:

值得庆幸的是,CSS 也在考虑加入 text-decoration-style 等新属性来改善这一情况,就不用再充满 hack 的味道去实现了,当然目前还得不到良好的浏览器支持。

现实中的文字效果

接下来轮到了 text-shadow 的现代魔法时间,这些字体都是可以比较常用于美化文字效果的方法,实现起来也不复杂。(下面的都比较短,就不贴 JSFiddler 了)

凸版印刷效果

我最初见到凸版印刷效果的时候还不会写代码,就觉得相当酷,简单的设置一下 text-shadow 就能实现:

p {
    padding: .8em 1em;
    background: hsl(210, 13%, 60%);
    color: hsl(210, 13%, 30%);
    text-shadow: 0 1px 1px hsla(0,0%,100%,.8);
}

预览:https://jsfiddle.net/csvwolf/qjxned96/

空心字效果

见:CSS 文字描边效果的研究

文字外发光效果的研究

外发光是我在 PS 时比较常用的效果(因为套个滤镜就可以了非常简单=_=),要实现这个效果,只需要多重叠几层 text-shadow 即可,颜色只要与文字保持一致,不需要考虑偏移,也非常容易:

a {
  text-shadow: 0 0 .1em, 0 0 .3em;
}

预览:https://jsfiddle.net/csvwolf/doe77rrh/

文字凸起效果

文字凸起效果是使用一长串累加的投影,不设模糊并以 1px 的跨度逐渐错开,使颜色逐渐变暗,然后在底部加一层强烈模糊的暗投影,从而模拟完整的立体效果。

body {
    background: #58a;
    color: white;
    text-shadow: 0 1px hsl(0,0%,85%),
                 0 2px hsl(0,0%,80%),
                 0 3px hsl(0,0%,75%),
                 0 4px hsl(0,0%,70%),
                 0 5px hsl(0,0%,65%),
                 0 5px 10px black;
    font: bold 500%/1 Rockwell, serif;
}

预览:https://jsfiddle.net/csvwolf/vsf3pur5/

环形文字

在 Word 中偶尔我们会使用环形文字,可是在 CSS 中并没有良好的解决方案,我们需要求助于 SVG。SVG 原生支持以任意路径排队文字。

我们需要做的就是用一个 <textPath> 包住这段文本,将他们装进<text>,在上方引入 <path> 规定路径。

<div class="circular">
<svg viewBox="0 0 100 100">
<path d="M 0,50 a 50,50 0 1,1 0,1 z" id="circle" />
        <text><textPath xlink:href="#circle">
            circular reasoning works because
        </textPath></text>
    </svg>
</div>

于是他可能是长这样的。

之后我们利用 CSS 设置 path 不可见来隐藏黑色的圆形:

.circular path { fill: none; }

接下来最大的问题是,几乎所有的文本都跑到 SVG 元素的外面去了,而且遭到了裁切。为了修正这个问题,我们需要让这个容器元素变小,然后再给 SVG 元素应用 overflow: visible 样式,这样它就不会把内容的溢出部分裁切掉了,此外,我们设置了 margin 来处理溢出元素的占位:

.circular {
    width: 30em;
    height: 30em;
    margin: 4em auto 0;
}

.circular svg {
    display: block;
    overflow: visible;
}

大功告成。

服务器迁移便捷式笔记

$
0
0

这周连续六天迁移服务器成就达成,本来踩的坑比较琐碎不想整理一下发 CodeSky 了,今天迁移 CodeSky 的时候遇到了以前单服务器配置时候没有遇到过的坑,所以还是记点东西好了。

supervisor

作为一个连续三天写了配置文件忘记重启的人不由得要大喊一声:supervisorctl reload

以前我一直以为大家说的 supervisornode-supervisor……直到看了旧机器的配置信息……

这种时候只要微笑就好了参考资料可以看:

需要注意的是:当你添加或者修改 supervisor 的配置信息后请重启 supervisorctl 保平安,否则配置不会载入/生效。

node-bunyan DtraceProviderBindings error

旧的项目有使用 restify 的,在迁移时发现了这个报错,然后升级了一下 restify 的版本,npm install --no-optional 搞定(升级的时候还是挺忐忑的 2333,生怕 breaking changes)。

参考:https://github.com/trentm/node-bunyan/issues/216

MongoClient 链接多个数据库

在就项目里也有用 mongodb 库来做数据库连接而不是 mongoose 的,mongoose 出现验证错误,不过升级一个版本就好了,mongodb 却在多数据库时不能共享 mongoose 可用的连接字符串,后来人工加了副本集 ?rs=name,搞定了这个问题。

参考:https://docs.mongodb.com/manual/reference/connection-string/

博客迁移

剩下的一些涉及到内部服务或者内部造轮之类的改造类问题就 Pass 了,今天晚上迁移博客还是踩到了一点坑的(主要还是踩的太少)。

fastcgi connection refused

安装并启动了 php-fpm,结果并没有发现监听的端口,在 /etc/php7.1/fpm/pool.d/www.conf 做一点微小的修改:

找到 listen,去掉本来的 /var/run/php7.1-fpm.sock 改为 127.0.0.1:9000 后重启服务。

(另外说一下最简单停用 php-fpm 的方式是 pkill php-fpm :cry)

参考:https://www.digitalocean.com/community/questions/nginx-error-111-connection-refused

MySQL 远程访问

安装完数据库之后发现局域网并连接不上,以前都是单机的,没有这个烦恼,需要做如下设置:

修改 /etc/mysql/my.cnf 或者在其他位置的配置文件(my.cnf 里可能是 include 信息)。

找到 bind-address 行,注释,重启服务。

在 mysql 中需要配置:

$ mysql -u root -p
mysql> GRANT ALL ON *.* to root@'%' IDENTIFIED BY 'password';
mysql> FLUSH PRIVILEGES;
mysql> exit

之前没有生效是因为没有用 FLUSH PRIVILEGES 刷新配置,生效之后就可以连接了,% 也可以是局域网服务器的 ip 地址。(% 表示全部)

参考:

Typecho with PHP7.1

尽管配置好了,但还是提示连接不上数据库,因为在 PHP 7 中移除了 mysql 方法,需要改用 mysqli 或 pdo,对 config.inc.php 进行修改:

// $db = new Typecho_Db('Mysql', 'typecho_');
// 改为
$db = new Typecho_Db('Pdo_Mysql', 'typecho_');

参考:https://faq.xiaoz.me/archives/133.html

另外之后发现我的内页都挂了,是忘了安装 php-mbstringphp-curl,嗯……毕竟 PHP 有些扩展都是全局。

vim 中文乱码

发现了很多乱码,刚开始以为是编码问题,但是 cat 并没有这个问题,还是需要配置一发 vim:/etc/vim/vimrc,加上:

set fileencodings=utf-8,gb2312,gb18030,gbk,ucs-bom,cp936,latin1
set fileencoding=utf-8
set encoding=utf-8

一次 Chrome 缓存锁的有趣探索

$
0
0

昨天同事问我关于 Node Event Loop 的问题,代码如下:

const Koa = require('koa')
const app = new Koa()
const logger = require('koa-logger')

let index = 0

const sleep = delay => new Promise(resolve => setTimeout(resolve, delay))

app.use(logger())

app.use(async ctx => {
  await sleep(500)
  ctx.body = index++
})

app.listen(3000)


在 Chrome 下用 fetch 同时发起五个请求:

Promise
  .all([
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000')
  ])
  .then(data => console.log(data))

结果会是一个个收到请求并一个个 response,看着不太对劲,毕竟从 Node.js 异步原理(或者用脚趾头想想)中我们已经知道 setTimeout 是不可能起到阻塞主线程的效果的:

这个时候考虑的重点只有:第一,我们遇到了一个神奇的 Bug,第二,浏览器端发生了什么延迟了请求的发送。

在浏览器打完 fetch,Network 长这样:

首先在用脚趾头思考之后排除了 Promise.all 会不会有毒的情况(因为我用 for 循环得到了相同的结论)。

之后看了下 Waterfall 中的详情:

Connection Start 中的 Stalled 占了很大的时间,在灰色的阶段请求根本没用被发出去,事情越发朝着一个神奇的方向展开,于是决定研究一下 Stalled 的原因:

Stalled/Blocking

请求等待发送所用的时间。 可以是等待 Queueing 中介绍的任何一个原因。 此外,此时间包含代理协商所用的任何时间。

Queuing
如果某个请求正在排队,则指示:

  • 请求已被渲染引擎推迟,因为该请求的优先级被视为低于关键资源(例如脚本/样式)的优先级。 图像经常发生这种情况。
  • 请求已被暂停,以等待将要释放的不可用 TCP 套接字。
  • 请求已被暂停,因为在 HTTP 1 上,浏览器仅允许每个源拥有六个 TCP 连接。
  • 生成磁盘缓存条目所用的时间(通常非常迅速)

关于以上内容可以在以下连接阅读和了解:

第一,我们的 XHR 并不涉及优先级问题,第二我们也没有满 6 个 TCP 连接——一脸懵逼之后看来只能考虑缓存的嫌疑了。

正巧我们查到了:关于请求被挂起页面加载缓慢问题的追查(01/13更),尽管原作者的原因非常复杂,但是我们的问题还是觉得是个缓存锁的问题,先来确认一下,做个小小的实验:

然后再次发送请求,发现这一次五个请求同时发送,没有被卡住,嫌疑人越来越倾向于是 Cache Lock 了。

然后我们学习了一波,通过 <chrome://net-internals/> 抓了波请求:

我们发现 HTTP_CACHE_ADD_TO_ENTRY 花了特别长的时间(dt),果然是缓存锁的锅:

实际上我们在上面那篇文章中也读到了一些信息:

https://codereview.chromium.org/345643003

Http cache: Implement a timeout for the cache lock.

The cache has a single writer / multiple reader lock to avoid downloading the
same resource n times. However, it is possible to block many tabs on the same
resource, for instance behind an auth dialog.

This CL implements a 20 seconds timeout so that the scenario described in the
bug results in multiple authentication dialogs (one per blocked tab) so the
user can know what to do. It will also help with other cases when the single
writer blocks for a long time.

The timeout is somewhat arbitrary but it should allow medium size resources
to be downloaded before starting another request for the same item. The general
solution of detecting progress and allow readers to start before the writer
finishes should be implemented on another CL.

大致感觉上去就是一个读者写者模型的锁,写者在写的时候为了避免重复下载,需要等到写者写完读者再读,但是由于我们默认没有设置缓存,这部分缓存又不能直接用,所以请的请求又准备申请写锁去缓存下一次请求获得的结果了。

要避免这个问题,一是像我们原来排查嫌疑人时的那样禁用缓存,二可以通过 query 时间戳解决(好复古的解决方案!)。

当然从中我们也学到了不少,比如更会看 Network 了(笑),以及昨天又成功的熬了个夜 =^=。

谈谈对 URIEncode 的一些处理

$
0
0

最近斗志非常差,当然很大程度还是自己的锅……这又不是个情感博客我就不细说了……

这周公司里又开始做新的东西了,大概是类似于反向代理,但是授权了私有库的权限,需要做一些限制,比如 GitHub 路径限制为 dist

最初的时候我们这么验证路径:

// file: such as dist/test.js
!/^(dist|lib)\//ig.test(file)

然后我们发现这个方案不靠谱,GitHub 可以解析 dist/../src 于是又回到了最初的起点,就想着能不能排除 ..,为了装逼又改了一波:

/^(dist|lib)\/((?!\.\.).)*$/

然而我们发现并没有什么卵用妈蛋怎么和公司里写的不一样这个亲测可用……

后来想到了用 path.resolve 去解决,它会自动计算出默认值……

但是实际上我们的这个 path 并不仅仅可以是 /,有可能经过一次 encode,用户也可能耍流氓想要通过 encode 去绕过什么。

刚开始想的是根据 %25(即 %) 为特征进行 while 处理,但是这样万一玩脱是否会造成死循环,或者或者会过度浪费系统资源……

然后又试验了一波,GitHub 可以解析一次 encode 的结果,但是二次就不行了,利用这个结果我们可以使用一次 encodeURIComponent(file),这样相当于做了一个请求时必须为 / 的强约定,就可以省去考虑 N * decode 了。

接下来就可以用 path.resolve 后的结果做愉快的目录判断了。

记一个 event.target 类型导致的 Bug

$
0
0

今天刚接的新锅报了个不太好复现的 Bug(实际上到最后都没有在原环境复现……),于是看了一波代码找问题,Bug 大致是:

xxx.hasAttribute is undefined

调用到的地方有几处,首先确定了 hasAttribute 是写在 Element 的原型链上的,即如果是 Element 类型(element.nodeType === 1),那么必然有 hasAttribute。

在此之前,我先查了一下,什么情况下 「Element」 会没有 hasAttribute:

很巧的发现了:https://stackoverflow.com/questions/10819230/element-has-no-method-hasattribute-why

上面提到了 document,所以很快就怀疑什么地方传入的 document

嫌疑人有两处,之后通过报错的信息(尽管是被压缩过的,看到眼残)……发现是一个 on('keydown', fun) 报的错。

监听传入的可能不是 Element 吗?——从上面那个问题来看是可能的,但是怎么复现?

于是我操作了一波,即使把 body 删完,至少也会触发到 html 这个 Element,而不是 #document

不死心,看了 MDN 的手册之后发现 keydown 中的手册写了 Target 的情况有 Document 或者 Element,也就是确实有可能。

可是我又确实复现不了,此外,W3 在 keydown 中的 Trusted Targets 为 Element,对于 Event.target 字段是这么解释的,大致意思是 聚焦的元素 -> body -> root element:

Event.target : focused element processing the key event or if no element focused, then the body element if available, otherwise the root element

root element: The first element node of a document, of which all other elements are children. The document element.

Well,实际上再不济也会回到的节点其实是第一个元素(通常就是 html 了),依旧是个元素,符合我多次尝试的结果:

顺着 W3 查,我们找到了 https://dom.spec.whatwg.org/#dispatching-events,在步骤二里规定了:

Let targetOverride be target, if legacy target override flag is not given, and target’s associated Document otherwise.

英文不太好……其实没怎么看懂,但似乎和 Document 有点关系,正巧之后看到了 event.isTrusted 属性:

isTrusted is a convenience that indicates whether an event is dispatched by the user agent (as opposed to using dispatchEvent()). The sole legacy exception is click(), which causes the user agent to dispatch an event whose isTrusted attribute is initialized to false.

大致是说可以判断是不是由用户触发的事件,回忆杀一波 W3 写的也是 Trusted Targets,是不是意味着指的是正常触发的情况,于是我就想到了模拟事件:

document.dispatchEvent(new KeyboardEvent('keydown',{'key':'a'}));

成功触发得到了 Document 而不是 Element,从这点上来说,每个手册都没错,但是触发条件确实有点苛刻,Bug 在原场景正常使用中也没有能复现,如果是其他库的干扰报错确实应该多几层调用栈信息……所以还是一个挺诡异的问题,但至少至此,终于确定了 event.target 真的可以是 Document,可喜可贺,可喜可贺。

需要用到 Element 特有的方法时还是应该判断一波 nodeType。

// 于是一天就修了一个 Bug……一行代码……

[翻译]在 macOS 使用 Dnsmasq 进行本地开发

$
0
0

今天又知道了新的黑科技,顺手练练英语也不错……(翻译太难了 :< 我觉得自己翻得还不如谷歌……)因为苹果改名了所以我也就把 OSX 改到了 macOS。

原文:https://passingcuriosity.com/2013/dnsmasq-dev-osx/

这是一篇快速入门,通过它你可以了解到如何在 macOS 中安装 Dnsmasq 和如何使用它来把开发的站点定位到本地机器。

大多数 Web 开发者非常熟悉更新 /etc/hosts 来把 coolproject.dev 的流量导流至 127.0.0.1,同时也知道这种方法带来的问题:

  • 他需要在你添加或删除项目时每次对配置文件进行修改
  • 他需要管理员权限来进行变更

安装一个像 Dnsmasq 一样的本地 DNS 服务器然后配置你的系统去使用这个服务可以让这些配置修改成为历史。在这篇文章中,我将会经历以下过程:

  1. 在 macOS 安装 Dnsmasq
  2. 配置 Dnsmasq,使用 127.0.0.1 来响应所有 .dev 的请求
  3. 配置 macOS 来让所有 .dev 请求走 Dnsmasq

在我们开始之前,我需要先警告你:这些说明将会向你展示如何安装新的系统软件和修改系统配置。像其他所有变更一样,除非你有足够的自信你已经理解了并且可以在有需要时取消这些变化,否则你不应该继续进行操作。

安装 Dnsmasq

引用 Dnsmasq 项目的首页:

Dnsmasq is a lightweight, easy to configure DNS forwarder and DHCP server […] is targeted at home networks[.]

有许多种方法可以安装,但是我(在 macOS 上)最喜欢的的还是用 Homebrew。安装 Homebrew 非常简单不过不在本文的讨论范围中。

一旦你安装完 Homebrew,用它来安装 Dnsmasq 非常简单:

# 升级你的 homebrew
brew up
# 安装 dnsmasq
brew install dnsmasq

这个安装流程将会输出若干命令,你可以使用它们去自动启动带默认配置的 Dnsmasq。我使用以下命令来操作,但你需要使用 brews 告诉你的命令去操作:

# 拷贝默认配置
cp $(brew list dnsmasq | grep /dnsmasq.conf.example$) /usr/local/etc/dnsmasq.conf
# 复制守护进程配置文件
sudo cp $(brew list dnsmasq | grep /homebrew.mxcl.dnsmasq.plist$) /Library/LaunchDaemons/
# 自启 dnsmasq
sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist

配置 Dnsmasq

现在你已经安装完 Dnsmasq 并且让它跑了起来,是时候配置一波了!默认的配置文件在:/usr/local/etc/dnsmasq.conf,所以使用你最喜欢的编辑器去打开它。

Dnsmasq 可以做的很多事情之一是将 DNS 请求与模式数据库进行比较,并以此来确定正确的应答。我使用这个功能来匹配以 .dev 结尾的任何请求,并发送 127.0.0.1 作为应答。Dnsmasq 配置指令非常容易:

address=/dev/127.0.0.1

把这一段插入 /usr/local/etc/dnsmasq.conf(我把它放在了 address=/double-click.net/127.0.0.1 这个例子的附近来让两条条目在一起)并且保存文件。

你可能需要重启 Dnsmasq 来使其识别这个更改。重新启动 Dnsmasq 和在 launchd 下运行的任何其他服务都一样:

sudo launchctl stop homebrew.mxcl.dnsmasq
sudo launchctl start homebrew.mxcl.dnsmasq

你可以通过使用 dig 程序来发送一个 DNS 请求来测试。选择一个以 dev 结尾的域名并且使用 dig 来查询你的 DNS 服务器:

dig testing.testing.one.two.three.dev @127.0.0.1

你可以得到像这样的应答:

;; ANSWER SECTION:
testing.testing.one.two.three.dev. 0 IN A   127.0.0.1

配置 macOS

现在你已经有了一个可以工作的 DNS 服务器,你可以在自己的操作系统上配置来使用它。有使用两种方法:

  1. 发送所有 DNS 请求到 Dnsmasq
  2. 只发送 .dev 的请求到 Dnsmasq

第一种方法非常简单,只要在系统偏好中改变你的 DNS 设置——但是可能在 Dnsmasq 配置文件不添加额外的修改的时候并不会生效。

第二种方法显得有点微妙,但并没有非常。大多数类 Unix 的操作系统有叫做 /etc/resolv.conf 的配置文件,用以控制 DNS 查询的执行方式,包括用于 DNS 查询的默认服务器(这是连接到网络或者在系统偏好中修改 DNS 服务器时自动设置的)。

macOS 也允许你通过在 /etc/resolver 文件夹中创建新的配置文件来配置额外的解析器。这个目录可能还不存在于你的系统中,所以你的第一步应该是创建它:

sudo mkdir -p /etc/resolver

现在你需要在这个目录中为要配置的每个解析器创建一个新文件。每个解析器大致对应于我们的目标——一个像我们的 .dev 一样的顶级域名。对于每个解析器你有许多可以配置的项,但我通常只选择以下两个:

  • 解析器的名字(对应于要解析的域名)
  • 要使用的 DNS 服务器

想要知道关于这些文件的更多信息,可以看 resolver(5) 的手册页了解:

man 5 resolver

/etc/resolver/ 目录中创建一个与新的顶级域名(记住,我正在使用 dev)同名的新的顶级域名,并且通过运行以下命令来添加名称服务器:

sudo tee /etc/resolver/dev >/dev/null <<EOF
nameserver 127.0.0.1
EOF

在这里,dev 是我配置 Dnsmasq 来响应的顶级域名,127.0.0.1 是要使用的服务器的 IP 地址。

一旦你创建了这个文件,macOS 将会自动读取并完成。

测试

要测试你的新配置非常容易,只要使用 ping 来检查你是否可以将你的新的顶级域名解析到一些 DNS 名中。

# 确定你没有玩坏你的 DNS
ping -c 1 www.google.com
# 检查 .dev 的 DNS 是否生效
ping -c 1 this.is.a.test.dev
ping -c 1 iam.the.walrus.dev

你应该可以看到在 Dnsmasq 配置中提到的 IP 地址的结果,如下所示:

PING iam.the.walrus.dev (127.0.0.1): 56 data bytes

现在你可以随时填写在 .dev 下的新的 DNS 名称。恭喜!


CSS 像 font 一样使用 svg 与上色

$
0
0

这周在切图的时候拿到了 svg,之前自己一直是用 font-icon 的,感觉挺方便的,svg 相比 font-icon 有一些优点(这个以前在网上看到过),本文暂时不展开讲述了。

但是一些图标,在 Element 里自带的 el-icon-xxx 的方式非常统一,其实是根据 class 设置的,我也不喜欢 svg 的插入方式(太硬),所以希望尽可能的让项目中的 font 保持统一。

然后就发现了 svg 没法像 font icon 一样设置 color。

svg 标签的情况下,倒是有很多方法,就比如:

svg { fill: #369; }

详细的可以看:SVG图标颜色文字般继承与填充

但是通过 background 方式定义就有了一些限制,让人觉得蛋疼,后来找到了用 mask 的方法来取代 background 设置,这样可以方便的设置颜色:

.icon {
    background-color: red;
    -webkit-mask-image: url(icon.svg);
    mask-image: url(icon.svg);
}

mask 可以像 background 一样设置类似的属性,所以我们可以很方便的做到 font 的效果,区别是兼容性——Emmmm,当然我们的系统不需要考虑兼容性,关于兼容性,查一下 caniuse 或者 MDN 就知道了。

后来我想到,是不是能用 filter 搞一波,filter 也有许多很魔幻的东西——果然:

.icon {
  width: 48px;
  height: 48px;
  display: inline-block;
  background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/18515/heart.svg) no-repeat 50% 50%;
  background-size: cover;
}

.icon-red {}

.icon-orange {
  -webkit-filter: hue-rotate(40deg) saturate(0.5) brightness(390%) saturate(4);
  filter: hue-rotate(40deg) saturate(0.5) brightness(390%) saturate(4);
}

当然相比 mask 就显得不太直接。

后来我发现,原来这方法并不是我原创的!早就有人写了!Well!

剩下的方法比如 Data URI 或者 SVG 的雪碧图,这种定制型不是很强,但是兼容性可以的方案也是备选项,在原文中也有提到。

所以说原文是什么:https://codepen.io/noahblon/post/coloring-svgs-in-css-background-images

CSS position sticky 的一些微小理解

$
0
0

position 中有哪些属性——相比这是一道送分题,大多数人都知道:static, relative, absolute, fixed,如果能答出 sticky——那么可以给你一颗五角星。

实际上,sticky 在一般的开发中并不常用,主要还是因为兼容性的问题,继续用 caniuse 查 sticky 你会发现支持性并不是很好,所以并不适合大部分业务场景。

不过考虑到我们的业务不需要考虑兼容性,于是就可以用了。所以学习了一波 MDN 的科普:

粘性定位是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。

其实意思也就是说在在某一个范围内会定在屏幕的位置,在范围之外会在容器(父节点的固定位置),就像知乎置于底层的功能条:

15001357593636.jpg
之前我们需要通过 JS 去搞,但通过 CSS 就很方便,一个 sticky 就能搞定。

当然实际上我发现,我们设定的其实是阈值,而并非实际的像 absolute 或者 relative 那样的位置,也就是说,设置:

#one { position: sticky; top: 10px; right: 15px; }

只有当水平滚动时,才会触发水平的阈值,所以想要达到 absolute / right: 15px 的效果的时候,通过包裹了一层定位到指定位置的父节点,并且控制了他的高度(更有效精准的控制 sticky 的范围),最终搞定了这个问题。

参考资料:https://developer.mozilla.org/zh-CN/docs/Web/CSS/position

Stream 与 Buffer 的相互转换

$
0
0

在工作的时候突然就有了这样一个需求,这一次正好也对本来不太熟的 Stream 有了一点更加深刻的理解……(好吧本来是根本没搞清楚状况)。

Stream,中文叫做流,和我们平时充值信仰的那个 Steam 还是差了那么一点的。所谓流,是一种消费的模型,被消费完就木有了,所以如果我们需要重复使用,就得存下来,也就是把 Stream 转成别的东西——Just Like Array or Buffer。

上代码表演一下:

function streamToBuffer(stream) {
  return new Promise((resolve, reject) => {
    let buffers = [];
    stream.on('error', reject);
    stream.on('data', (data) => buffers.push(data))
    stream.on('end', () => resolve(Buffer.concat(buffers))
  });
}

Buffer.concat(buffers) 之后就转换为了 Buffer,可以重复使用了。

逆向转换也很简单:

let Duplex = require('stream').Duplex;
function bufferToStream(buffer) {
  let stream = new Duplex();
  stream.push(buffer);
  stream.push(null);
  return stream;
}

太困了写不下去,这篇稍微水一下(逃)

参考:

http://derpturkey.com/buffer-to-stream-in-node/

TypeScript 中 import JSON 的正确姿势

$
0
0

最近 TypeScript 中毒,想想我一个弱类型出身的人,怎么就喜欢上了类型约束……当然这不是重点,重点可能还是 JS 没有接口,我没法靠 class 语法糖写的非常 OO……

关于 TS 的安利部分结束,今天我想说的其实是在 ts 中如何正确的 import json 格式。

首先我使用了基本姿势

import * as variable from './fooooooo.json'


结果发现他提示我并没有这个 module(Cannot find module),咋回事呀大佬,明明 JavaScript 中我可以正常使用。

查了一下,找到了一个方法:

命名一个 typings.d.ts

declare module "*.json" {
    const value: any;
    export default value;
}

接下来理论上你就可以愉快的使用了,比如

import * as variable from './fooooooo.json'

const data = (variable as any).data

当然在使用中由于我不小心写错了 config 文件的文件名所以没有生效于是……我又去找了别的方法。

比如可以使用 @types/node 加上 require 引入,如果你的服务中禁用 any,这也是一个比较好的方法。

甚至你可以手写读取文件,不过感觉上去还是比较智障……还是觉得前面两种方法更优雅一点

参考:

用 Node.js 快速开发 cli 应用攻略

$
0
0

这周花了两天 + 一天测试修 Bug 的时间完成了一个 cli,踩了一些坑,觉得也可以总结一波。

主要代码由于是 private 的不便公开,这里主要安利一些相关库。

cli 第一步:commander

commander 用于快速生成 command-line interface。

$ bili_live --help

  Usage: bili_live [options] [command]


  Commands:

    init                 init the live config
    add [roomids...]     add roomids to listen(space to split)
    remove [roomids...]  remove roomids(space to split)
    list [config]        list config
    run                  just run the listener once
    task <command>       add task to crontab for minute monitor
    backup <dist>        backup config to <dist> dir
    recover <src>        recover config from <src>

  Options:

    -h, --help     output usage information
    -V, --version  output the version number

可以非常方便的生成诸如此类命令行工具,遥想 sky-weibo-services 的时候我还是拿 process.argv 人肉分析的,到了 bili_live 的时候就开始使用 commander 了,果然方便了很多。

commander 还支持 git style commands,这个到时候可能可以单独水一篇(滑稽),本文暂不赘述。

信息存储:conf

理所当然的,可能需要存储一些 cli 工具长期使用的信息,之前我存储在了 json 文件中人肉读写,如果使用 conf 就不用自己写入了,用起来跟 localStorage 似的,非常方便。

读取配置文件 cosmiconfig

cosmiconfig 会查找文件,支持package.json 的指定字段或者 JSON 或 YAML rc 文件,.config.js 文件,甚至是 --config,在当前目录下未找到的情况下会不断向父目录查找。

读取最近的 package.json:read-pkg-up

除了配置信息,我们可能还需要读取包中的一些信息,这个时候 read-pkg-up 就有用了,它同时提供同步和异步操作,可以根据需求使用。

同步的 shell 库:shelljs

默认可以通过 child_process 执行 shell 语句,但是更多的时候我们希望是同步的,可以使用 shelljsexec() 方法中有 slient 选项可以不输出到控制台,用以执行一些赋值。

为 cli 加上升级提示:update-notifier

自己固然也可以请求 npm 检测,自己做缓存,不过用封装好的库开发起来更快更方便更稳定——比如 update-notifier。支持缓存,自定义输出等。

加入 Debug 模式

使用 debug 可以非常方便的为你的 cli 加上 debugger,可以进行更详细的输出。

滑稽的输出:node-emoji

作为一个新时代的逗比,emoji 符合基本法,那么问题来了,怎么样更好的输出呢——node-emoji 或许是个不错的选择,根据占位符输出。

给点颜色看看:chalk

接下来我们自然不希望我们的控制台枯燥无味,适当上色也是有必要的,chalk 提供的色彩选项估计可以让你画一个彩虹小马!

Node.js 快速开发 cli 应用攻略 - 坑篇

$
0
0

「哇夭寿拉,文章都按照上下章来写啦」

因为一直找不到时间写,上次匆匆做完了介绍,所以开个下集谈谈几个库遇到的坑。

commander

commander 的 bug 确实挺多的,所以下一个坑准备试试 yargs 这个库(差点就准备自己写了 OTZ)。

argument 'domain' (and probably others) cause naming collision


本身我们提供了一个 domain 选项进行配置,但是后来发现使用这个选项就会报错……换了个名字就好了。于是翻了一下 issue……看到了一串「保留字」,就是这串保留字让我坚定了下次再也不用的决心:

[ 'Command',
  'Option',
  '__defineGetter__',
  '__defineSetter__',
  '__lookupGetter__',
  '__lookupSetter__',
  '__proto__',
  '_allowUnknownOption',
  '_args',
  '_events',
  '_execs',
  '_maxListeners',
  '_name',
  'action',
  'addImplicitHelpCommand',
  'addListener',
  'alias',
  'allowUnknownOption',
  'apply',
  'args',
  'arguments',
  'bind',
  'call',
  'caller',
  'command',
  'commandHelp',
  'commands',
  'constructor',
  'description',
  'domain',
  'emit',
  'executeSubCommand',
  'hasOwnProperty',
  'help',
  'helpInformation',
  'isPrototypeOf',
  'largestOptionLength',
  'length',
  'listeners',
  'missingArgument',
  'name',
  'normalize',
  'on',
  'once',
  'option',
  'optionFor',
  'optionHelp',
  'optionMissingArgument',
  'options',
  'opts',
  'outputHelp',
  'parse',
  'parseArgs',
  'parseExpectedArgs',
  'parseOptions',
  'propertyIsEnumerable',
  'prototype',
  'rawArgs',
  'removeAllListeners',
  'removeListener',
  'setMaxListeners',
  'toLocaleString',
  'toString',
  'unknownOption',
  'usage',
  'valueOf',
  'variadicArgNotLast',
  'version' ]

如果你要使用保留字就会出现上述错误,并在后头说 3.0 会修复这个问题——然而一年过去了并没有_____

相关 issue: argument 'once' (and probably others) cause naming collision #404

git style commands

在一个「比较大」型的 cli 中,我们可能会想把子命令作为一个「分类」作用,也就是 sub sub command 去决定究竟做什么事情。

如果我们要执行一个添加 token 指令,大致有三种设计:

$ wang token add abcdefg
$ wang token:add abcdefg
$ wang add-token abcdefg

要实现第一种,目前需要使用 Git-style sub-commands,但是实际查了 issue 之后,感觉也有点问题,比如必须要是不带后缀的文件。命名必须是命令-子命令-sub sub command-... 的形式去解析每一层,虽然也可以写 bin 去映射,但是当时觉得和命令名耦合会不会不太好,最终自己实现了一套。

相关 issue: Sub Sub command issues #527

网络库

axios 与 form-data

axios 只有在浏览器环境才提供 form-data 的支持,所以额外使用了一个 form-data 库用于发送请求,当发送布尔值时发生了:

     Uncaught TypeError: first argument must be a string or Buffer
      at ClientRequest.OutgoingMessage.write (_http_outgoing.js:433:11)
      at FormData.ondata (stream.js:51:26)
      at FSReqWrap.oncomplete (fs.js:95:15)

感觉非常哲学,后来发现是因为 Boolean 的锅,必须要是 String 才可以,执行 Boolean 的 toString() 方法手动处理之后就能被正确处理了。

相关 issue: Error when trying to write boolean type #137

Nginx 动态 DNS 反向代理的几种写法

$
0
0

这篇文章的更新拖了半个月,真的是感动中国了。

Nginx 默认会缓存 DNS,大家都知道,如果做反向代理,其实是访问的目标 ip,所以一旦缓存了目标 ip 就会非常麻烦。

茴字的四种写法远近闻名,万万没想到,Nginx 动态 DNS 反向代理也有 N 种玩法,这主要取决于你用的 Nginx 版本,当然运维告诉我,最好是不要用动态 DNS,但是基于业务需要,咱们也没有办法。

茴字的第一种写法:Nginx

在 Nginx 中通过设置变量可以曲线救国,当然有缺点,通过变量的方法没有办法进行负载均衡,非常麻烦,当然如果你只有单机(在实际业务场景中几乎不存在),还是可以一试的:

server {
    ...
    resolver 127.0.0.1;
    set $backend "http://dynamic.example.com:80";
    proxy_pass $backend;
    ...
}

第二种写法:Nginx Plus

当然 Nginx 不太好用,增强版的 Plus 就提供了一个更棒的写法,它支持在 server 中写入:

server {
    ...
    resolver 127.0.0.1 valid=30s;
    proxy_pass http://upstream-sites;
    ...
}

第三种写法:Tengine

Tengine 是淘宝家的魔改 Nginx,其实我挺烦的,因为最开始我不知道我们用的是 Tengine,导致配置没有什么卵用,但是 Tengine 对于动态域名解析却支持的非常不错,官方文档写得也很清楚。

upstream backend {
    dynamic_resolve fallback=stale fail_timeout=30s;
    server a.com;
    server b.com;
}
server {
    ...
    location / {
        proxy_pass http://backend;
    }
}

其他

当然,还可以使用 Lua 脚本来实现,或者是别人的模块,但是考虑到怕鼓捣坏,我还是不乱折腾了,所以没有采取这种方案。

参考


Haproxy 入门、监控和 statsd 打点指北

$
0
0

作为一个奇奇怪怪的前端码畜,偶尔就会和一些奇奇怪怪的事情打交道,前一阵子也算是挺忙的了(尽管只是个没有更新博客,连续跳票的借口)。

这次使用 haproxy,是因为他作为负载均衡和反向代理而言配置起来比 Nginx 简单,并且自己就提供了监听页面。

我们来看一个完整的配置文件就知道了:

global
    daemon
    maxconn 500
    ulimit-n 8000
    user daemon
    group deamon
    chroot /var/empty
    pidfile /var/run/haproxy.pid
    log localhost local0 notice

defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend http-front
    bind *:80
    option httpclose
    maxconn 100

    reqirep ^Host:\\ www.abc.com Host:\\ abc.com

    acl host_abc_com hdr(host)      -i abc.com
    acl host_cn      hdr_end(host)  -i .cn
    acl host_xyz     hdr_beg(host)  -i xzy.
    acl url_xxx      url_reg        -i ^/xxx

    use_backend host-abc-com if host_abc_com
    use_backend host-cn      if host_cn
    use_backend host-xyz-url-xxx if host_xyz url_xxx
    default_backend default-servers

backend default-servers
    server default-servers1 127.0.0.1:8000 maxconn 32

backend host-abc-com
    balance roundrobin
    option httpcheck
    option forwardfor
    option httpclose
    option redispatch
    retries 3
    server abc-com1 192.168.1.100:80 check inter 2000 rise 2 fall 3 maxconn 32
    server abc-com2 192.168.1.101:80 check inter 2000 rise 2 fall 3 maxconn 32

backend host-cn
    balance roundrobin
    option httpcheck
    option forwardfor
    option httpclose
    option redispatch
    retries 3
    server abc-com1 192.168.1.100:80 check inter 2000 rise 2 fall 3 maxconn 32
    server abc-com2 192.168.1.101:80 check inter 2000 rise 2 fall 3 maxconn 32
    server abc-com3 192.168.1.102:80 check inter 2000 rise 2 fall 3 maxconn 32 backup

backend host-xyz-url-xxx
    balance roundrobin
    option httpcheck
    option forwardfor
    option httpclose
    option redispatch
    retries 3
    server abc-com1 192.168.1.100:80 check inter 2000 rise 2 fall 3 maxconn 32
    server abc-com2 192.168.1.101:80 check inter 2000 rise 2 fall 3 maxconn 32

相比起 Nginx 来说,可读性应该来说是很强的了,于是我写了一个配置生成器,就能够实现动态的配置生成了,由于这是我们的内部代码,不方便公开啦,不过可以看到,这一切都是非常简单就能实现的。

关于配置文件,只要通过看一下文档就可以知道怎么写了。

启动:

haproxy -D -f haproxy.cnf -p haproxy.pid -sf $(cat haproxy.pid)

Haproxy 自带了分析与监控(统计),这样可以设定一个页面来查看各个服务的健康状况。

要设置健康检查和健康检查的条件,只要在 backend 中设置:

  option httpchk GET /ping
  http-check expect ! rstatus ^5

以上条件的意思是:检查是否有 /ping,如果不是 5xx 则代表健康检查通过。

统计设置:

listen stats
  bind :9000
  mode http
  stats enable
  stats hide-version
  stats realm Haproxy Statistics
  stats uri /haproxy_stats
  stats auth test:test

通过 url 来设置页面路径,通过 auth 来设置用户名和密码。所以我们访问 localhost:9000/haproxy_stats 就能访问统计面板。

之后老板又多了新需求,需要打点,这里用到了 Grafana,一个开源的统计和监控程序,Haproxy 中的统计数据通过 ;csv 可以输出纯数据版用以给程序使用。

我们用过 Haproxy 的统计数据达到 statsd。

这里用到了一个开源的 Python 程序,可以简单用 supervisor 跑起来:https://github.com/softlayer/haproxy-statsd,把配置文件写好之后即可使用:

[haproxy-statsd]
haproxy_url = http://127.0.0.1:1936/;csv
haproxy_user =
haproxy_password =
statsd_host = 127.0.0.1
statsd_port = 8125
statsd_namespace = haproxy.(HOSTNAME)
interval = 5

然后在 Grafana 中可以用以下路径访问:stats.gauges.[namespace].[pxname].[svname].[statname]

之后,我们通过配置 Grafana 就可以实现监控,具体的不多说,看文档:http://docs.grafana.org/features/datasources/graphite/


后记:由于这是十月份开的坑,我十二月才……Emmm,所以……有的东西有点忘了,以后再用了在补充。

localhost / 0.0.0.0 与 127.0.0.1 的区别

$
0
0

在服务端开发的时候,我们往往会遇到这样的问题:localhost / 127.0.0.1 和 0.0.0.0 有什么区别,为什么我设置的 host 在外网无法被正常的访问。

首先先来说说最简单的 localhost,服务端开发的新手可能会认为:localhost === 127.0.0.1,毕竟在大多数场合里访问,似乎这两个地址都能访问到同样的网站。其实他们之所以一样,是因为在 /etc/hosts 中的指向,同样的你也可以改成其他地址,换言之,localhost 只是一个一般的域名,如同其他域名一样,你可以在 hosts 中任意修改其解析的指向。

而 127.0.0.1 和 0.0.0.0 的区别对于很多人而言则更为复杂:127.0.0.1 是本地回环地址,供自己访问,速度快,而 0.0.0.0 提供给外部使用,它意味着监听每一个可能网络地址,如果你的 IP 有一个局域网的地址和一个公网地址,很明显的通过 0.0.0.0 才能绑定你的每一个网络地址,达到公网访问的效果。

参考资料:

GitHub / GitLab Webhook 接口开发指南

$
0
0

前一阵子要开发一个从 GitLab / GitHub 通过 Webhook 拉取文件并且上传指定 OSS 的接口,于是就找起了 GitHub 和 GitLab 的官方文档。

当然官方文档实在是太过冗长,尤其是公司自建的 GitLab 版本还有可能不一致,所以就只能看搭建的 GitLab 提供的文档信息,对于一些遇到的问题就看命看版本查了。

当然所幸除了接口版本以外并没有遇到什么太大的坑,遇到了问题也可以根据版本来查,可以说这绝对是严格遵照 RESTful API 的一个优点,这一点无论是 GitHub 还是 GitLab 都做得很好。

由于 GitLab 和 GitHub 用的是两套接口,所以这里分开来介绍,不过方法都是相同的:

拉取压缩包,解压缩,上传文件。另一种方法是 git clone 下来,但是感觉 git clone 依旧太麻烦,所以就选了拉取压缩包。

GitLab

下载压缩包的接口:https://docs.gitlab.com/ee/api/repositories.html#get-file-archive

这里有两个参数:

id (required) - The ID or URL-encoded path of the project owned by the authenticated user
sha (optional) - The commit SHA to download defaults to the tip of the default branch

这里需要两个信息,project 的 id 与 sha,都可以通过 Webhook 获得:

const { checkout_sha: checkoutSha, project_id: projectId } = body

但是不同的是 id 是放在 参数(param)中的,而 sha 则是以 querystring 传入的(GitHub 与它不一样),如果不传入 sha,默认为 master 分支最新的代码。

当然,我们还会设置 secret token,这个 secret token 可以通过 header 中的 x-gitlab-token 获得。

然后我们就得到了 GitLab 的相关信息。

如果 API 中获取的内容需要权限,可以使用 PRIVATE-TOKEN 这一 header 传入 token。

GitHub

GitHub 的 Webhook 接口方式与 GitLab 有点不同,我们这里使用的还是 V3 版的 Restful 接口。

针对 GitHub 的 Webhook 最重要的是怎么计算出 secret token 与我们原先设定的相一致。GitHub 就没有 GitLab 那么简单了,我们需要先计算一番,具体可见Validating payloads from GitHub,如果看不懂 Ruby 没关系,下面提供一段 JavaScript:

const strBody = JSON.stringify(body); // 将整个 body 带出去变成 string
const sign = crypto.createHmac("sha1", secretKey) // secretKey 为你需要核对的 token
  .update(strBody)
  .digest("hex"); // 加密成十六进制
ctx.assert(ctx.header["x-hub-signature"].replace("sha1=", "") === sign, 403, "Wrong Sign");

然后把 header 中的 x-hub-signature 带有 sha1=value 中的 value 和 sign 作对比即可。

其次 GitHub 需要通过 header 中的 x-github-event 来判断 event 的类型,否则会出错,不同的 Event 传入的内容可能不一样,需要对 ping 做一些特殊处理,如果是 ping,直接返回 200,这样就可以过 GitHub 的检查了。

从 request body 中,你还可以找到 repository 信息和相对应的 commit 的信息,也可以获得 Repo Org 的信息(这里我们不需要仓库 Org,所以暂不表述)

const { repository, head, head_commit: headCommit } = body;

我们需要的接口是:

https://github.com/${org}/${repositoryName}/archive/${sha}.zip

其中:

repositoryName = repository.name
sha = headCommit.id

其中如果需要授权,在 header 中写入:

"Authorization": `token ${TOKEN}`

CSS 实现左右交换的效果

$
0
0

鞋厂的 App 有这么一个效果,而我的小伙伴刚好要做这样一个效果,于是就开始研究到底是怎么做的,Emmmm,大家都知道我平时糊设计图的功底为 0,所以好好研究了一下。:

2017-12-05 at 20.42.gif

首先布局用 flex 可以实现,而动画可以用 transform 或者 animation 实现。

这个布局第一反应是 justify-content: space-between,写了一波之后果然可以实现这个效果,但是动画该怎么定位才是大难题。

这里我们第一反应是用了 vw 这个单位,1vw 的意思是视口宽度的 1%。最简单的动画效果是用 transition 来做,变化属性是 transform,那么东京二字就应该定位在 100vw - ${x}px - ${width} 的位置。假设两侧的留白是 10px,则我们可以得出上海和东京的位置,而不需要借助 space-between

但事实是,如果用了 transform: translateX(),那么上海即使被位移了也会占据位置,如果我们用纯 CSS 实现,那么没有办法很好的定位东京二字。

所以我们回到 flex: space-between,那么东京默认为 -10px(向右留白 10px),上海为 10px(向左留白 10px),那么运动时东京定位在 -100vw + ${x}px + ${width},而上海定位在 100vw - ${x}px - ${width}

而至于不需要运动的中间按钮,只要留在中间就可以了。

具体的效果见:https://jsfiddle.net/csvwolf/4pgak94t/

折腾记:Hello Parcel

$
0
0

Parcel 是一个新出的「快速,零配置的 Web 应用程序打包器」,光是零配置这一点,看着就已经比 webpack 好一万倍了——webpack 让我们怀疑,前端是否要衍生出一种副职业:配置工程师。我至今也没有能够掌握「如何优雅的配置 Webpack」,之前本来准备用 poi来简化 webpack 配置,不过突然想起了这个更高、更快、更强、(更懒)的新道具,于是又折腾了起来。

Parcel 有啥优点

首先,他的优点是零配置,也就是说,你不用自己配置就能支持 JS / CSS / HTML,开箱即用。此外,零配置不代表不能配置,它提供了接口,可以让你进行一些插件的开发,在此基础上做出一些 DIY,零配置的意义其实是带默认值、开箱即用的配置,而并非不可配,灵活性差。

其次,速度快,这也是很重要的一个提高,webpack 要打包大型项目不免让人觉得有点慢,而 Parcel 利用多核与缓存(你可以在打包后在目录内看到 .cache),让打包速度提高了几倍。

Hello World for Vue and Typescript

首先,parcel 的插件列表可以在相关 issue 中看到,中文也已经有一个 awesome-parcel 了(但不太全,可以去补充一波)

这里我就配了一下 Vue + Typescript,事实上,单用其中的一个确实可以达到开箱即用,但是两个结合的时候还是踩了一些坑。

整个项目可以看:https://github.com/csvwolf/parcel-test

从项目中就可以看出非常的简单,配置以下依赖:

  "devDependencies": {
    "parcel-bundler": "^1.2.0",
    "parcel-plugin-typescript": "^0.2.1",
    "parcel-plugin-vue": "^1.0.1",
    "typescript": "^2.6.2",
    "vue": "^2.5.11"
  }

需要注意的是,尽管说好了全局安装 parcel-bundler,然而在项目里还要在安装一次,而且需要由这个来启动,否则会无法正常运行。

然后配置 tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "strict": true,
    "module": "es2015",
    "moduleResolution": "node"
  }
}

配完官方推荐配置,滚蛋,两个插件安装完就能搞定了。

哈?这么简单

这么看来,似乎并没有什么坑——Emmm,比起 webpack 来,我们在前端有一个常见的功能:alias,我似乎并没有看到这个功能,可是按照官方的设定,基本上是这么玩的:

官方配置不能满足 -> 插件 -> 插件不能满足 -> 插件暴露配置

那么我应该给 plugin 提 issue 还是 parcel 提 issue 呢……我陷入了沉思。

此外,还有一些微小的(也不怎么小的)bugs 和待改进的功能点,不过 plugin 的作者也在积极修复,态度也非常棒,但是相关 plugin 的用户还不是很多,感觉踩坑的人还是略少,期待 parcel 生态有更好的发展。总的而言,如果目前要把复杂的项目迁移到 parcel,我还不太推荐这么做,不过如果是在现有的项目中使用试试水或者只是单纯的懒得配置,那真是极好的。

Viewing all 195 articles
Browse latest View live




Latest Images