zwhu / blog Goto Github PK
View Code? Open in Web Editor NEW嘛,写 blog 也要遵守基本法。
License: MIT License
嘛,写 blog 也要遵守基本法。
License: MIT License
只是记录下安装过程和配置,也许你刚好会用到:
npm install --save-dev jest power-assert babel-preset-power-assert
// .babelrc
{
"presets": [
"power-assert"
]
}
// package.json
{
"test": "jest"
}
npm test
TL;DR
前面大段的内容都是基本概念的介绍,建议没时间的同学直接拖到最下面看。
Web 缓存是可以自动保存常见文档副本的 HTTP 设备。对,当谈到缓存的时候,就是指那些设备,如浏览器,代理缓存服务器等。
通过网络获取内容既缓慢,成本又高:大的响应需要在客户端和服务器之间进行多次往返通信,这拖延了浏览器可以使用和处理内容的时间,同时也增加了访问者的数据成本。因此,缓存和重用以前获取的资源的能力成为优化性能很关键的一个方面。
使用缓存有下列的优点:
有很多小网站没有对文档做缓存处理,这样客户端每次访问相同的文档(例如 jQuery.js)的时候,都要从服务器下载相同的文档到本地客户端,造成大量的冗余数据传输。
缓存会缓解有限广域网络带宽的瓶颈问题。很多网络会为本地客户端提供的带宽比为远程服务器提供的带宽更宽。如果客户端可以从一个快速局域网的缓存中获得一份副本,自然可以提高性能。
12306 的春运,微博的春晚红包等都会遇到这种情况。12306 放票的时间段,会有大量的用户去抢票,出现瞬间拥塞。瞬间拥塞可能会使网络和 web 服务器发生崩溃。 DDOS 也是相同情况。
假设淘宝的主服务器都放在杭州的一台服务器上。而在美国的客户端打开了淘宝,需要下载淘宝的首页;再假设数据的传输都是以光速的速度传输。杭州到华盛顿的距离大概有14,000公里,这样光速自身传输就需要大概90ms的时间(算上请求和返回的时间),如果淘宝页面上只有20个图片,这样单连接的情况下,就大概需要(打开连接请求 90ms + GET web 页面的90ms + GET 所有图片的 90 * 20 = 1800 ms)1980ms 的时延。注意,这个只是时延。也就是说这个距离下 20 张图片就会比客户端在本地的请求延迟大概 2s 的时间。
服务器上的文本内容随时可能发生变化,如:淘宝首页的一个文件中需要增加记录用户点击日志的功能,所以需要修改某个js文件,以增加对应的功能。对于这种情况,缓存就要不时的对其进行检测,看看它们保存的副本是否仍是服务器上最新的副本。对于这种检测,就被称为新鲜度检测,这些新鲜度检测就被称为 HTTP 再验证。
为了有效的进行再验证,HTTP 定义了一些特殊的请求,不用从服务器上获取整个对象,就可以快速检测出内容是否是最新的。最常用的是 If-Modified-Since 首部(后面的内容会提一下 ETag 和 If-None-Match)。当这个首部被加入到 GET 请求中去,就可以告诉服务器:只有缓存了对象的副本之后,又对其进行了修改的情况下,才发送此对象。
对于服务器接收到 GET If-Modified-Since 请求时大概会发生以下三种情况:
再验证命中
如果服务器对象未被修改,服务器回想客户端发送一个小的 HTTP 304 Not Modified 响应。
再验证未命中
如果服务器对象与已缓存副本不同,服务器向客户端发送一条普通的、带有完整内容的 HTTP 200 OK 的响应。
对象被删除
如果服务器对象已经被删除了,服务器就会回送一个 404 Not Found 响应,缓存也会将其副本删除。
If-Modified-Since 是 HTTP 请求首部,可以与 Last-Modified 服务器响应首部配合工作。原始服务器会将最后的修改日期附加到所提供的文档上去。当缓存要对已缓存文档进行再验证时,就会包含一个 If-Modified-Since 首部,其中携带有最后修改已缓存副本的日期。
<!-- test.html 最后一次修改时间: 2016-3-12 20:03 -->
<!Doctype html>
<html>
<head>
<title>hello</title>
</head>
<body>
<div>no cache</div>
</body>
</html>
// demo1.js
'use strict'
const http = require('http')
const fs = require('fs')
const onRequest = (req, res) => {
const filepath = './test.html'
, file = fs.readFileSync(filepath)
, stats = fs.statSync(filepath)
, mtime = stats.mtime
, reqMtimeString = req.headers["if-modified-since"]
let status = 200
if(reqMtimeString) {
const reqMtime = new Date(reqMtimeString)
if(reqMtime.getTime() === mtime.getTime()) status = 304
}
res.writeHead(status, {'Content-Type': 'text/html', 'Last-Modified': mtime})
if(200 === status) res.write(file)
res.end()
}
http.createServer(onRequest).listen('8000', () => console.log('server start:8000'))
上面是用 Node.js 写了一个简易的服务器,检测 test.html 是否有变化,如果最后一次修改的时间和客户端的时间不同的话,就返回新鲜的文档。
通过 node demo.js
运行服务器。打开浏览器的开发者工具(记得把 disable cache 的选项勾掉),可以看到此时HTTP请求的 header 为:
General
Request URL:http://localhost:8000/
Request Method:GET
Status Code:200 OK
Remote Address:[::1]:8000
Response Headers
HTTP/1.1 200 OK
Content-Type: text/html
...
Last-Modified: Sat Mar 12 2016 20:03:58 GMT+0800 (CST)
Request Headers
GET / HTTP/1.1
Host: localhost:8000
...
If-Modified-Since: Sat Mar 12 2016 20:03:58 GMT+0800 (CST)
此时的返回的状态码为 200, 服务器设置了 Last-Modified 首部之后,浏览器端会加上 If-Modified-Since 的头部。之后再刷新浏览器,查看开发者工具,发现一般头(即 General)的 status code 变成 304 Not Modified,即上述的再验证命中。
再修改 test.html 的内容:
<!-- test.html 最后一次修改时间: 2016-3-12 20:26 -->
<!Doctype html>
<html>
<head>
<title>hello</title>
</head>
<body>
<div>change cache</div>
</body>
</html>
刷新浏览器,此时的 header 如下:
General
Request URL:http://localhost:8000/
Request Method:GET
Status Code:200 OK
Remote Address:[::1]:8000
Response Headers
HTTP/1.1 200 OK
Content-Type: text/html
...
Last-Modified: Sat Mar 12 2016 20:26:36 GMT+0800 (CST)
Request Headers
GET / HTTP/1.1
Host: localhost:8000
...
If-Modified-Since: Sat Mar 12 2016 20:03:58 GMT+0800 (CST)
可以看到一般头的 status code 又变成了 200,且响应头的 Last-Modified 变成最后一次修改时间,即上述的再验证未命中,服务器会返回修改后的文件。
对象被删除的情况就不再写代码验证了。
服务器也可以通过添加一个 HTTP Cache-Control 首部和 Expires 首部让缓存可以在缓存文档未过期的情况下随意使用这些文档副本。
HTTP/1.0 的 Expires 首部或 HTTP/1.1 的 Cache-Control: max-age 响应首部来指定过期日期。Expires 使用的是绝对日期,绝对日期依赖于计算机时钟的正确设置,如果计算机时钟不正确,会造成缓存的过期日期不正确,可能就达不到缓存的初衷,所以在 HTTP/1.1 就增加了 Cache-Control: max-age 来替代 Expires 。
max-age 响应首部表示的是从服务器将文档传来之时起,可以认为此文档处于新鲜状态的秒数,还有一个s-maxage的首部,其行为与 max-age 类似,仅适用于共享缓存。
服务器可以请求缓存不要缓存文档(Cache-Control: no-store),或者将最大使用期设置为零(Cache-Control: max-age=0),从而在每次访问的时候都进行刷新。
下面是一段 Nodejs 实现的 max-age 代码:
// demo2.js
'use strict'
const http = require('http')
const fs = require('fs')
const onRequest = (req, res) => {
if('/req.js' === req.url) {
let filepath = './req.js'
, file = fs.readFileSync(filepath)
res.writeHead(200, {'Content-Type': 'text/javascript', 'Cache-Control': 'max-age=60'})
res.write(file)
res.end()
} else {
let filepath = './test2.html'
, file = fs.readFileSync(filepath)
res.writeHead(200, {'Content-Type': 'text/html'})
res.write(file)
res.end()
}
}
http.createServer(onRequest).listen('8000', () => console.log('server start:8000'))
// req.js
'use strict'
console.log(123)
<!-- test2.html -->
<!Doctype html>
<html>
<head>
<title>hello</title>
</head>
<body>
<div>no cache</div>
<script src='/req.js'></script>
</body>
</html>
打开浏览器,先打开开发者工具,再输入地址之后,按回车可以看到下图,req.js 没有被缓存。
重新再浏览器输入地址回车(手动刷新和 cmd+r 属于强制刷新,会清除缓存),可以看到下图 req.js 已经被缓存了(from cache):
由于在服务器上设置的缓存失效时间是 60s,所以 60s 之后再看,此时的缓存已经失效,又会像第一幅图一样, req.js 没有 from cache。
HTTP 允许用户对 Etag 的版本标识符进行比较。在服务器端设置 Etag 首部之后,客户端会对应的生成 If-None-Match 首部。服务器端可以通过 If-None-Match 首部和对应的文档内容的hash值或者其它指纹信息进行校验,来决定是否返回新鲜的文档。
由于使用 Etag,服务器端每次都要对文档内容 hash 来确定是否返回新鲜的文档,还是会浪费大量的服务器资源,所以 Etag 的缓存策略不建议使用。
所以结合 Google 给出的最优缓存策略,总结如下:
上面第一条所说的指纹码一般是指文档内容的 hash 值,这个可以通过 gulp,webpack 等打包工具在生成文件的时候就生出 hash 值,附在文件名后面,例如:jquery.min.js,根据文档生成的 hash 值为 1iuiqe981823,文件名可以自动生成为: jquery.min.1iuiqe981823.js。这样既可以保证在文档没有变化是可以从缓存中读取,又可以保证文档在有变化可以及时更新。
本文大部分内容都是直接引用『HTTP 权威指南』,最后一部分的最优策略是参考 Google Developers 的文档。有些许内容是理解之后给出的代码实现或验证。
作者 @zwhu
原文章 @github
知乎上有人问「比一个人吃火锅更寂寞的是什么?」我想回答「一个人写前后端,却要装成一个团队。」
我们在前期开发的过程中,更多是一个人单打独斗,因为是自己一个人,可以把代码写的很随意,也不用注意什么工程化的东西;但作为一个有追求的程序员,不能为未来的自己挖坑,坚决走前端工程化的路线。
虽然我是一个人,但我是一个团队。
所以这是我自己在开发过程中总结的一些知识,慢慢会写成一个系列吧。「单页应用」作为系列的第一篇。
对于单页应用我的定义是:在浏览器地址栏输入地址之后,服务器获取到HTML文档,之后所有页面的呈现都在这份HTML文档之上进行。
因为这是流行啊。即使我司对性能啥的完全没有任何要求,我还是强行用了单页应用,自己不努力,没人帮我学。具体有什么好处还是坏处,如果自己没写过单页应用,即使别人说一大堆好处坏处你也还是不懂。(这是吐槽,不用理会)
在浏览器地址栏输入地址之后,会从服务器端下载HTML文档并开始渲染。(这个谁能看懂,谁看吧,反正我看不懂)在渲染的过程中,解析 script 标签,外部引入的话,就发起请求获取 JavaScript 文件。
页面渲染完成,之后所有网站的内容都会在这个页面上呈现,所有的操作也只会在这个页面进行。
假设我们在浏览器输入 http://xxxx.com 的时候,服务端会返回 index.html 文档如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
<div id="app">
<nav id="nav"></nav>
<div id="container"></div>
</div>
<script src="/jquery.js"></script>
<script src="/app.js"></script>
</body>
</html>
这里插一句为什么把 script 标签放到最后?浏览器在渲染的时候会被 script 标签阻塞,影响页面的首次渲染。可能会因为 JavaScript 文件过大,等待加载时间过长(如果是外部引入),页面一直是空白。
我们在构建单页面应用时,大部分的内容通过 JavaScript 生成。在 app.js 中,我们会生成一个导航:
var $nav = $('#nav')
var sLi = ['home','about','article','history'].reduce(function(pre, n) {
return pre + '<li><a href="/' + n + '">' + n + '</a></li>';
}, '');
var sUl = '<ul>' + sLi + '</ul>';
$nav.html(sUl);
之后我们想根据导航生成不同的页面内容:
var $container = $('#container')
$('#nav ul li').on('click', function() {
$container.html('这是' + $(this).find('a').text() + '区域哦')
return false;
})
然而现实中,我们的页面多数是根据后端返回的数据来渲染,例如我们的后端提供了一个接口 /api/home
, 通过这个接口可以获得数据 ["今", "天", "天", "气", "不", "错", "啊"]
,当我们进入 home 页面的时候页面上会结合通过 api 接口获取来的数据展示新的页面:
$('#nav ul li').on('click', function() {
var route = $(this).find('a').text()
if('home' === route) {
$.get('/api/home', function(data) {
$container.html(data.join('') + '!')
})
} else {
$container.html('这是' + route + '区域哦')
}
return false;
})
这是我刚接触单页应用时候比较头疼的地方。
没有路由,我就不知道你在哪儿。 --- by 我要某上头条君
通过 Ajax 可以获取服务器的数据然后再渲染到页面上,这个方法虽然交互很友好,不需要重新刷新页面就可以看到新的内容;但是有一点不好,那就是当点击导航后,浏览器地址栏的链接不会有变化,用户完全不知道现在在哪个页面。
不过这里有个知识点是需要大家理解的,当我们在浏览器地址栏输入地址或者通过其他页面的外链跳转到这个页面时,整个页面都会刷新一遍,从服务器获取 HTML 文档渲染。在 JavaScript 中,可以通过 location.href 修改地址,但是这个方法和浏览器输入地址的效果是相同的,那有没有办法只修改浏览器的地址栏,而不刷新整个页面呢?
还好我们有 HTML5 的 History Api 来解决这个问题,当然低版本的IE浏览器也可以通过 location.hash 的方法实现,hash 来解决的方法不在这里深究了。毕竟低版本浏览器已经没必要支持了。
$('#nav ul li').on('click', function() {
var route = $(this).find('a').text()
history.pushState({ title: route }, route, route)
show()
return false;
})
function show() {
var route = window.location.pathname
if('/home' === route) {
$.get('/api/home', function(data) {
$container.html(data.join('') + '!')
})
} else {
$container.html('这是' + route + '区域哦')
}
}
在点击导航之后,会将当前的路由体现在浏览器的地址中。不过,浏览器地址变化之后,点击前进后退,页面并不会有什么变化。我们可以通过对 window 对象绑定 popstate 方法来解决
window.addEventListener("popstate", show);
传统的网站,用户通常输入具体 url 来进入相关页面,例如:我们进入 GitHub 自己的主页是输入的是 github.com/user
;但是单页面应用,我们的页面都是在一个空的 html文档中通过 JavaScript 生成的页面,所以服务器不能通过url来返回具体的页面(其实是可以的,同构
,不在这里展开),那么应该怎么做呢?
对于服务器来说,不用管那么多,当用户输入 url 之后,只要 url 符合一定的规则(例如:请求头的 accept 是 'text/html' 或者所有非 /api/ 开头的 url 等),都返回默认的 index.html。JavaScript 可以通过当前的路由来渲染:
function show() {
var route = window.location.pathname
if('/home' === route) {
$.get('/api/home', function(data) {
$container.html(data.join('') + '!')
})
} else if('/' !== route) {
$container.html('这是' + route + '区域哦')
}
}
// 初始化的时候就调用
show()
到此为止,单页面的应用就算介绍完了。理解了单页面应用的原理,对于最近流行的 Angular
, React
, Vue
之类库或框架的才算刚刚开始。
如果觉得本文对你有帮助的话,就点个推荐呗。
下一篇我会介绍 一个人的团队(二)--- 神兵利器之 webpack 和 webpack-dev-server
- 一元运算符 * (克莱尼闭包)具有最高的优先级,并且是左结合的。
- 连接(即ab)具有次高的优先级,它也是左结合的。
- | (运算符并)的优先级最低,并且也是左结合的。
例如:我们可以将 (a)|((b)*(c))
改写成a|b*c
。
上面是基础的正则约定,下面总结了一份常用正则表达式扩展中的优先级列表(顺序从高到低)。
优先权 | 符号 |
---|---|
最高 | \ |
高 | ()、(?:)、(?=)、[] |
中 | *、+、?、{n}、{n,}、{m,n} |
低 | ^、$、(\任何元字符、任何字符) |
最低 |
定律 | 描述 |
---|---|
r | s=s |
r | (s |
r(st) = (rs)t | 连接满足结合律 |
r(s | t) = rs |
ε人= rε = r | ε 是单位元 |
r* = (r | ε)* |
r** = r* | * 具有幂等性 |
虽然说这个表格作用不是很大,但是熟练掌握之后可以少写不少让人讨厌的小括号,也算是珍爱生命,珍爱眼睛吧。
最近在用 node 学写爬虫,也可以叫模拟登陆,遇到某个网站在返回的 HTML 中插入了一个生成页面token的script。
这个HTML的结构大概类似
<!Doctype html>
<html>
<head></head>
<body>
<div></div>
<script>
(function (w) {
w.token = (function () {
// ....
// 生成token
return token;
})();;
})(window);
</script>
</body>
</html>
首先拿到这个页面,然后获取script标签的表达式字符串,剩下的事就是怎样执行表达式字符串,并获取token。
在此处的情景中用eval也是挺不错的选择,但是既然 JS 不推荐这种用法,那我们就换成 new Function 来做。
看下 new Function 的用法如下
new Function ([arg1[, arg2[, ...argN]],] functionBody)
new Function会返回一个函数。例如 fn = new Function('a', 'b', 'return a + b')
会返回一个函数对象
fn = function(a, b) {
return a + b
}
so,我们可以使用 new Function 构造一个函数,用来执行获取到得表达式,并返回token。
fn = new Function('window', functionBody + ';return window.token')
var token = fn({})
console.log(token)
构造的这个函数有个window形参,作为函数体中立即执行表达式的实参传入最内层的函数中。所以在最后执行的时候 return window.token 便会获得token 的内容。
失败就是失败,会有什么收获?
很多人都会说失败了没关系,爬起来,从这次失败中总结经验和教训
可是失败会有什么经验,总结失败的经验,再经历一次失败?
也许成功比失败获得的经验更多,为什么不去总结成功的经验
一些人不说真话,殊不知被当成傻子的假话更伤人
历史的车轮滚滚向前,从不会在意碾死了几只蚂蚁
记得之前看到一句话,大意是:
凡是能用 JS 写的轮子,最终都会用 JS 重写一遍
昨天把 Linux 的 tree 用 Node.js 重写了一遍,算法用到了深度优先搜索和前序遍历。具体用法可以看这里。
拿我写的walk-dir-tree
来做例子,可以看到基本目录结构如下:
.
├── .git
├── .gitignore
├── .npmignore
├── bin
| └── index
├── example
| ├── a.js
| ├── b.js
| └── c
| └── d.js
├── index.js
├── node_modules
├── package.json
└── readme.md
.git
,.gitignore
这两个不需要介绍了。.npmignore
的用法同 .gitignore
, 用来忽略上传到 npm 仓库的文件。 bin
目录存放的是可执行脚本,稍候介绍。index.js
作为模块的主文件。 node_modules 存放的是引入的模块,package.json
为 npm
包的描述文件。 readme.md
为模块的介绍。
初始化包可以用过 npm init
命令来生成 package.json
文件,系统会出现交互提示,按照提示填写即可。
{
"name": "walk-dir-tree", // 包名称
"version": "0.0.4", // 包版本号
"description": "walk-dir-tree is a recursive directory listing command that produces a depth indented listing of files like tree", // 包描述
"keywords": ["tree", "directory", "walk-dir-tree", "co"], // 关键字
"repository": "https://github.com/zwhu/walk-dir-tree", // 包的repo链接
"bin": "./bin/index", // 命令行工具所要引入的包得位置
"main": "index.js", // 入口文件
"author": "zwhu <[email protected]>", // 作者
"license": "ISC", // 包的开源许可证
"dependencies": { // 包依赖
"co": "^4.6.0",
"colors": "^1.1.2",
"mz": "^2.1.0"
}
}
上面就是我的 package.json
的设置。在设置完 package.json
之后,可以把自己编写的包提交到 npm 去了。通过 npm adduser
在npm上创建一个输入自己的账户,千万要记住密码!
然后通过 npm publish <folder>
就可以上传 npm 包。如果在当前 package.json
所在的目录,执行$ npm publish
,等待上传好就可以。
上传完,去 npm 得官网,输入自己刚刚注册的账号,可以对 npm 包进行管理。
上面说的是普通的 npm 包,如果想像我一样上传一个可以让用户在本地执行的 shell 包,需要package.json
中的 bin 字段中写上需要执行的文件的相对路径。用户安装的时候需要全局安装了$: npm install 项目名称 -g
。 安装完在命令行中输入$: 项目名称
可以执行你上传的库。
function tounicode(s) {
return s.charCodeAt().toString(16)
}
在JavaScript中,字符串可以表示为 '\u{${tounicode(s)}}' 的格式 ,实际作用可以用正则来匹配一些预定义的特殊字符,比如 $, + 之类的
作为备忘
FP(Functionial Programming)
的概念很火,React
、Redux
全家桶系列都是基于 FP
的概念,而 Scheme
作为 FP
的鼻祖 Lisp
的一门方言,又是王垠曾经很推崇的语言,很是期待掌握这门语言。Scheme
写的,而我最近想学习这本经典书籍,且书里推荐使用 Scheme
来学习。不过不用 Scheme
也没有关系, 老赵在 老赵书托(2):计算机程序的构造与解释 有推荐 IronScheme
。在 官网下载 MIT/GNU Scheme
。我的电脑是 MBP, 所以选择 OSX 版本下载。 下载完了之后解压缩,把下图的 MIT/GNU Scheme
拖拽到 Applications 文件夹即可。
打开 Finder 在 Applications 或者应用程序文件夹中找到 MIT/GNU Scheme app
右键选择显示包内容
然后进入 /Contents/Resources/ 目录,双击 mit-scheme bash
如下图,说明安装一切顺利
不过每次这样每次通过 Finder 启动 Scheme
都非常麻烦,所以我们需要简单点的方法来启动:
$ sudo ln -s /Applications/MIT\:GNU\ Scheme.app/Contents/Resources/mit-scheme /usr/bin/scheme
如果你出现 ln: /usr/bin/scheme: Operation not permitted
的错误,这是因为 mac osx 在 El Capitan
加入 Rootless
机制,所以没有权限来设置软连接,在网络上搜索之后有些简单的设置方法:设置方法链接
$ echo "export MITSCHEME_LIBRARY_PATH=\"/Applications/MIT\:GNU\ Scheme.app/Contents/Resources\"" >> ~/.profile
$ echo "export MIT_SCHEME_EXE=\"/usr/local/scheme\"" >> ~/.profile
$ source ~/.profile
如果在输入下面的命令之后,出现下图内容,说明设置已经生效
$ scheme
注: 设置快捷启动方式的 原文 在此,但是不知道为什么打不开了,还好我很久之前保存过这个页面的快照,遂整理并加入解决权限问题的方法一并放在此处。
在 MIT-Scheme
中输入 code 非常麻烦,光标不能回退和上下移动,所以比较简单的方法就是运行已经写完的文件:
$: scheme -load yourfile.scm
MIT-Scheme
到此,可以愉快地使用 MIT-Scheme
开始我的 SICP
之旅了,祝大家也能早日熟练使用 Scheme
开发程序。
朝霞不出门,晚霞行千里;古人在日复一日的生活中总结了经验并作为谚语保留下来。一些归类总结能力更强的先人,通过把这些日常生活经验进行分类组合,形成『二十四节气』,作为后人劳作时的指导
抽象是对日常生活经验的共性进行总结并沉淀。体系化的建设的对更大范围的经验进行抽象之后的结果按照个人理解进行结构化的分类组合后并作为日后的行事准则
结构化思维的训练,抽象能力的训练,演变成见微知著的能力并沉淀成方法论后,便是个人的核心竞争力(并不是)
作者 @zwhu
源码 @raw
老板让我这个不会P图的伪前端把公司的Logo放到公司网站上,结果给了我一张 JPEG 格式的图片,作为一个有追求的码农,怎么能现学 ps,于是利用一点HTML5+NODE的知识写了个转换的脚本。
此脚本的功能是将 JPEG 格式的图片中的白色背景转成透明,然后再保存成PNG格式的图片;如果图片本身有不想被转换的白色区域,使用其他方法吧。
原理很简单,只有两点:
RGBA的解释 我们知道图片由很多个像素点组成,每个像素点都有颜色,而颜色是由三基色RGB构成。而A是Alpha通道,用作不透明度参数,0%为完全透明,100%是完全不透明。所以说如果我们想实现白色背景的JPEG 图片转成透明的 PNG 图片,只需要将白色背景对应的像素点得Alpha值变成0就好了。
HTML5新增加了canvas,可以用来绘制图形,也可以对图片的像素进行操作。通过 getImageData()
方法可以返回原始的像素信息 ImageData 对象。ImageData 对象中的像素是可写的(由 RGBA 组成),因此我们可以修改像素的Alpha通道值,然后再通过 putImageData() 方法将这些像素复制到画布中。
有了上面的知识,我们可以很轻松的通过查 canvas 的 API 来写出转换的代码(ES6),代码在下面,代码不难,也写了很详细的注释:
import Canvas from 'canvas'
import fs from 'fs'
const Image = Canvas.Image
// 初始化 img 和 start time
// 获取命令行输入的源图片和保存的图片地址
let img = new Image
, start = new Date()
, rawPath = process.argv[2]
, savePath = process.argv[3]
// 在命令行中没有输入图片地址,抛错
if(!rawPath)
throw new Error('input raw image path')
if(!savePath)
throw new Error('input save image path')
img.onerror = function(err){
throw err
}
// 图片加载完成
img.onload = function(){
// 获取图片的width和height
let width = img.width
, height = img.height
, canvas = new Canvas(width, height)
, ctx = canvas.getContext('2d')
// 将源图片复制到画布上
// canvas 所有的操作都是在 context 上,所以要先将图片放到画布上才能操作
ctx.drawImage(img, 0, 0, width, height)
let imageData = ctx.getImageData(0, 0, width, height)
// 获取画布的像素信息
// 是一个一维数组,包含以 RGBA 顺序的数据,数据使用 0 至 255(包含)的整数表示
// 如:图片由两个像素构成,一个像素是白色,一个像素是黑色,那么 data 为
// [255,255,255,255,0,0,0,255]
// 这个一维数组可以看成是两个像素中RBGA通道的数组的集合即:
// [R,G,B,A].concat([R,G,B,A])
, data = imageData.data
// 对像素集合中的单个像素进行循环,每个像素是由4个通道组成,所以 i=i+4
for(let i = 0; i < data.length; i+=4) {
// 得到 RGBA 通道的值
let r = data[i]
, g = data[i+1]
, b = data[i+2]
// 我们从最下面那张颜色生成器中可以看到在图片的右上角区域,有一小块在
// 肉眼的观察下基本都是白色的,所以我在这里把 RGB 值都在 245 以上的
// 的定义为白色
// 大家也可以自己定义的更精确,或者更宽泛一些
if([r,g,b].every(v => v < 256 && v > 245)) data[i+3] = 0
}
// 将修改后的代码复制回画布中
ctx.putImageData(imageData, 0, 0)
// 将修改后的图片保存
let out = fs.createWriteStream(`${__dirname}/${savePath}`)
, stream = canvas.pngStream()
stream.on('data', function (chunk) {
out.write(chunk)
})
stream.on('end', function () {
console.log(`保存到 ${__dirname}/${savePath}`)
console.log(`耗时: ${new Date()-start}ms`)
})
}
img.src = `${__dirname}/${rawpath}`
前端在开发过程中接触到的算法最多的莫过于排序和 dfs(深度优先遍历) 。 dfs 算法广泛用于图(树是图的一种)的遍历,如:没有
querySelectorAll
的时候,根据 classname 或者 tag 查找 element。
关于 dfs 算法的遍历过程,我简略的画了一个示例图:
最近在实际业务场景中,跟后端约定页面中所有组件的消息根据页面上的组件 id 聚合到一个对象中,后端返回的是类似如下的一个树形数据结构。前端需要把所有的错误信息都拿出来,按照页面上所有组件的顺序聚合显示在一个全局信息面板组件上(至于按照组件顺序排序算法本文暂且略过)
let tree = {
'id1': {
message: 'hello'
},
'id2': {
message: 'world',
children: {
'id2-1': {
message: 'haha',
children: {
}
},
'id2-2': {
message: 'heihei'
}
}
}
}
由于某些大组件可能是由多个小组件层层嵌套组合而来,且每个小组件都有相应的 message 需要展示,所以就选择了上述的树形结构来表达组件的信息。这个时候就会有人问,为什么不让后端把所有 message 都聚合到数组里面?因为前端不仅需要把这些错误信息聚合到一起展示,也需要把错误定位到具体组件上
function dfs(tree = {}, messages = []) {
let i = 0;
if(!messages) messages = [];
if(tree.message) messages.push(tree.message);
const keys = Object.keys(tree.children || {});
while (i < keys.length) {
dfs(tree.children[keys[i]], messages);
i += 1;
}
return messages;
}
tree = {
message: null,
children: tree
};
dfs(tree);
function dfs(tree = {}) {
const array = [tree];
let messages = [];
while (array.length) {
const top = array.pop();
if (top.message) {
messages.push(top.message);
}
const keys = Object.keys(top.children || {});
let i = keys.length;
while (i > 0) {
i -= 1;
array.push(top.children[keys[i]]);
}
}
return messages
}
tree = {
message: null,
children: tree
};
dfs(tree);
在实际使用中,考虑到数据结构的层数没那么多,其实尾递归版本和非递归版本所消耗的时间在浏览器的优化下几乎可忽略了。
🤔
今天在看 ES7 新增的部分 Api 的时候刚好看到 padStart 的这个方法,好像还挺实用的,而且也想在正式开始工作之前先找找写代码的感觉,于是顺手(其实还是花了不少时间的)就实现了这个 polyfill。
相关的 API 用法在 MDN 上有说明。 链接 下面是具体实现
if(!String.prototype.padStart)
String.prototype.padStart =
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
function (maxLength, fillString=' ') {
if(Object.prototype.toString.call(fillString) !== "[object String]") throw new TypeError('fillString must be String')
let str = this
// 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉
if(str.length >= maxLength) return String(str)
let fillLength = maxLength - str.length,
times = Math.ceil(fillLength / fillString.length)
// 这个算法叫啥?
// SICP 的中文版第 30页 有用到同种算法计算乘幂计算
while(times >>= 1) {
fillString += fillString
if(times === 1){
fillString += fillString
}
}
return fillString.slice(0, fillLength) + str
}
// padStart 对于我来说最常用的地方就在于时间或者数字格式补全了
'5'.padStart(2, '0') // '05'
'15'.padStart(2, '0') // '15'
ps:
写完之后突然发现这个好像就是之前 npm 的 left-pad
删库事件;隐约记得好多人都重写过这个库。anyway, 练手的目的算是达到了。
一个前后端分离的 Node 项目,怎么合理分配依赖包的 Key?
package.json
中, 有 dependencies
和 devDependencies
的配置,顾名思义, dependencies
中放的是项目必不可少的依赖包,比如后端是 koa 的项目,那么如果别人想运行你的项目就必须安装 koa
,而 devDependencies
放的是使用者无需安装的依赖包,比如 mocha,eslint 之类的依赖,那么问题来了,在一个前后端分离的项目中,如何合理分配依赖包的 Key 。
现在大概有两种做法:
devDependencies
中,后端的放到 dependencies
中,在服务器上使用 npm install --production
安装后端需要的依赖包。前端在开发机上打包之后上传到服务器上。dependencies
中,后端的放到 devDependencies
中,然后使用 npm install --dev
安装。一和二的做法其实也没什么区别,只不过 production
更符合在生产环境使用的语义,身边没什么朋友能讨论这个问题,只能自己瞎试了,在使用的过程中遇到的坑都会尽力记录下来。
我们在写node的时候有可能会遇到循环依赖的情况,什么是循环依赖,怎么避免或解决循环依赖问题?
先看一段官网给出的循环依赖的代码:
a.js
:
console.log('a starting');
exports.done = false;
var b = require('./b.js'); // ---> 1
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done') // ---> 4
b.js
:
console.log('b starting');
exports.done = false;
var a = require('./a.js'); // ---> 2
// console.log(a); ---> {done:false}
console.log('in b, a.done = %j', a.done); // ---> 3
exports.done = true;
console.log('b done');
main.js
:
console.log('main starting');
var a = require('./a.js'); // --> 0
var b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
如果我们启动 main.js
会出现什么情况? 在 a.js
中加载 b.js
,然后在b.js
中加载 a.js
,然后再在 a.js
中加载 b.js
吗?这样就会造成循环依赖死循环。
让我们执行看看:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true
可以看到程序并没有陷入死循环,从上面的执行结果可以看到 main.js
中先require
了 a.js
,a.js
中执行完了console
和export.done=fasle
之后,转而去加载b.js
,待b.js
被load完之后,再返回a.js
中执行完剩下的代码。
我在官网的代码基础上增加了一些注释,基本 load 顺序就是按照这个0-->1-->2-->3-->4
的顺序去执行的,然后在第二步下面我打印出了require('./a')
的结果,可以看到是{done:false}
,可以猜测在b.js
中require('./a')
的结果是a.js
中已经执行到的exports
出的值。
上面所说的还只是基于结果基础上的猜测,没有什么说服力,为了验证我的猜测是正确的,我把 Node 的源码稍微翻看了一些,C++ 的代码看不懂没关系,能看懂 JS 的部分就可以了,下面就是 Node 源码的分析(主要是 module 的分析, Node 源码在此):
将会分析的主要源码:
C++ 的代码我看不懂,总而言之,在我查了资料之后知道当我们在shell
中输入node main.js
之后,会先执行 node/src/node.cc
,然后会执行 node/src/node.js
, 所以C++代码不分析,从分析 node/src/node.js
开始(只会分析和主题相关的代码)。
node.js
文件主要结构为
(function(process) {
this.global = this
function startup() {
...
}
startup()
})
这种闭包代码很常见,从名字可以看出,此处为启动文件。接下来看看 startup 函数中有一大块条件语句,我删除大多数无关代码,如下:
if (process.argv[1]) {
// ...
var Module = NativeModule.require('module');
Module.runMain();
}
我把无关的代码基本都删除了。可以看到这段代码主要做的事是先通过 Native 引入module
模块,执行 Module.runMain()
。
很多人都知道 require
核心代码,如 require('path'),不需要写全路径,Node 是怎样做到的呢?
Node 采用了 V8 附带的 js2c.py 工具,将所有内置的 JavasSript 代码( src/node.js 和 lib/*.js) 转成 c++ 里面的数组生成 node_navtives.h 头文件。
在这个过程中, JavasSript 以字符串的形式存储在 node 命名空间中, 是不可直接执行的。
在启动 Node 进程时, JavaScript 代码直接加载进内存中。Node 在启动时,会生成一个全局变量 process, 并提供 binding() 方法来协助加载内建模块。
上面大段介绍基本引自朴老师的「深入浅出 Node.js」。大概理解就是在启动命令的时候,Node 会把 node.js
和 lib/*.js
的内容都放到 process
中传入当前闭包中,我们在当前函数就可以通过process.binding('natives')
取出来放到 _source 中,如下代码所示:
function NativeModule(id) {
this.filename = id + '.js';
this.id = id;
this.exports = {};
this.loaded = false;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
接下来看看NativeModule.require
做了哪些事情:
NativeModule.require = function(id) {
if (id == 'native_module') {
return NativeModule;
}
var cached = NativeModule.getCached(id);
if (cached) {
return cached.exports;
}
var nativeModule = new NativeModule(id);
nativeModule.cache();
nativeModule.compile();
return nativeModule.exports;
};
这上面的代码表明内建模块被缓存,就直接返回内建模块的exports
,如果没有的话,就生成一个核心模块的实例,然后先把模块根据id来cache
,然后调用nativeModule.compile
接口编译源文件:
NativeModule.getSource = function(id) {
return NativeModule._source[id];
};
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) {\n',
'\n});'
];
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
var fn = runInThisContext(source, {
filename: this.filename,
lineOffset: -1
});
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
};
NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};
cache 是把实例根据 id 放到 _cache 对象中。先从 _source 中取出对应id的源文件字符串,包上一层(function (exports, require, module, __filename, __dirname) {\n', '\n});
。比如main.js
最终变成如下JS代码的字符串:
(function (exports, require, module, __filename, __dirname) {
// 如果是main.js
console.log('main starting');
var a = require('./a.js'); // --> 0
var b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
})
runInThisContext
是将被包装后的源字符串转成可执行函数,(runInThisContext
来自contextify
模块),runInThisContext
的作用,类似eval
,再执行这个被eval
后的函数,就算被 load 完成了,最后把 load 设为 true。
可以看到fn
的实参为 this.exports; NativeModule.require; this; this.filename;
。
所以require('module')
的作用是加载/lib/module.js
文件。让我们再回到 startup 函数,加载完 module.js,紧接着运行 Module.runMain()
方法。(估计有人忘了前面的startup函数是干嘛的,我再放一次,省得再拉回去了)
if (process.argv[1]) {
// ...
var Module = NativeModule.require('module');
Module.runMain();
}
上面走完了NatvieModule
的加载代码。再看看module.js
是怎样加载用户使用的文件的。
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
module.exports = Module;
Module._cache = {};
Module._pathCache = {};
Module._extensions = {};
var modulePaths = [];
Module.globalPaths = [];
Module.wrapper = NativeModule.wrapper;
Module.wrap = NativeModule.wrap;
这是Module
的构造函数,Module.wrapper
和Module.wrap
,是由NativeModule
赋值来的,Module._cache
是个空对象,存放所有被 load 后的模块 id。
在node.js
文件的 startup 函数中,最后一步走到Module.runMain()
:
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};
在runMain
方法中调用了_load
方法:
Module._load = function(request, parent, isMain) {
var filename = Module._resolveFilename(request, parent);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
var module = new Module(filename, parent);
Module._cache[filename] = module;
module.load(filename);
return module.exports;
};
上述代码照例我删除了一些不是很相关的代码,从剩下的代码可以看出_load
函数的主要干了两件事(还有一件加载NativeModule的代码被我删掉了):
Module.prototype.load = function(filename) {
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
};
在load
方法中判断源文件的扩展名是什么,默认是'.js'
,(我这里也只分析后缀是 .js
的情况),然后调用 Module._extensions[extension]()
方法,并传入 this 和 filename;当extension
是'.js'
的时候, 调用Module._extensions['.js']()
方法。
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
这个方法是读到源文件的字符串后,调用module._compile
方法。
Module.prototype._compile = function(content, filename) {
var self = this;
function require(path) {
return self.require(path);
}
var dirname = path.dirname(filename);
// create wrapper function
var wrapper = Module.wrap(content);
var compiledWrapper = runInThisContext(wrapper,
{ filename: filename, lineOffset: -1 });
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
};
其实跟NativeModule
的_complie
做的事情差不多。先把源文件content
包装一层(function (exports, require, module, __filename, __dirname) {\n', '\n});
, 然后通过 runInThisContext
把字符串转成可执行的函数,最后把
self.exports, require, self, filename, dirname
这几个实参传入可执行函数中。
require
方法为:
Module.prototype.require = function(path) {
return Module._load(path, this);
};
所谓的循环依赖就是在两个不同的文件中互相应用了对方。假设按照最上面官网给出的例子中,
在 main.js
中:
require('./a.js')
;此时会调用 self.require()
,module._load
,在_load
中会判断./a.js
是否被load过,当然运行到这里,./a.js
还没被 load 过,所以会走完整个load流程,直到_compile
。./a.js
,运行到 exports.done = false
的时候,给 esports 增加了一个属性。此时的 exports={done: false}
。require('./b.js')
,同 第 1 步。./b.js
,到require('./a.js')
。此时走到_load
函数的时候发现./a.js
已经被load过了,所以会直接从_cache
中返回。所以此时./a.js
还没有运行完,exports = {done.false}
,那么返回的结果就是 in b, a.done = false
;./b.js
全部运行完毕,回到./a.js
中,继续向下运行,此时的./b.js
的 exports={done:true}
, 结果自然是in main, a.done=true, b.done=true
let vdom = {
tag: 'div',
props: {
style: 'display:none'
},
children: [{
tag: 'a',
props: {
id: '1',
href: 'http://test.com',
target: '_blank'
},
children: ["click!"]
}, 'this is text!!!', {
tag: 'div',
props: {
class: 'class1 class2',
'data-attr': 'hello'
},
children: ['haha', {
tag: 'br'
}]
}, {
tag: 'br'
}]
}
function render(root) {
function dfs(node) {
let element
if (node.tag) {
element = createElement(node.tag, node.props)
node.children && node.children.map(dfs).forEach(element.appendChild)
} else if (typeof node === 'string')
element = createTextNode(node)
return element
}
return dfs(root)
}
function createElement(tag, props = {}) {
const element = document.createElement(tag)
Object.keys(props).forEach((key) => { element.setAttribute(key, props[key]) })
return element
}
function createTextNode(text) {
return document.createTextNode(text)
}
console.log(render(vdom))
运行结果如下:
Node.js中最常用的恐怕就是 require, exports 以及 module.exports 了,那么 exports 和 module.exports 这两者有什么区别,在什么情况下使用 exports,又在什么时候使用 module.exports。
先举个官网的例子:
// circle.js
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
在 circle.js 中写的源字符串是上面贴出的代码,然而实际 Node.js 在加载的时候会在原字符串上外面拼出一个闭包,拼出之后的代码如下(有想了解为什么会拼出这个代码的朋友,请看我之前一篇文章):
(function(exports, require, module, __dirname, __filename) {
// circle.js
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
})
Node.js 调用这段代码的为:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
Module.prototype._compile = function(content, filename) {
var self = this;
function require(path) {
return self.require(path);
}
var dirname = path.dirname(filename);
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
};
从上面这段代码可以看到 exports 是 module 的一个属性,exports 值为 {}。在拼接之后的代码中,给这个函数传入的 exports 是 module.exports, 也就是说 exports 和 modules.exports 引用的是同一个对象。如果我们给 exports 增加属性,那么因为 modules.exports 也会增加相同的属性,此时 modules.exports === exports。然而如果对 exports 赋值的话,那么就会造成 modules.exports 和 exports 不指向同一个对象,此时再对 exports 做任何动作都跟 modules.exports 没有任何关系了,用一段代码模拟就是:
var module = { exports: {}};
var exports = module.exports;
exports.a = 1;
console.log(module.exports); // {a: 1}
exports = {b:2};
console.log(module.exports); // {a: 1}
所以从上面的代码可以看出,如果我们想在模块文件中提供其他模块可以访问的接口,最好写成
exports["key"] = value 或者 module.exports = {key: value} 的形式。
<button id="update">update</button>
<div id="root"></div>
let vdom1 = {
tag: 'div',
props: {
style: 'border: solid 2px red;'
},
children: [{
tag: 'a',
props: {
id: '1',
href: 'http://test.com',
target: '_blank'
},
children: [{ text: 'click!' }]
}, {
text: 'text!!'
}, {
tag: 'div',
props: {
class: 'class1 class2',
'data-attr': 'hello'
},
children: [{ text: 'hahaha!' }, {
tag: 'br'
}]
}, {
tag: 'br'
}]
}
let vdom2 = {
tag: 'div',
props: {
class: 'vdom2'
},
children: [{
tag: 'a',
props: {
id: '1',
href: 'http://test.com',
target: '_blank'
},
children: [{ text: 'click!!!' }]
}, {
tag: 'h2',
children: [{ text: 'hahahah!' }]
}, {
tag: 'div',
props: {
class: 'class1 class2',
'data-attr': 'hello'
},
children: [{ text: 'xxixii!' }, {
tag: 'br'
}]
}, {
tag: 'br'
}]
}
let prev
function render(container, root) {
function dfs(node, prev = {}) {
let element
if (node.tag) {
if (prev && prev.tag === node.tag) {
element = prev.raw
updateProps(element, node.props, prev.props)
} else {
element = createElement(node.tag, node.props)
}
node.children && renderChildren(node.children, prev.children, element)
} else if (typeof node.text === 'string') {
element = createTextNode(node.text)
}
node.raw = element
return node
}
function renderChildren(children = [], prevChildren = [], parentElement) {
children.forEach((child, index) => {
let prevChild = null
if (prevChildren) prevChild = prevChildren[index]
if (prevChild) {
let newNode = dfs(child, prevChild)
if (prevChild.tag !== child.tag) {
parentElement.replaceChild(newNode.raw, prevChild.raw)
}
} else {
let newNode = dfs(child)
parentElement.appendChild(newNode.raw)
}
})
prevChildren.slice(children.length).forEach((child) => {
parentElement.removeChild(child.raw)
})
}
dfs(root, prev)
if (!prev) {
prev = root
container.appendChild(prev.raw)
}
}
function createElement(tag, props = {}) {
const element = document.createElement(tag)
updateProps(element, props)
return element
}
function updateProps(element, props = {}, oldProps = {}) {
Object.keys(oldProps).forEach((key) => { element.removeAttribute(key) })
Object.keys(props).forEach((key) => { element.setAttribute(key, props[key]) })
}
function createTextNode(text) {
return document.createTextNode(text)
}
let fake1 = render(root, vdom1)
update.addEventListener('click', () => {
fake2 = render(root, vdom2)
})
最近在服务器上使用 pm2 来管理 node 程序,当然其他程序也可以用 pm2 管理,具体使用方法可以看 这里,(可能需要翻墙)。
pm2 有个 watch file 功能,开启方法 :
$: pm2 start app.js --watch
检查程序中有没有文件被修改,如果被修改的话,重启服务器;我在使用过程中遇到一个很奇怪的bug:每当用户登录的时候,服务器内存就会暴涨,且在服务端设置的 cookie 也会过了很久才被浏览器接收到。
后来经过一番苦逼的排查才发现原来是我在用户登陆的时候会在一个文件中记录用户的登录日志,这个时候 pm2 就会检测到项目目录下有文件变动,然后就是无情地重启服务器... 排查出问题就好解决了, pm2 有个 ignoreWatch
的配置,设置好忽略监测目录。
TLDR;
在远程主机中运行命令的 shell 语法:$: ssh user@host "commands"
最近的一个项目中,我们前端需要自己发布静态资源到远程目标服务器上,中间需要经过一个跳板机(为什么不用公司内部的 GitLab 方式发布,原因很多,这里就不表述了)。前端如果手动发布的话需要经过以下步骤:
从上面的步骤可以看出如果手动发布一个服务器,大概需要经过7个步骤,每次发布都需要至少 4-5 分钟时间等待(我们有 12 台需要发布的机器,每次发布都需要 4(从第 3 步开始重复 12 次) * 12 分钟),而且如果网络不太好的话,等待的时间就更长了;更不要说手动发布的时候会经常忘了步骤且更容易出错了,每次发布都好像回到了原始时代。
万幸的是 ssh 本身支持在远程主机中运行命令的,语法就是
$: ssh user@host "command1; command2; command3; ...."
此处的 command 应该用 ""
包起来,避免 ;
被本地主机当做定界符处理。 ok,知道 ssh 的用法,我们写个 bash 处理一下上面 7 个步骤:
# 打压缩包并上传到跳板机的指定目录
function compress() {
tar -zcvf sc.tgz -C build . && scp -r sc.tgz [email protected]:~/oss_download/demo/
}
# 通过 ssh 登录跳板机执行 3 - 7 步
# 注意在跳板机跳登录到目标服务器的时候需要 ssh -tt ,可以在远程机器上 ssh 到其他的远程主机并执行。详细的解释通过 man ssh 查看
# mkidr -p 是如果目标机器不存在这个目录,就先创建这个目录,保证 cd 或者 tar 的时候不会因为目标目录不存在而引起报错
function send() {
ssh [email protected] "scp -r oss_download/demo/sc.tgz alibaba@${1}:~/ossdowload/data/demo/ ; ssh -tt alibaba@${1} 'mkdir -p ossdowload; cd ossdowload; mkdir -p data/demo/sc/${version}; tar -zvxf data/demo/sc.tgz -C data/demo/sc/${version} && rm -rf data/demo/sc.tgz && ./ceph_tmp.py'"
}
# 执行 compress 和 send 函数
function deploy() {
compress
send ${1}
}
# 发送到目标服务器
deploy yyy.yyy.yyy.yy
这是发布到一个远程主机的命令的脚本,是不是很简单?之后如果想发布到多个远程主机,只要通过简单的 while 语句就可以搞定;到这里就算结束了,大多数时候前端只需要掌握简单的 bash 语法,就可以通过自动化脚本节省了大量的时间去泡妹子,好开心。
用 node 来学习「Algorithms(第四版)」的时候,经常遇到逐行读取文件的需求,索性用 node 自带的readLine Api 写个逐行读取文件的程序。
import {createReadStream} from 'fs'
import {createInterface} from 'readline'
let readFile = (inputFile, cb) => {
let inStream = createReadStream(inputFile, 'utf8')
, rl = createInterface({input: inStream})
rl.on('line', function (line) {
rl.pause()
//sync
cb(line)
rl.resume()
})
}
export default readFile
用法比较简单
import {join} from 'path'
import readFile from './readFile'
readFile(join(__dirname, './path.txt'), (line)=> {
console.log(line)
})
有两个月没有去 codewars 刷过题,最近总收到 codewars 的邮件说 codewars 已经支持 es6 了,下午登录 codewars 发现还差几分就可以升到 3 阶,于是花了一下午的时间做了最后一道 4 阶题,顺利升了阶,撒花!🎉🎉
下面就是我的最后一道四阶题:
Description:
You have to create a function that takes a positive integer number and returns the next bigger number formed by the same digits:
nextBigger(12)==21
nextBigger(513)==531
nextBigger(2017)==2071
If no bigger number can be composed using those digits, return -1:
nextBigger(9)==-1
nextBigger(111)==-1
nextBigger(531)==-1
我的解法如下:
var exch = (a, k) => {
return [a[k], ...a.slice(0,k),...a.slice(k+1)]
}
var nextBigger = (n) => {
if(n < 11) return -1
var a = Array.from(`${n}`).map(v=>v|0)
, i = a.length
while(--i) {
if(a[i] > a[i-1]) {
var c = a.slice(i-1).sort((a,b) => (a|0) > (b|0))
, k = c.indexOf(a[i])
for(var j = 1; j < c.length; j++) {
if(c[j] > a[i-1] && c[j] < c[k])
k = j
}
return Number([...a.slice(0, i-1), ...exch(c, k)].join(''))
}
}
return -1
}
经常遇到「既然GET请求可以做POST请求的事情,为什么还要区分GET和POST而不只使用一个请求?」的问题。作为在实际中被使用最广的两个请求方法,这个问题其实挺难回答的,但万物总有其根由,今天就追根究底。
查看RFC规范再加上之前查过的一些二手文章,整理了如下的观点:
0. GET 被强制服务器支持
All general-purpose servers MUST support the methods GET and HEAD. All other methods are OPTIONAL.
GET 通常用于请求服务器发送某个资源。在HTTP/1.1中,要求服务器实现此方法;POST请求方法起初是用来向服务器输入数据的。在HTTP/1.1中,POST方法是可选被实现的,没有明确规定要求服务器实现。
RFC 2616 (Hypertext Transfer Protocol — HTTP/1.1) states in section 3.2.1 that there is no limit to the length of an URI (URI is the official term for what most people call a URL)
RFC 2616 中明确对 uri 的长度并没有限制。不过虽然在RFC中并没有对uri的长度进行限制,但是各大浏览器厂家在实现的时候限制了URL的长度,可查到的是IE对长度限制为2083;而chrome遇到长度很长的URL时,会直接崩溃。
所以这条结论算是正确的。
只能通过写代码验证了:下面第一个文件是服务器代码,作用是在客户端发送GET和POST请求的时候返回200状态码。第二个文件是客户端HTML文件,点击两个button,分别发送GET请求和POST请求。
import koa from 'koa'
import fs from 'mz/fs'
const app = koa()
app.use(function* (next) {
if(this.path == '/test')
return this.status = 200
yield next
})
app.use(function* (next) {
this.type = 'html'
this.body = yield fs.readFile('./index.html')
yield next
})
app.listen(8080)
console.log('koa server port: 8080')
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="get">GET</button>
<button id="post">POST</button>
</body>
<script>
function http(type) {
return function (url) {
var req = new XMLHttpRequest()
req.open(type, url)
req.send()
}
}
var getDom = document.getElementById('get')
, postDom = document.getElementById('post')
, get = http('GET')
, post = http('POST')
getDom.addEventListener('click', function () {
get('/test')
})
postDom.addEventListener('click', function () {
post('/test')
})
</script>
</html>
从上两张图可以看到POST请求的headers要比GET请求多了两个属性。所以这条结论其实也算是对的,不过从请求发送时间来看的话,其实两者并没有差别。
Of the request methods defined by this specification, the GET, HEAD,
OPTIONS, and TRACE methods are defined to be safe.
A request method is considered "idempotent" if the intended effect on
the server of multiple identical requests with that method is the
same as the effect for a single such request. Of the request methods
defined by this specification, PUT, DELETE, and safe request methods
are idempotent.
从上面可以看到GET请求是安全的,在幂等性中说PUT和DELETE以及安全method都是幂等的,所以GET自然也被包括了。
我们在实际使用过程中对HTTP请求的优化大多数都放在GET请求上,比如对没有数据变化的请求(网站中常用的静态文件)做缓存,在潜意识中认为只有GET请求才可以被缓存,所以从来也不会考虑POST请求的缓存优化,然而在RFC中提到GET和POST以及HEAD都是可以被缓存的。不过不要惊讶,之前不知道POST可以被缓存是因为标准也很无奈,浏览器的实现总是比标准厉害。
In general, safe methods thatdo not depend on a current or authoritative response are defined as cacheable; this specification defines GET, HEAD, and POST as cacheable, although the overwhelming majority of cache implementations only support GET and HEAD.
这一点很多人都会质疑,被抓包之后的POST请求和GET请求是一样裸露的,所以更安全的说法是不对的。我这里所有的「安全」是相对的,因为GET请求有时候会直接反应在浏览器的地址栏,而现在的浏览器大多会记住曾经输入过的URL。试想如果你曾经在别人电脑上填过一个很私密的表单,那么你的这份记录很可能被连没什么电脑常识的人都一览无遗。
let vdom = {
tag: 'div',
props: {
style: 'display:none'
},
children: [{
tag: 'a',
props: {
id: '1',
href: 'http://test.com',
target: '_blank'
},
children: ["click!"]
}, 'this is text!!!', {
tag: 'div',
props: {
class: 'class1 class2',
'data-attr': 'hello'
},
children: ['haha', {
tag: 'br'
}]
}, {
tag: 'br'
}]
}
function render(root) {
function dfs(node, space = '') {
let str = ''
if (node.children)
str = (`<${node.tag}${renderProps(node.props)}>\n${node.children.map(_ => dfs(_, ' ') + '\n').join('')}</${node.tag}>`)
else if (typeof node === 'string')
str = node
else
str = `<${node.tag}${renderProps(node.props)}/>`
return str.split('\n').map(_ => space + _).join('\n')
}
return dfs(root)
}
function renderProps(props) {
if (!props) return ''
return Object.keys(props).reduce((str, key) => `${str} ${key}="${props[key]}"`, '')
}
console.log(render(vdom))
<div style="display:none">
<a id="1" href="http://test.com" target="_blank">
click!
</a>
this is text!!!
<div class="class1 class2" data-attr="hello">
haha
<br/>
</div>
<br/>
</div>
今天一个初学者问我怎样在老的浏览器下实现通用的事件绑定函数。这个问题挺简单,我随手写了如下的函数:
var addEvent = function(el, type, handler) {
if(window.addEventListener) {
return el.addEventListener( type, handler, false)
} else if(window.attachEvent) {
return el.attachEvent('on' + type, handler)
}
}
但是这个函数写完之后我就感觉到有些问题,那就是每次调用的时候都要对浏览器进行嗅探,即执行 if 判断。虽然执行 if 不会对计算机造成太大开销,但是作为一个有追求的程序员,怎会放弃如此可以优化的机会。
所以回到问题的原点,为什么要对浏览器进行嗅探?因为不同浏览器有不同的兼容性。那么我们只要嗅探一次且要保证在没有调用过这个函数的情况下不进行嗅探可不可以?当然可以!下面的代码就是利用惰性加载函数的方法:
var addEvent = function(el, type, handler) {
if(window.addEventListener) {
addEvent = function(el, type, handler) {
el.addEventListener( type, handler, false)
}
} else if(window.attachEvent) {
addEvent = function(el, type, handler) {
el.attachEvent('on' + type, handler)
}
}
addEvent.apply(null, arguments)
}
第一次调用 addEvent 的时候会对浏览器进行嗅探,然后把嗅探之后的结果再重新替换掉 addEvent。早期的 Redux 的 applymiddleware 实现也是使用这个方法。
我之前开发网站的时候一直用的是 Flux, 自从出了 Redux 之后由于种种原因没有跟进了解,最近手头上的事情基本忙的差不多了,抽空阅读了 Redux 的源码,并整理了这篇博文。
先说重点: Redux 与 React 没有关系,就好像 Javascript 和 Java ,雷锋和雷峰塔的关系一样。 Redux 旨在处理数据的流动。
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。 Redux 是由 Flux 演变而来。
那么 Flux 是什么? Flux 在这里并不是一个框架,而是提供了一套数据流动的方案,类似 MVVM 的概念。
Redux 是一个状态容器,这句话挺难理解的,下面的分析也是我的个人见解,不见得正确,欢迎指正。
我们知道状态机是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
例子:
有一个提供简单加减计算的二则运算的算法。初始值为0,可以增加一个值和减去一个值。
可以如下自己实现一个状态机:
const fsm = {
currentState: 0,
create(state) { this.currentState = state },
getState() {
return this. currentState
},
transition(action) {
switch(action.type) {
case 'add':
this.currentState = this.currentState + action.num
break
case 'sub':
this.currentState = this.currentState - action.num
break
default:
break
}
}
}
fsm.create(5)
fsm.transition({'type':'add', 'num':-1})
console.log(fsm.getState()) // ==> 4
fsm.transition({'type':'sub', 'num':1})
console.log(fsm.getState()) // ==> 3
从上面的例子可以看到状态的改变方式为:输入初始状态值5,此时的 currentState 为0,输入{'type':'add', 'num': 1}
,经过条件判断是要将状态值 5 加 -1 变成 4,再输入{'type':'sub', 'num': 1}
,经过条件判断是要将状态值 4 减 1 变成 3。
对比 Redux 来看的话, 我们的 fsm 就是 Redux 的 createStore 返回的 store,store.getState() 返回的状态对应 fsm.getState()。 那么 reducer + dispatch + action 对应的就是 fsm.transition()。之后会我们分析源码看看 Redux 是怎样把 reducer + dispatch + action 转成 fsm.transition。
对于 Redux 来说,就是把数据当成状态来处理,reducer 就是根据行为(action) 将当前数据(状态)转成新的状态,新的数据状态可以继续被 reducer 处理。
Action 是把数据从应用传到 stateTree(状态树)的输入动作(payloads)。按照约定来说 action 是一个带有 type 属性的 javascript plain object
,对应着 Flux 中的 payload。
Action Creator 是一个创建 Action 的函数,额,其实就是函数式编程搞出的概念,把一个表达式包装成一个函数,返回这个 Action。
对照上面我们自己写的状态机代码可以看出 action 的作用告诉 statetree (状态树)发生什么变化,及所需要的数据是什么。
Reducer 的是根据 action 来决定数据应该变化成什么样子的函数,即将上面 fsm 中的switch case 表达式包装而成的函数。
dispatch 是更新状态树的方法,在 dispatch 中会调用 reducer, 且通知监听者数据已发生变化。
从上面的分析应该可以推断出 Redux 暴露的 dispatch 会接受一个 action,来决定根据 reducer 去转换状态树,那么也可以推断出 Redux 一定也需要提供一个接受 reducer 函数的API。
Redux 提供的 createStore(reducers, initialState)
API 确实如我们推断,会在此时传入 reducer,以及一个可选的初始状态。 createStore
返回的是一个 store
, store 和状态树是不同的,此处的store具有dispatch(action)
方法的对象,真正的状态树是 store.getState()(也就是我们真正要使用的数据)。
export default function createStore(reducer, initialState) {
// 在调用 createStore 的时候,必须传入 reducer, 且 reducer 必须为函数
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
var currentReducer = reducer
var currentState = initialState
var listeners = []
var isDispatching = false
// 返回此时的状态
function getState() {
return currentState
}
// 订阅函数,调用 dispatch 的时候会调用 listener
function subscribe(listener) {
// ...
}
// 发布函数, 在 action 触发状态的改变后,通知所有订阅的 listener
function dispatch(action) {
// 传入的 action 必须为 plain object,也就是 action creator 返回的对象
// 自己传入 action 对象也是可以的
// 但是 Redux 推荐的写法是 action creator 的写法
// 至于写成函数的好处不在这里讨论
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// 强制要求 action 必须带入 type 属性,比 Flux 有更强的约束
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
// 这里就是把 action 和当前状态经过 reducer 处理之后返回一个新的状态
// currentReducer 就是 createStore 传进来的 reducer
// 可以切回去看看上面我总结的图
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 通知订阅的事件
listeners.slice().forEach(listener => listener())
return action
}
// 状态初始话,此时的 Action 为 { type: ActionTypes.INIT }
dispatch({ type: ActionTypes.INIT })
// createStore 最后返回一个含有 dispatch 和 getState 的对象
return {
dispatch,
subscribe,
getState,
replaceReducer
}
}
Redux 就像是作者自己的介绍,是一个 JavasSript 的状态容器,所有的数据(状态)的变化都是当前状态和 Action 共同的作用结果。 对于使用者(一般都是指 view)来说,不用关心数据是怎样变化,只需要在 view 层面等待 store 通知自己数据发生变化,然后把数据渲染成页面即可。
这里没有提到 Redux 的另一个比较重要也比较难理解的 Middleware。因为如果在这里说的的话,文章不知道要写多长,而长文我现在也驾驭不住,所以干脆就不写了,后面我会再补一篇理解 Middleware 的文章。
其实我对 Redux 的这种实现状态的方式并不太喜欢,相对来说 javascript-state-machine 看起来更舒服一些,不知道和 Redux 结合有什么效果。可能画面太美,我不敢想😄。
原文 @github
作者 @zwhu
TLDR;
这篇文章的风格是在致敬 Jim 老师;致敬,致敬,懂吗,不是抄袭,程序员的事怎么能叫抄袭。
当然我对 Node.js 的 stream 也是现学现卖,有使用不当的地方,敬请指出。
原文链接 欢迎 star。
写这篇文章的初衷是年前看 SICP 的时候,第二章介绍构造数据抽象的时候有提到 Lisp 对序列的处理采用类似『信号流』的方式。所以很自然的就想到了 Node.js 中的 pipe 方式,于是就一直想用 pipe 的方式尝试一下。
同 Jim 老师的这篇 文章 中描述的一样, 我也是懒癌发作,从年尾拖到今年年初,然后在年初又看到了 Jim 老师 的博客,深受启发,终于下定决心要开始码了...... 然后,嗯,又拖到昨天。促使我下定决心要写的主要原因是昨天部门的年会!反正年会跟我这种死肥宅也没多大关系,在大家 happy 的时候构思了下代码实现,回家用了一晚上的时候补上了代码。
Jim 老师在他的文章里面也说了,JS 的那些数组操作 (map
/ reduce
/filter
) 啥的,每次调用的时候都会进行一次完整的遍历。试想一下如果有一个第一个数是1,长度是 1亿 的递增为 1 的数组,需要把所有的数组都乘 3,再排除其中的奇数,如果用 (map
/filter
) 的方法,只要也需要循环 一亿五千万次;那么如果有其他办法能只循环一亿次,是不是节省了大量的内存资源和循环消耗的时间。
废话不多说,直接上代码吧。
在编写代码时,我们应该有一些方法将程序像连接水管一样连接起来 -- 当我们需要获取一些数据时,可以去通过"拧"其他的部分来达到目的。这也应该是IO应有的方式。 -- Doug McIlroy. October 11, 1964
关于 node 的 stream 可以看看这篇 文章。
下面是代码部分,整个代码我是在边学 pipe 边用一晚上的时间仓促写就的,懒癌发作,也不想再重构了,各位相公讲究看吧,求别喷代码。
const stream = require('stream')
const last = Symbol()
// 在 selfArray 中接收一个真正的数组
// 返回一个可读流
// 如果再做的精细点,可以做成可读可写流,这样就能通过控制流的大小,来控制内存的大小,别几亿条数据直接撑爆内存了
// 不过对后面 reduce 的处理就比较麻烦
function selfArray(a) {
const rs = new stream.Readable({
objectMode: true
})
a.forEach((v, index) => {
rs.push(v)
})
rs.push(last)
rs.push(null)
return rs
}
上面的 selfArray 在流的最后面 push 了一个 Symbol 对象来标志整个流的输入结束,留待为之后 reduce 的使用。
Map
/Filter
/Reduce
的实现function forEach(callback) {
const ws = new stream.Writable({
objectMode: true
})
let index = 0
ws._write = function (chunk, enc, next) {
if (chunk !== last) {
callback(chunk, index++)
next()
}
}
return ws
}
function filter(callback) {
const trans = new stream.Transform({
readableObjectMode: true,
writableObjectMode: true
})
let index = 0
trans._transform = function (chunk, enc, next) {
if (chunk === last) {
next(null, last)
} else {
let condition = callback(chunk, index++)
if (condition) {
this.push(chunk)
}
next()
}
}
return trans
}
function map(callback) {
const trans = new stream.Transform({
readableObjectMode: true,
writableObjectMode: true
})
let index = 0
trans._transform = function (chunk, enc, next) {
if (chunk === last) {
next(null, last)
} else {
next(null, callback(chunk, index++))
}
}
return trans
}
function reduce(callback, initial) {
const trans = new stream.Transform({
readableObjectMode: true,
writableObjectMode: true
})
let index = 0,
current = initial,
prev = initial
trans._transform = function (chunk, enc, next) {
if (chunk === last) {
if (index > 1) {
prev = callback(prev, current, index - 1)
}
this.push(prev)
this.push(last)
return next(null, last)
}
if (initial === void 0 && index === 0) {
prev = chunk
}
if (index > 0) {
prev = callback(prev, current, index - 1)
}
current = chunk
index++
next()
}
return trans
}
上面的代码在 reduce 的实现稍微麻烦了一些,reduce 对没有初始值,原始数组为空的条件下有各种不同的处理情况,翻看了下 MDN 的解释又自己实现了下。
selfArray([9, 2, 6, 3, 5, 6, 7, 1, 4, 4])
.pipe(map(v => v * 3))
.pipe(filter(v => v % 2))
.pipe(reduce((p, c) => p + c, 0))
.pipe(forEach(v => {
console.log('pipe 计算最后的结果是:', v)
}))
为了好看我故意把各种括号都删掉了。嗯,看起来还挺完美,我们来测试下
selfArray([9, 2, 6, 3, 5, 6, 7, 1, 4, 4])
.pipe(map(v => {
console.log('map:', v)
return v * 3
}))
.pipe(filter(v => {
console.log('filter:', v)
return v % 2
}))
.pipe(reduce((p, c) => {
console.log('reduce:', p, c)
return p + c
}, 0))
.pipe(forEach(v => {
console.log('pipe 计算最后的结果是:', v)
}))
加上 log 之后可以看到结算结果是:
map: 9
filter: 27
map: 2
filter: 6
map: 6
filter: 18
map: 3
filter: 9
reduce: 0 27
map: 5
filter: 15
reduce: 27 9
map: 6
filter: 18
map: 7
filter: 21
reduce: 36 15
map: 1
filter: 3
reduce: 51 21
map: 4
filter: 12
map: 4
filter: 12
reduce: 72 3
pipe 计算最后的结果是: 75
从上面的 log 可以看到, 第一个数 9 先执行了 map
,然后在 * 3 之后就直接进入了 filter
,此时第 2 个数 2 也开始被 map
处理,然后被 filter
处理,但是由于 * 3 之后是偶数不会被 reduce
接收, reduce
会一直等到第二个奇数,也就是 3 进入之后才会被处理... 嗯,直到最终的计算结果是 75, 被 forEach
消耗。
虽然我没有像 Jim 老师一样进行性能测试,但是猜测也知道 pipe 的方式在数量比较小的时候肯定要弱于正常方式,pipe 的好处在于数据量比较大的时候,可以使用比较小的内存,尽快的处理数组中前置的数据。
TLDR;
- 介绍 Git 钩子的基本开发流程
- 介绍如何用 Node.js 写 Git 钩子
Git 钩子是指在特定的 Git 动作(如:git commit
、 git push
)下被触发的脚本。而钩子主要被分为两种:
而客户端钩子又被分为以下几种:
类型 | 钩子名称 | 接收参数 | 可否终止操作 |
---|---|---|---|
提交工作流钩子 | pre-commit | \ | 是 |
提交工作流钩子 | prepare-commit-msg | filepath、committype、sha-1 | \ |
提交工作流钩子 | commit-msg | filepath | 是 |
提交工作流钩子 | post-commit | \ | \ |
电子邮件工作流钩子 | applypatch-msg | merge-filename | 是 |
电子邮件工作流钩子 | pre-applypatch | \ | 是 |
电子邮件工作流钩子 | post-commit | \ | 否 |
其它客户端钩子 | pre-rebase | | 是 | |
其它客户端钩子 | post-rewrite、post-checkout 和 post-merge | commandname | | |
其它客户端钩子 | pre-push | originbranhname & head | 是 |
服务器端钩子主要有三种:
钩子名称 | 接收参数 | 可否终止操作 |
---|---|---|
pre-receive | 推送的引用 | 是 |
update | 引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值 | 是 |
post-receive | 同pre-receive | 否 |
Git 的钩子不管客户端钩子还是服务端钩子,都是放在当前项目的
.git/hooks
目录下。不同的是,客户端钩子是放置在你的本地项目的目录下,而服务器端钩子是放在对应的服务器上的目录。
我们知道 Git 相当于本地的文件数据库,而 .git
目录存放了项目文件的快照以及其他一系列 git 信息,且 .git
目录是不会被提交到服务器上的,所以放置在 .git/hooks
目录中的客户端脚本也不会被提交。所以如果想让项目中的其他人使用你的钩子,就需要一种策略来偷偷的安装这个钩子或者在服务端放置实现这个钩子的功能。
钩子都被存储在 Git 目录下的 hooks 子目录中。 也即绝大部分项目中的 .git/hooks 。 当你用 git init 初始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本。这些脚本除了本身可以被调用外,它们还透露了被触发时所传入的参数。 所有的示例都是 shell 脚本,其中一些还混杂了 Perl 代码,不过,任何正确命名的可执行脚本都可以正常使用 —— 你可以用 Ruby 或 Python,或其它语言编写它们。 这些示例的名字都是以 .sample 结尾,如果你想启用它们,得先移除这个后缀。
把一个正确命名且可执行的文件放入 Git 目录下的 hooks 子目录中,即可激活该钩子脚本。
之后我会用 Node.js 来写一个拒绝提交没有被解决的冲突的文件
的钩子。
需要的知识储备:
写这个钩子的初衷是因为在多人合作项目中,总是难免会遇到文件冲突的情况,而有些同事没有找到全部的冲突文件并一一解决,这个钩子就会在 commit 的时候检查是否有冲突,如果有冲突,就会把所有冲突找到,并提示出错文件后,拒绝 commit。
直接上源码:
#!/usr/bin/env node
// 在 commit 之前检查是否有冲突,如果有冲突就 process.exit(1)
const execSync = require('child_process').execSync
// git 对所有冲突的地方都会生成下面这种格式的信息,所以写个检测冲突文件的正则
const isConflictRegular = "^<<<<<<<\\s|^=======$|^>>>>>>>\\s"
let results
try {
// git grep 命令会执行 perl 的正则匹配所有满足冲突条件的文件
results = execSync(`git grep -n -P "${isConflictRegular}"`, {encoding: 'utf-8'})
} catch (e) {
console.log('没有发现冲突,等待 commit')
process.exit(0)
}
if(results) {
console.error('发现冲突,请解决后再提交,冲突文件:')
console.error(results.trim())
process.exit(1)
}
process.exit(0)
把这个文件拷贝到 .git/hooks/pre-commit
下,并执行 chmod 777 pre-commit
就可以在每次 commit 的情况下检查之前文件是否有冲突。
试想一下,我们在写钩子的时候,并不会一次就把代码写对,所以需要经常把这个文件拷贝到 .git/hooks
目录下;有没有更好的做法? 有的。只需要在.git/hooks
下面创建一个 shell 脚本,来调用这个 js 文件即可。
#!/usr/bin/env node
const execSync = require('child_process').execSync
execSync("./pre-commit.js" )
这中 shebang
写法在使用 git 的命令来运行的时候是没有问题的,但是在使用 Source Tree
的 Git-GUI,会报 node 命令不存在, 这是新版本的 osx 的安全策略造成的(可以运行 which node 命令看看和上面的 shebang 有什么区别),对于这种情况使用下面的脚本可以完美解决。
#!/usr/bin/env bash
# 支持 sourcetree
export PATH=/usr/local/bin:$PATH
node "./pre-commit.js"
NOTE:
注意 node './pre-commit.js'
这个路径,是指如果在当前项目的根目录下运行 git commit
,所以 pre-commit.js
是相对于当前根目录的路径。想优化的话可以通过 Git 的一些默认环境变量来配置。
到这里就基本结束了,但是我们再回忆下之前说过的内容『客户端钩子是不会被其他项目成员 clone 下来的』,所以需要一种策略来保证项目中每个成员都安装了这个钩子。由于我们的前端项目是需要每个成员都通过 npm start 命令开启服务的,所以可以在 npm start 中做些手脚。
const fs = require('fs');
// 判断是否已经存在 pre-commit,不存在就读取 pre-commit.sh 并写入
if (!fs.existsSync('.git/hooks/pre-commit')) {
if(!fs.existsSync('.git/hooks/')) {
fs.mkdirSync('.git/hooks/');
}
let preCommitFile = fs.readFileSync('./pre-commit.sh');
fs.writeFileSync('.git/hooks/pre-commit', preCommitFile, {
encoding: 'utf8',
mode: 0o777
});
}
凡是能被 JS 重写的项目,最终一定会被 JS 重写。
花了一个周末的时间,把 css-postion 的标准读了一遍,把关于 Absolute 和 Relative 的部分的理解在这里记录一下。
在 css 中,有三种定位方案:
Normal flow 是最常见的 BFC 和 IFC,也就是常说的块级元素从上到下,内联元素从左到右布局的情况;Floats 即常见的盒子水平布局的情况;Absolute positioning 让盒子完全脱离 Normal flow, 通过设置 top,left 等属性来决定 position。
注意:这里只说到 Floats 和 Absolute 是 out-of-flow
的,没有说 Relative。我看过很多文章都说 Relative 也让盒子脱离了流,是错误的说法(我就深受其害)。
一个元素的盒子的位置和大小通常是由一个特定的矩形计算出来的,这个特定的矩形就是元素的 containing block
。 对于 static
和 relative
的元素,它的 containing block
同普通盒子,一般是指包含它的最近的父级元素(nearest ancestor
)。对于 fixed(不在这次文章之内,按下不表)和 absolute 有如下的表现:
containing block
被最近的positon
不等于static
的祖先元素创建:
containing block
由盒子的内边距层(padding edge) 创建。containing block
由书写模式的 direction
属性决定。这种情况比较少用,不分析了。position
为 static
以外的值,那么 containing block
就是 initial containing block
。注意:对于上面第三点,initial containing block
并不是指 body
, 我看到很多文章都把这里说成 body
。
相对定位:是相对于自己定位。在 normal flow
中,元素的大小是不变的,通过设置top
等属性,改变元素相对于自身的位置。所以被 relative
的元素,是可能会和其他元素重叠的,但是不会对之后的元素的位置有影响。 一个相对定位的盒子会为后代节点创建一个新的绝对定位的 containing block
。
对于 position: absolute
和 position: fixed
都是指 Absolute positioning
。 本文暂只讨论 position: absolute
。 Absolute positioning
被称为绝对定位。 绝对定位的元素是根据其 containing block
决定的,完全脱离 normal flow
, 对后续的兄弟节点的布局无任何影响。一个绝对定位的盒子会为后代节点创建一个新的绝对定位的 containing block
和为子节点创建一个 normal flow
的containing block
当一个元素的 position 属性被设置了除 static
之外的值, 这个元素的位置可以被 top, right, bottom, left
这四个物理属性决定。注意,在同时设置 left、right
或者 bottom、top
的时候会出现竞争情况。
因为女朋友在学 css,需要我在旁指导 而 css 一直是我的弱项,所以我花了一个周末时间把 css-position 的标准梳理了下,这样指导起来也比较有底气。这篇文章基本都是对下面参考的链接的笔记,我之前学这部分的内容时,都是从网上找的别人的博客看的,有很多和标准出入的地方,这一天也解了不少的疑惑;如果有想学的同学,还请直接看 css-positon 的草案比较靠谱,而且草案中 example 也有不少。
作者 @zwhu
原文章 @github
首先声明,本文所有的代码都是在 ES6 下面运行,ES5需要修改之后才能运行,但是本文没有涉及到太多的ES6新特性,而且由于v8对u修饰符不支持,最后的实现也基本是用ES5的知识写的代码。
最初我只是想记录下正则表达式用unicode的方式来匹配特殊字符,写着写着发现 v8 对 u 修饰符的不支持,又转而去研究怎么转换字符串到utf-16的格式,在研究怎么转换的过程中发现ES5的正则对 unicode 编码单元 > 0x10000 的字符串不支持,再转而去实现了一遍对大于 0x10000 的字符串的转换,特此记录。
之前有遇到过一个实用正则表达式匹配特殊字符的需求,例如一段文本 'ab*cd$你好我也好]\nseg$me*ntfault\nhello,world'
,用户可以选择用 * 或者 $ 来分割字符串。
在javascript中,$
和 *
都是预定义的特殊字符,不能直接写在正则表达式中,而需要转义,写成 /\$/
或者/\*/
。
我们需要根据用户的选择来写正则表达式,封装成一个函数就是:
function reg(input) {
return new RegExp(`\\${input}`)
}
这种写法初看上去很美好,将字符都转义之后遇到一些特殊的字符可以匹配,然而现实是残酷的:当用户输入的是n
或者t
这一类的字符的话,返回的正则表达式为/\n/
或者/\t/
,匹配的就是所有的制表符,这就违背了用户的初衷。
通常有一种写法就是把所有需要转义的特殊字符都列出来,然后再逐一匹配,这种写法很耗费精力,而且可能因为没有统计到的特殊字符而出现漏匹配的情况。
这个时候unicode就隆重登场了,在 JavaScript
中,我们也可以用unicode来表示一个字符,例如 'a' 可以写成'\u{61}', '你' 也可以写成 '\u{4f60}'。
关于unicode的介绍大家可以看 Unicode与JavaScript详解
ES5
中提供了 charCodeAt()
方法来返回指定索引处字符的 Unicode 数值,但是 Unicode 编码单元 > 0x10000 的除外,ES2015
中又增加了一个新的方法 codePointAt()
可以返回大于 0x10000 字符串的数值。返回的数值是十进制的,此时我们还需要通过toString(16)
转成16进制。
封装之后的函数如下
function toUnicode(s) {
return `\\u{${s.codePointAt().toString(16)}}`
}
toUnicode('$') -> '\u{24}'
重新封装reg函数为
function reg(input) {
return new RegExp(`${toUnicode(input)}`, 'u')
}
其实写到这里,我希望是对的,但是很不幸,V8 不支持 RegExp 的 u 修饰符。V8支持的话,写到这里就应该结束了,没关系,这里只是提供一种用unicode的方式来转义特殊字符的**。
虽然v8不支持u修饰符,作为一个有追求的码农,当然不能止步于此,我们也可以使用其他方法继续把这个完善
function toUnicode(s) {
var a = `\\u${utf(s.charCodeAt(0).toString(16))}`
if(s.charCodeAt(1))
a = `${a}\\u${utf(s.charCodeAt(1).toString(16))}`
return a
}
function utf(s) {
return Array.from('00').concat(Array.from(s)).slice(-4).join('')
}
// 这里用var而没有用let声明,是因为这些代码直接复制到 chrome 的控制台下就可以看到执行结果
// 测试一下
// toUnicode('a') --> "\u0061"
// toUnitcode('𠮷') --> "\ud842\udfb7"
function reg(input) {
return new RegExp(`${toUnicode(input)}`)
}
// 再测试一下
reg('$').test('$') --> true
把自己对这MVVM设计模式的理解整理并记录,仅作自己以后查询之用。
先说前端为什么需要 MVVM 或者 FLUX。在我看来,是为了保证不那么优秀的前端er在团队中写出不那么垃圾的代码,即使确实十分垃圾,也不会污染到团队中其他同事的代码,其它的设计模式应该也具有这种作用。
MVVM 是 Model-View-ViewModel (双向数据绑定)的简写。不管是 MVVM 或者是 FLUX, 都强调的是视图和数据的分离。MVVM 是将视图和数据分离之后,通过 ViewModel 将数据和视图进行绑定。
View 一般是指模板,例如 Handlerbars ,或者 ES6 的字符串模板,写法如下:
let message = 'hello,world!!'
let demo = `
<div id="demo">
<p>${message}</p>
</div>
`
然后通过 jQuery 插入 body 中
$('body').html(demo)
对于 demo 便是 View
,message 是此 View 指定的 Model。由于在这个例子中并没有交互,仅仅只是为了说明视图和模型, 所以并没有 VM 部分。
下面的例子是一个完整的用 JQuery 实现的 MVVM 模式写法
//------ Model
let model = {
value: 1,
fns: [],
set: function(v) {
this.value = v
this.fns.forEach(fn => fn.call(this, this.value))
},
on: function(fn) {
this.fns.push(fn)
}
};
//------ View
let demo = `
<div>
<p>${model.value}</p>
<input value='${model.value}' />
<button id="button1">+1</button>
</div>
`
$('body').html(demo)
//------ ViewModel
$('input').on('keyup', function() {
model.set($(this).val()|0)
})
$('button#button1').on('click', function() {
model.set(model.value + 1)
})
model.on(function(value) {
$('p').html(value)
$('input').val(value)
})
在这个例子中, input输入的内容会实时显示在p标签中,而button被点击之后,也会对p标签的值和input标签的值都做+1处理;如果我们按照通常的 jQuery 来处理的话应该怎么做?
$('input').on('keyup', function() {
let value = $(this).val()|0;
$('p').html(value)
$('input').val(value)
})
$('button').on('click', function() {
let value = $('input').val()|0 + 1;
$('p').html(value)
$('input').val(value)
})
从这两段代码来看,并没有太大区别,甚至下面一段代码看起来更短。然而下面一段代码将 视图和模型的处理写到了一起,试想一下,当我们想再增加一个 -1 的button 呢?对于 MVVM的模式,只要再写
<button id='button2'>-1</button>
$('button#button2').on('click', function() {
model.set(model.value - 1)
})
即可。
而对于传统的方式,还需要先取得当前input值或p的值,然后再进行-1操作,最后还需要将input和p的innerHTML都修改一次。
随着DOM的增加,处理难度的差距越来越大,越来越不容易理解,修改一次,如履薄冰。
通过这个小例子可以看出 MVVM 相对传统的写法的最大的优势:
ps: 按照我的理解 Angular 中对于基础的VM进行了封装,所以在模板中绑定数据之后,数据更新,会自动更新视图。
莱文斯坦距离,又称Levenshtein距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
莱文斯坦距离与 react 有千丝万缕的关系,所以花了几天的时间研究了 levenshtein 的算法,以及使用 js 实现了一遍,从 这里 (此链接给出了大多数语言版本的实现)copy 了测试用例。后面如果有时间我再另写一篇莱文斯坦距离与 React 的 key
的关系的文章。
'use strict'
const min = Math.min
// 构造二维数组
const initialArray = (lgt1, lgt2) => {
let array = []
for (let i = 0; i < lgt1; i++) {
array[i] = Array(lgt2)
}
return array
}
/**
* Levenshtein Distance
* 从一个字符串转成另一个字符串所需要的最小操作步数
*/
const levenshtein = (sa, sb) => {
let d = []
, algt = sa.length
, blgt = sb.length
, i = 0
, j = 0
if(algt === 0) return blgt
if(blgt === 0) return algt
// 初始化二维数组
d = initialArray(algt + 1, blgt + 1)
for (i = 0; i < algt + 1; i++) {
d[i][0] = i
}
for (j = 0; j < blgt + 1; j++) {
d[0][j] = j
}
for(i = 1; i < algt + 1; i++) {
for(j = 1; j < blgt + 1; j++) {
if(sa[i - 1] === sb[j - 1]) {
d[i][j] = d[i - 1][j - 1]
} else {
d[i][j] = min(
d[i - 1][j] + 1,
d[i][j - 1] + 1,
d[i - 1][j - 1] + 1
)
}
}
}
return d[i - 1][j - 1]
}
// test case
// copy from https://rosettacode.org/wiki/Levenshtein_distance#JavaScript
;[
['', '', 0],
['yo', '', 2],
['', 'yo', 2],
['yo', 'yo', 0],
['tier', 'tor', 2],
['saturday', 'sunday', 3],
['mist', 'dist', 1],
['tier', 'tor', 2],
['kitten', 'sitting', 3],
['stop', 'tops', 2],
['rosettacode', 'raisethysword', 8],
['mississippi', 'swiss miss', 8]
].forEach(function(v) {
var a = v[0], b = v[1], t = v[2], d = levenshtein(a, b);
if (d !== t) {
console.log('levenstein("' + a + '","' + b + '") was ' + d + ' should be ' + t)
}
})
这是一篇水文,老板给面试同学出的笔试题,顺手练一下
function toBinary(n) {
function toBinaryIter(n, s) {
if(n === 0) {
return s || '0'
}
var a = n / 2
, b = n % 2
return toBinaryIter(a|0, b+s)
}
return toBinaryIter(n, '')
}
toBinary(0) === (0).toString(2)
toBinary(1) === (1).toString(2)
toBinary(3) === (3).toString(2)
toBinary(10) === (10).toString(2)
toBinary(101) === (101).toString(2)
在公司的项目中用了 koa
向前端(还是我)提供数据接口和渲染页面。有一些操作是和 Java 端交互,所以需要写一些代理转发请求,从网上找了一些koa的代理库,要不就是bug横生;要不就是功能不完全,只能代理 get 请求,于是用 node-fetch
写了个简单的 proxy ,代码挺简单的,写篇博文记录下。
用到了 fetch api,可以看 node-fetch
// proxy.js
import fetch from 'node-fetch'
export default (...args) => {
return fetch.apply(null, args).then(function (res) {
return res.text()
})
}
}
// 没错,就是这么简单,稍微把fetch封装下就可以了
// app.js
import koa from 'koa'
import proxy from './proxy'
const app = koa()
const proxyServer = koa()
app.use(function* (next) {
if ('/users' === this.path && 'GET' === this.method)
this.body = yield proxy('http://localhost:8087/users')
if ('/user' === this.path)
this.body = yield proxy('http://localhost:8087/user', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Hubot',
login: 'hubot',
})
})
yield next
})
proxyServer.use(function* (next) {
if ('/users' === this.path && 'GET' === this.method)
this.body = {
'test': 1
}
if ('/user' === this.path && 'POST' === this.method) {
this.body= {
'data' : `yes, it's right`
}
}
yield next
})
app.listen(8086)
proxyServer.listen(8087)
console.log('server start 8086')
console.log('server start 8087')
上面 app.js
中创建了两个 server,8086端口为代理 server, 8087端口为被代理的 server,访问 localhost:8086/users
会返回 {"test": 1},说明get请求代理成功,同理访问 localhost:8086/user
,会返回
{ "data": "yes, it's right"},说明成功代理了post请求并返回了被代理server的结果。
昨天被问到「你学编译原理有什么用,对你现在的工作有什么帮助吗?」
我当时脑袋不太好,没有很好地回答出来,只说了一些关于 JavaScript 的变量提升方面的东西。直到今天下午我读 w3c 的 rfc2612 规范的时候,我才意识到学习编译原理对我的帮助和影响。
在 rfc2612 规范中定义了一系列的 Catch-Control 的指令如下:
Cache-Control = "Cache-Control" ":" 1#cache-directive
cache-directive = cache-request-directive
| cache-response-directive
cache-request-directive =
"no-cache" ; Section 14.9.1
| "no-store" ; Section 14.9.2
| "max-age" "=" delta-seconds ; Section 14.9.3, 14.9.4
| "max-stale" [ "=" delta-seconds ] ; Section 14.9.3
| "min-fresh" "=" delta-seconds ; Section 14.9.3
| "no-transform" ; Section 14.9.5
| "only-if-cached" ; Section 14.9.4
| cache-extension ; Section 14.9.6
cache-response-directive =
"public" ; Section 14.9.1
| "private" [ "=" <"> 1#field-name <"> ] ; Section 14.9.1
| "no-cache" [ "=" <"> 1#field-name <"> ]; Section 14.9.1
| "no-store" ; Section 14.9.2
| "no-transform" ; Section 14.9.5
| "must-revalidate" ; Section 14.9.4
| "proxy-revalidate" ; Section 14.9.4
| "max-age" "=" delta-seconds ; Section 14.9.3
| "s-maxage" "=" delta-seconds ; Section 14.9.3
| cache-extension ; Section 14.9.6
cache-extension = token [ "=" ( token | quoted-string ) ]
看到上面一段内容,我脑海里自然地就知道这是 cache-control 的 DSL
,定义了 cache-control 的写法。
讲道理,学「编译原理」或者「算法」这种基础学科,也许对目前的工作确实没有什么明显的帮助,但是它终究会在你无意识的情形下,润物细无声的影响你。
创建数组除了字面量和 new Array()
外,还可以通过 Array(n)
创建,n 为数组的长度。 Array(n)
生成了长度为 n 的空数组,注意,和数组中元素赋值为 undefined 是有区别的;chrome 中查看空数组为[undefined * n]
,而赋值为 undefined 的数组为 [undefined, undefined, ..... , undefined]
。
range:
let rangeArray = (start, end) => Array(end - start + 1).fill(0).map((v, i) => i + start)
rangeArray(0,10) // return [0,1,2,3,4,5,6,7,8,9,10]
由于map
不能对数组中未赋值的元素进行遍历,所以可以通过 ES6 的新方法 fill
对数组进行填充,把数组中的所有数转为0(转成什么都无所谓),然后通过 map
方法将数组中所有0都转成对应的数字。
ES5 没有 fill
方法也可以通过 Array.apply(null, {length: end - start + 1}).map((v, i) => i + start)
搞定。说起来比第一种方法速度可能更快。
random:
let randomArray = (start, end) => {
let range = rangeArray(start, end)
, random = []
while (range.length) {
let i = Math.random() * range.length | 0
random.push(range[i])
range.splice(i, 1)
}
return random
}
// test
let random = randomArray(1, 50)
console.log(random.length === 50)
console.log(Math.min.apply(Math, random) === 1)
console.log(Math.max.apply(Math, random) === 50)
console.log(random.sort((a, b) => a - b).every((v, i, a) => v === a[i - 1] || 0 + 1))
具体原理就是:生成一个 range 数组,然后随机 range 数组的长度,得到下标 i,取出 range 中下标为 i 的元素放入新数组 random 中, 删除 range 数组这个元素,接着循环,直到 range 数组被删完。
最近在看「算法」,所以厚着脸皮分析下时间复杂度
吧,不对的地方欢迎指出:生成一个 range 数组,2n 次循环,循环 range 数组 n 次,所以加起来就是 3n 次,所需时间为线性级别的,时间复杂度为 O(n),所以看起来还是挺快的。
作为对比,分析一种以前经常用到的方法:
let randomArray = (start, end) => {
let o = {}
, length = end - start + 1
, random = []
while (random.length < length) {
let i = (Math.random() * length + 1) | 0
if (o[i]) continue
else {
o[i] = true
random.push(i)
}
}
return random
}
// test
let random = randomArray(1, 50)
console.log(random.length === 50)
console.log(Math.min.apply(Math, random) === 1)
console.log(Math.max.apply(Math, random) === 50)
console.log(random.sort((a, b) => a - b).every((v, i, a) => v === a[i - 1] || 0 + 1))
从上面代码可以看到在最好的情况下(不会出现 random 结果重复的情况),所需的时间复杂度为 O(n),最坏的情况下(每次都重复的话...循环到无数次),这样就只能算期望了,数学不好就不瞎算了。
自己对上面的分析之后认为在输入数特别大的情况下,前一种方法会把后一种方法碾压成渣,然而实际情况是反被轰成了渣滓,原因是忽略了splice方法,splice 是对数组的元素进行移动操作,会耗费了大量时间。
let a = [], b = []
console.time('�push')
for (let i = 0; i < 1e5; i++) {
a.push(i)
}
console.timeEnd('push') // ==> 3ms
console.time('splice')
for (let i = 0; i < 1e5; i++) {
b.splice(i, 1, i)
}
console.timeEnd('splice') // ==> 21ms
从上面可以看出splice花费的时间�远远超过push 。
写着写着就好像与标题相差了万八千里.... 不过具体写了什么就没所谓了,权当记录。
======= 2015-11-4 更新 ======
let randomArray1 = (start, end) => {
let range = rangeArray(start, end)
, random = []
, N = range.length
while (N--) {
let i = Math.random() * (N + 1) | 0
random.push(range[i])
range[i] = range[N]
//range.splice(i, 1)
}
return random
}
避免使用 splice 方法,算法没什么变化,实现写法改了一点点,速度大大提升,测试结果如下:
console.time('random1')
let random1 = randomArray1(1, 1e5)
console.timeEnd('random1')
console.time('random2')
let random2 = randomArray2(1, 1e5)
console.timeEnd('random2')
random1: 12ms
random2: 79ms
可以看到在我的电脑下,对1万条数据的处理速度提升了接近6倍。和之前的分析结果还算是相符的。
工欲善其事,必先利其器。
给自己定下写文章的目标后,就去找了几家博客平台来发布文章;作为一个懒人,不能所有博客文章都手动去各家平台发布,只好通过编写脚本来发布。但是除了Github
提供了比较详细的Api
外,其他国内的博客平台都没有提供对应的接口,但总有办法的。
下面是我对某家博客平台模拟登录流程的记录(打死我都不会说这家平台是S开头的),个人觉得挺有意思的,也能从中学到不少产品安全设计的思路。
Babel
Cheerio.js
SuperAgent
Chrome 浏览器
注:工具只是实现结果的一个手段,并不一定需要掌握这些工具,只要知道它们是干嘛的就行了。
先进入主页找到用户登录页,如下图所示:
标准的登录框,在这边需要把Chrome
的控制台打开,进入Network
页,把 Preserve log (页面跳转也能记录日志,感谢 铁臂狗 告知)的选项勾中, 如下图所示:
抓包分析请求,先从输入正确密码开始:
我把暴露隐私的两个地方打码了(这两块也是我们接下来要着重要分析的点)
可以从中看到请求头,我们先把这些请求头照抄下来
const base_headers = {
Accept: '*/*',
'Accept-Encoding':'gzip, deflate',
'Accept-Language':'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,ja;q=0.2',
'Cache-Control':'no-cache',
Connection:'keep-alive',
DNT:1,
Host:'segmentfault.com',
Origin: 'http://segmentfault.com',
Pragma:'no-cache',
Referer: 'http://segmentfault.com/',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest'
}
我们可以看到在请求登录的时候 Header 就已经带有 Cookie 了,这在我平常的设计中没有做过,所以我就试着把 Cookie 删后再请求,看看有什么效果。删除 Cookie 的方法如下所示:
利用排除法不停删除并继续试着登录,都能完成登录;直到删除 PHPSESSID 的时候发现删除之后再登陆是会报错的,所以这个 PHPSESSID 肯定是有用的(没用过PHP对这个不太了解),因此我断定这个 Cookie 是在后端作为验证登录的一个字段;因此我可以通过在登录之前先下载首页并拿到 Cookie,放到请求头上做作为模拟 Header。
import request from 'superagent'
let cookie;
req
.get(urls.mainpage)
.end((err, res) => {
// 从上图可以看到我们需要的cookie是PHPSESSID开头的
cookie = res.headers['set-cookie']
.join(',').match(/(PHPSESSID=.+?);/)[1]
})
本以为拿到 Cookie 之后就可以开开心心的做登录请求,然而这么简单的话这篇文章页也就没什么写的必要了。
继续分析请求 HTTP 包,可以发现在每次请求的时候,url 后面总是会带一个 queryString(图 2),我在这里耗费了不少时间,毫无头绪,只能追进源码里面摸索。
找到上图中的源码,可以看到这个源码是被压缩过的,不要着急,chrome 提供了 formatt 功能,点击最下面的{}
,可以对压缩的代码重排,至少是勉强可以阅读的代码了。
接下来的事情就是怎么从这堆代码中抽丝剥茧找到对我们有用的信息,可是这么多的代码一步步看下来也会看到头晕脑胀,眼睛滴血。那么就试试看能不能使用查找的方式从源码中找到我需要的东西。使用快捷键 ctrl+F,键入 /login
(/login
是作为登录的链接的,感觉上可能会有很大概率能搜到相关代码)
很巧的是,搜到了相关的代码。从中可以看到此网站使用了 JQuery 的 Ajax 发送相关 HTTP 请求,那么,url 便是 e.attr("action")
,从下面的 DOM 结构能看到 action 是api/user/login
。
还是没有找到 queryString, 那就换个关键词试试看,这次搜索 _=
(看图2,queryString 是由_=
拼接起来的)
从上图可以看到有7个结果,而被黄色标注出来的那行才是我们想要的。JQ 的 ajaxSend 可以在 Ajax 发送之前做一些处理。从上图可以看出,请求的时候在 url 的后面增加一个 n._
,那就继续去找n._是什么?由于截图截少了,我就不再重新截图,从上图的第一行可以看到 _ 是window.SF.token,由此我们就摸到 token 的 G 点,整个流程明朗了许多。接下来全局搜索 window.SF.token,没找到。我知道 window 是全局变量,为什么把 token 放到 window 上?可以想多的是 token 并没有在当前的 script 标签内。接下来去 index.html 内查找:
找到了!可以看到 token 是被包裹在一个独立的 script 标签内,在后端生成HTML模板的时候就已经插入。
找到 token 之后就很简单了,拿到这个字符串表达式,运行,拿到token。
原理我之前写过一篇文章,移步
import cheerio from 'cheerio'
import request from 'superagent'
let cookie;
// 为什么这样做
function getToken(s) {
let $ = cheerio.load(s)
, text = $('body script').eq(2).text()
, fn = new Function('window', text + ';return window.SF.token')
, token = fn({})
$ = null
return token
}
req
.get(urls.mainpage)
.end((err, res) => {
let token = getToken(res.text)
// 从上图可以看到我们需要的cookie是PHPSESSID开头的
cookie = res.headers['set-cookie']
.join(',')
.match(/(PHPSESSID=.+?);/)[1]
})
拿到 token 和 Cookie ,抓包分析所需要的登录字段:
{
mail: '[email protected]', // 邮箱
password: 'xxxxxxx', // 密码
remember: '1' // 是否记住登录
}
登录:
req
.get(urls.mainpage)
.end((err, res) => {
let token = getToken(res.text)
// 从上图可以看到我们需要的cookie是PHPSESSID开头的
cookie = res.headers['set-cookie'].join(',')
.match(/(PHPSESSID=.+?);/)[1]
req
.post(urls.login)
.query({'_': token})
.set(base_headers)
.set('Cookie', cookie)
.type('form')
.send(conf)
.redirects(0)
.end((err, res) => {
console.log(res)
})
})
})
世上无难事只怕有心人
登录是最基础也最核心的功能,通过对登录流程的分析,基本弄清楚了此博客平台的验证机制,在分析的过程中斗智斗勇,利用自己掌握的知识一步一步破解谜题的本身就是一件很有意思的事情,以后也可以将此方法用到自己的登录流程设计中。
登录之后能施展的手段就很多了: 提问题,发表文章,创建标签等等,用到得知识都在上面说过了,按下不表。
有需要源码的同学,欢迎 Star
// /roki/index.js
const roki = function () {
let oldvnode
return {
h,
render
}
function isString(s) {
return typeof s === 'string'
}
function h(type, props = {}, children = []) {
children = children.map((child) => {
if (isString(child)) return createTextVNode(child, void 0)
return child
})
return createVNode(type, props, props.key, children, void 0)
}
function patch(oldvnode = {}, vnode) {
let element
if (vnode.type) {
if (oldvnode && oldvnode.type === vnode.type) {
element = oldvnode.dom
updateProps(element, vnode.props, oldvnode.props)
} else {
element = createElement(vnode.type, vnode.props)
}
vnode.children && patchChildren(oldvnode.children, vnode.children, element)
} else if (vnode.text) {
if (oldvnode.text) {
element = oldvnode.dom
if (vnode.text !== oldvnode.text) element.nodeValue = vnode.text
} else {
element = createTextNode(vnode.text)
}
}
vnode.dom = element
return vnode
}
function patchChildren(oldChildren = [], children = [], parentElement) {
children.forEach((child, index) => {
let oldChild = null
if (oldChildren) oldChild = oldChildren[index]
if (oldChild) {
let newNode = patch(oldChild, child)
if (oldChild.type !== child.type) {
replaceChild(parentElement, oldChild.dom, newNode.dom)
}
} else {
let newNode = patch(undefined, child)
appendChild(parentElement, newNode.dom)
}
})
oldChildren.slice(children.length).forEach((child) => removeChild(parentElement, child.dom))
}
function render(vnode, container) {
vnode = patch(oldvnode, vnode)
if (!oldvnode) appendChild(container, vnode.dom)
oldvnode = vnode
}
function createVNode(type, props, key, children, dom) {
return {
type,
children,
props,
dom,
key
}
}
function createTextVNode(text, dom) {
return {
text,
dom
}
}
function removeChild(element, child) {
element.removeChild(child)
}
function appendChild(element, child) {
element.appendChild(child)
}
function replaceChild(element, oldChild, newChild) {
element.replaceChild(newChild, oldChild)
}
function createElement(tag, props = {}) {
const element = document.createElement(tag)
updateProps(element, props)
return element
}
function updateProps(element, props = {}, oldProps = {}) {
Object.keys(oldProps)
.filter((key) => !key.startsWith('on'))
.forEach((key) => { element.removeAttribute(key) })
Object.keys(oldProps)
.filter((key) => key.startsWith('on'))
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
element.removeEventListener(eventType, oldProps[key]);
})
Object.keys(props)
.filter((key) => !key.startsWith('on'))
.forEach((key) => { element.setAttribute(key, props[key]) })
Object.keys(props)
.filter((key) => key.startsWith('on'))
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
element.addEventListener(eventType, props[key]);
})
}
function createTextNode(text) {
return document.createTextNode(text)
}
}
window.Roki = roki()
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button id="update">update</button>
<div id="root"></div>
<script src="./roki/index.js"></script>
<script>
const {
render,
h
} = Roki
let vnode = h('div', {}, [
h('span', {
style: 'font-weight:bold;',
onclick: function () {
console.log('old span')
}
}, ['This is bold']),
' and this is just normal text',
h('a', { href: '/foo' }, ['I\'ll take you places!']),
h('br'),
'this will delete'
])
let newVnode = h('div', { onclick: function () { console.log('new div') } }, [
h('span', { style: 'font-weight: normal; font-style: italic;' }, ['This is now italic type']),
' and this is still just normal text',
h('a', { href: '/bar' }, ['I\'ll take you places!!!']),
h('br'),
]);
render(vnode, root)
update.addEventListener('click', () => {
console.log(222)
render(newVnode, root)
})
</script>
</body>
</html>
写这篇文章的起因是源于和同事的一次讨论, 同事的周报上说在写 React 组件的时候, A 组件 Import 了 B 组件, 同时在 B 组件中也 Import 了A 组件,此情况就形成了循环依赖,在 A 组件中引入的 B 组件为 undefined
。但是在我之前的印象中( #16) ,此时的 B 组件应该为空对象 {}
。
一言不合,看看 babel 把 ES6 的 Module 翻译成什么:
// =================
// a.js
// =================
import b from './b'
function foo() {
b.bar();
}
export default foo
// =================
// b.js
// =================
import a from './a'
console.log(a)
上面一段代码,如果是 node
的写法,在读取 a.js
的时候就被 cache
为一个空对象了,此时在 b 中读取
a一定会是一个空对象,但是通过
babel-cli把
a.js和
b.js翻译成
ES5` 之后再看看:
babel a.js > a.bundle.js
babel b.js > b.bundle.js
// =================
// a.bundle.js
// 把 require './b' 改成 './b.bundle'
// =================
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _b = require('./b.bundle');
var _b2 = _interopRequireDefault(_b);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function foo() {
_b2.default.bar();
}
exports.default = foo;
// =================
// b.bundle.js
// 把 require './a' 改成 './a.bundle'
// =================
'use strict';
var _a = require('./a.bundle');
var _a2 = _interopRequireDefault(_a);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_a2.default);
从上面两段代码可以看到 babel 把 export default foo
转成 exports.default = foo
, 在 b.bundle.js
中根据 #16 中分析的可以知道 _a
的值为 {}
,所以此时 _a2.default
的值为 _a.default
, _a 为 {}
, 那么 {}.default
是 undefined
。
其实如果按照 ES6 的标准,ES6 module 的出现就天然的解决了循环依赖的问题,babel 对 ES6 的module 的支持还是有些问题的,只是简单的翻译成 common.js 的方式。😄
branch summary
summary
summary
Changelog 的二级标题是分支名,每个分支都是有自己的目标的(例如修复 bug,增加功能点等等),所以可以在分支下面附上一些简短的介绍分支功能(不建议很长,和之后要写的内容重叠)。summary 都是非必填但是推荐填写的。
Changelog 的三级标题是组件名称,Container 仓库的以组件为维度。
列表:
[功能类型](Aone 的 id):此次修改所做的事情的简短介绍。
列表的第一层级是独立事件,第二层级是第一层级的关联事件,深度为 2 层。
每次组件的提交都应该去更改 Changelog,这是对 Changelog 的持续维护。
一个组件有多个新功能点,分多次提交,那么每次提交都应该去修改 Changelog,标注此次更新内容。
一个组件只有一个新功能点,但是第一次提交的时候,以为自己的提交已经完成所有功能,但是实际情况是没有完成,那么就可以去更新 Changelog 为
### Container Name (input)
-[feat]: 增加 xxx 功能点
* [feat]: 上一次代码完成的功能有哪些 ....
* [feat]: 本次代码完成的功能有哪些....
Tips:
虽然原则是应该是最小化提交。每次 commit 都只做一件事,但是如果真的在一次 commit 里面做了多件事,改了多个 cotainer 的代码, 也是同样可以在 changelog 中修改多个 container 的内容。
Fix the Problem, Not the Blame
要修正问题,而不是发出指责
--『程序员修炼之道 -- 从小工到专家』
在几个月前我接手了一个几经易手的老业务代码,相关开发人员都已转岗。在前几个月的时间,我被新的项目需求弄得焦头烂额,还要不停的为老的业务代码的 bug 擦屁股。每次爆出老项目的 bug ,心里总是想着指责、抱怨、推诿、逃避。
一个星期有 3 天时间在开会,剩下 2 天修 bug,每天都在焦虑中度过,一度在想自己是不是应该换份新的工作来逃避这种状况。「我的天呐,这种日子什么时候才能到头」「再坚持坚持,事情总会做完的」「加油,忙完这段时间就可以忙下一段时间了,呵呵」这就是我上段日子最真实的心里活动写照。
接手老的项目也有一季度了,慢慢地调整心态:首先要承认自己力有不逮,把一些做不完的需求推给老板帮忙解决;其次遇到他人的 bug 之后,虽然也会有些许指责,但是要明白 bug 是你的过错还是别人的过错,并不是真的很有关系。它仍然是你的问题。
放平心态,不要恐慌;最后,量化自己的工作,所有的工作都要在文档中有体现,否则不管是别人和自己都不知道你到底做了多少事。
最后推荐一本书:『程序员修炼之道 -- 从小工到专家』,我用了 3 个月想通的事情,书上都有。
现在有100块钱人民币,将 100 块钱换成零钱(最小币值 1 元),一共有多少方式?
总的不同方式的数目等于:
ok, 根据上面的说法来实现吧:
'use strict'
// 实现 lisp 中的 list
// car 是 list 中的第一个值
// cdr 是 list 中的剩下的值的集合
const list = (...args) => args
,car = (list) => list[0]
,cdr = (list) => list.slice(1)
// 换零钱的方式
// 如果换 0 元钱,就算是有一种换钱方式
// 如果换的钱小于 0, 那么就算有零种换钱方式
// 如果币值的长度为 0, 那么也算是有零种换钱方式
function count_change(amount, coin_values) {
switch (true) {
case (amount === 0):
return 1
case (amount < 0 || no_more(coin_values)):
return 0
default:
return (
count_change(amount, except_first_denomination(coin_values))
+
count_change(
amount - first_denomination(coin_values),
coin_values
)
)
}
}
function no_more(coin_values) {
return coin_values.length === 0
}
function first_denomination(coin_values) {
return car(coin_values)
}
function except_first_denomination(coin_values) {
return cdr(coin_values)
}
测试一下:
const cn_coins = list(100, 50, 20, 10, 5, 2, 1)
count_change(100, cn_coins) // ---> 4563
jsx 中允许使用 map 方法动态生成子组件,如下所示
render: function() {
var results = this.props.results;
return (
<ol>
{results.map(function(result) {
return <li key={result.id}>{result.text}</li>;
})}
</ol>
);
}
{...}
包裹的语句块中实际返回的是一个子组件的集合,[<li>...</li>,....,<li>..</li>]
,之后会被babel
转换成
React.createElement('ol', {}, results.map(function(result) {
return React.createElement('li', {key: result.id},result.text)
})
)
这样就可以动态生成子组件,但是没法解决生成dl组件的问题啊:
render: function() {
var results = this.props.results;
return (
<dl>
{results.map(function(result) {
return (<dd>{result.text}</dd><dt>{result.content}</dt>)
})}
</dl>
);
}
一个return语句中只允许返回一个子节点,so,这种方法是行不通的。但是jsx允许返回组件的集合,例如[<div></div>,<div></div>]
,所以我们可以在map中返回一个数组。
render: function() {
var results = this.props.results;
return (
<dl>
{results.map(function(result) {
return ([<dd>{result.text}</dd>,<dt>{result.content}</dt>])
})}
</dl>
);
}
TL;DR
本文是对Promise
一些概念点的归纳和对Promise
在解决什么问题的思考。
并不会介绍Promise
的 Api。
一直想写一篇关于 Promise
的文章,一直没有动笔,觉得自己对 Promise
是一知半解,连记录想法的勇气都没有。前几天刚好在看 Dr. Axel Rauschmayer
的『 Exploring ES6 』 这本书的时候,看到关于 Promise
的章节,仔细读了一遍,动手实现了简易版的 Promise
一遍,印证其它的资料又回味了一遍,这才敢开始动手写这篇文章。
Promise
哪些知识点const p = new Promise(
function (resolve, reject) { // (A)
···
if (···) {
resolve(value); // success
} else {
reject(reason); // failure
}
})
executor
Promise
的时候,executor
总是「立即执行」的Promises
总是异步执行的。在 then 的消耗 Promise
value 时候,会把 then 的参数放到任务队列中,等待执行。Promise
中的状态进入稳定态(settled)的时候,Promise
的值便不会再发生变化,这就是 Promise
的名称由来:承诺。// executor 立即执行的验证
new Promise( (res) => {
console.log(1)
setTimeout(() => console.log(3), 1000)
})
console.log(2)
// ------> 1
// ------> 2
// ------> 3
// then 的参数是异步调用的验证
new Promise( (res) => {
res(2)
}).then(v=> console.log(v))
console.log(1)
// ------> 1
// ------> 2
// 当在 executor 中通过调用 res 使状态稳定之后,不管调用多少次 res,值都不会再发生变化
new Promise( (res) => {
res(1)
res(2)
}).then(v=> console.log(v))
// ------> 1
Promise
-- 一诺千金Promise
的优点是什么?我看了很多文章在介绍 Promise
的时候都会提到回调噩梦(Callback Hell
)。然而我觉得 Promise
并没有简单的解决回调噩梦的问题,写 then 往往比写 callback 更恶心。
在我看来,Promise
提供了控制反转的机会。
假设有下面这样一个函数,10 秒后调用传入的 fn。这是一个你的同事提供给你的独立文件。
// foo.js
function foo(fn) {
setTimeout(fn, 1000*10)
}
module.exports = foo
你在写一个 10 秒之后打印 log 的业务:
var foo = require('foo')
foo(()=> console.log('hello,world!!'))
然而很不幸,你的同事离职,他的代码被交给一个很笨的新同事维护,新同事维护这个代码的时候,不小心复制了一行代码:
// foo.js
function foo(fn) {
setTimeout(fn, 1000*10)
setTimeout(fn, 1000*10)
}
module.exports = foo
这时每次调用 foo 的时候,都会写两遍日志,不到 1 天服务器的硬盘就被撑爆了,用户就不能访问网页了,接着用户就流失了,公司倒闭了,你就被失业了,想想都是很可怕的事情。这些都是因为你把你的函数交给你的同事调用并且无条件信任他。
然而 Promise
会怎样做?
// foo.js
function foo() {
return new Promise((res) => {
setTimeout(res, 1000*2)
})
}
module.exports = foo
// main.js
var foo = require('foo')
foo().then(()=> console.log('hello,world!!'))
那个笨同事又干了同样的蠢事,这次他复制了三行:
// foo.js
function foo() {
return new Promise((res) => {
setTimeout(res, 1000*10)
setTimeout(res, 1000*10)
setTimeout(res, 1000*10)
})
}
module.exports = foo
然而这次让我失业的行为并没有得逞,因为当 Promise
的状态稳定之后,值就不会再改变,不管调用多少次 reslove 方法都是同样的效果。Callback 会把你做的事情的权限交出去,你不再对你的函数有控制权,而 Promise 是在等状态稳定之后才会再去执行你自己的函数,你对此函数拥有控制权。
不过说到底,都没有绝对的信任,也说不定有人会把 Promise
的 then 实现 hack 了,而这种恶意做法的成本要比他不小心多复制一行代码的成本要高得多。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.