深入了解 Express 框架

1. 基础

express 是基于nodejs的web框架,而一个传统的nodejs server 处理一个http请求的逻辑是这样的

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

express 所做的,就是传入一个函数,来处理所有的请求:即 http.createServer(app), 这个app函数lib/express.js,由new Express()而来,

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  app.request = { __proto__: req, app: app };
  app.response = { __proto__: res, app: app };
  app.init();
  return app;
}

new Express() 的时候,返回的是一个 (req, res, next) => { app.handle(req, res, next); } 函数,处理http模块派发的每一个请求,同时给这个对象加入了事件派发,默认的方法,并暴露request和 response的原型给app.request, app.response 属性。init, use, route, engine, param, set, enabled, disabled, enable, disable, all, [http_methods], render, listen 等属性也被添加进去。

app._router 为Router实例,决定了每个请求的行为。

2. app.use

我们知道,express的middleware 和router都是通过app.use生效的,

2.1 middleware

所有的express middleware,都是一个 (req, res, next) => { ... next(); } 结构的函数, 调用app.use(middleware)的时候,实际上是 app._router.use('/', middleware)。

2.2 路由

按照express.Router()声明的路由本质上也是middleware, 通过 app._router.use('/', routerN) 挂载到app._router对象。

2.2.1 路由内部的派发

看一个实际的例子

var router = express.Router();

router.get('/all', (req, res, next) => {})

router.HTTP_VERBS 实际上调用了 router.route(HTTP_VERBS)。

假如在 user 相关的 Router routerU 上注册一个路由 routerU.get('/test', fn), 过程如下:

  1. 创建一个和路径 /test 相关的 Route 实例 routeT
  2. 在routeT.stack 中插入一个新建的 路径为 '/', method 为 'GET' 的LayerFn
  3. 创建一个和路径 /test 相关的 Layer 实例 layerT, layerT 的 handle 被设置为 routeT.dispatch, 将layerT push 到routerU.stack 中
    app._router.stack
        |-Layer
        |-Layer // jsonParser middleware
            |-name: "jsonParser"
            |-handle *function jsonParser*
        |...
        |- Layer // users router
            |-name: "router"
            |-handle: router // users router 的实例
                |- stack // router 的stack
                    |-Layer // Layer for router
                        |-name: "bound dispatch"
                        |-handle: route.dispatch
                        |-route: Route // users Route 具体的一条规则
                            |-path: "/register"
                            |-stack // router 的stack
                                |-Layer // Layer for route
                                    |- handle  *function (req, res, next) { return res.json('xxx') }*
                                    |- regexp: /^\/&/

示例图2

注意:node-inspector 在 node v6.2 下有bug,调试异步回调的时候出现 Internal Error: Illegal Access 的报错, 将node版本切换到 v5.0 之后,就可以正常调试了

[2016-05-26]

详解 HTTP 缓存

HTTP协议的缓存逻辑一直让人摸不着头脑,本文将带领你全面了解传说中的HTTP缓存

浏览器对缓存的处理行为

主流的打开页面的方式有:

  1. 直接地址栏输入
  2. 按住F5刷新当前页面
  3. 按住Ctrl键刷新当前页面

HTTP 缓存机制

HTTP缓存机制由两部分组成:文档过期()和服务器再验证(server revalidation)

过期日期和使用期

服务器使用 HTTP/1.0+ 的Expires 首部或 HTTP/1.1 的 Cache-Control: max-age 响应首部来 指定过期日期。两者本质上是一样,但是Cache-Control 使用的是相对日期(秒),因此更倾向于用 Cache-Control。 (Expires 指定的是绝对日期,如果服务器的时间设置不准确,就会出错)

服务器再验证

缓存文档过期并不意味着它和原始服务器上的最新文档有实质的区别,需要通过“服务器再验证”来确认原始文档是否发生了变化。

用条件方法进行再验证

HTTP 定义了 5 个条件请求头部,都以 If- 开头,其中对缓存再验证来说最重要的是下面两个条件首部:

  • If-Modified-Since: <date> 可以与 Last-Modifeid 响应头配合使用,如果从指定日期之后文档被修改过了,执行请求的方法,
  • If-None-Match: <tags> 服务器为文档提供特殊的标签,如果与服务器文档中的标签有所不同,执行请求的方法。

If-Modified-Since 具有以下的局限性:

  1. 周期性重写某些文档,但是内容没有变化
  2. 文档在一秒内发生多次变化,不能准确标注文件的新鲜度
  3. 有可能存在服务器没有准确获取资源修改时间,或者与代理服务器时间不一致的情形

因此,HTTP允许被称为实体标签(ETtg)的“版本标识符”进行比较,如果实体标签被修改了,缓存就可以用“If-None-Match” 条件首部来GET文档的新副本。

Etag 和 Last-Modified 优先级

如果服务器回送了ETag,HTTP/1.1 必须使用 Etag,

如果HTTP/1.1 服务器同时收到 If-Modified-Since 和 If-None-Match 条件首部,只有两个条件都满足时,才能返回 304 Not Modified 响应。

Reference

[2016-05-06]

Babel 使用教程

什么是 Babel

Babel 是一款 Javascript 编译器, 将书写的代码编译转换成被广泛支持的 JavaScript 代码,其最为广泛的运用主要有 将 ES6 转换成 ES5代码, 将 JSX 编译成 JavaScript

使 nodejs 全面支持 ES6

nodejs v5 版本原生支持大部分 ES6 语法,但对于某些特性并不支持,比如 Module, Destructuring Assignment, Parameter 等,借助 Babel 能够让 Node.js 也能支持全部的ES6特性。其核心原理为配置使得 babel 将 Node.js 尚不支持的 ES6 语法转换为支持的 ES5 代码进行执行.

参考 :

[2016-03-23]

ES6 中的一些新特性

随着 nodejs 和 iojs 的合并,原生支持 ECMAScript2015(ES6) 的特性让人看到了ES6在实际运用的良好条件。而浏览器端,则有类似于 Babel 之类的编译器,将 ES6 转换为 ES5 供普通浏览器使用,尤其是移动端基本上都支持ES5。本文记录ES6中的一些新特性

Block-Scope

  • let 声明的变量仅在 { } 内部可见
  • 声明在 { } 内的函数对外不可见

Arrow Function

  • v => v + 1, (v, i) => v + i 可声明单行表达式函数
  • v => { } 可声明多行函数
  • lexically binds the this value (does not bind its own this, arguments, super, or new.target)

Extened Parameter Handling

  • function(x, y = 10, z = 20){ }
  • function(x, y, ...a) 此时多余的参数变量会被放在数组a中
  • f(...params) 会将参数params进行拆分,数组变成每个元素,字符串变成每个字符 [--es_staging]


Spread Operator

... 作用在数组和对象前面时,执行 spread 运算,具体表现为: + 将数组对象展开为函数所需要的参数

    Math.max(...[1, 5, 8, 10]) // == Math.max(1, 5, 8, 10)
  • 展开数组对象到当前数组中
    let cities = ['San Francisco', 'Los Angeles'];
    let places = ['Miami', ...cities, 'Chicago']; // ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']


Symbol

symbol 是唯一且不变的数据类型,能作为对象属性的标识符,即作为哈希对象的键值,其使用示例如下

var obj = {};

obj[Symbol("a")] = "a";
obj[Symbol.for("b")] = "b";
obj["c"] = "c";
obj.d = "d";

for (var i in obj) {
   console.log(i); // logs "c" and "d"
}

// 
Symbol('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol.for('foo') // true


Map

Map 是新的数据结构,能对内部含有的值进行set, get, search等操作。最重要的是不像对象哈希那样只 接受字符串作为 key,Map 接受任何类型的 key 且不会将其转换为字符串。其初始化语法如下:

let map = new Map([
    ['name', 'david'],
    [true, 'false'],
    [{}, 'object'],
    [function () {}, 'function']
]);

for (let key of map.keys()) {
    console.log(typeof key);
    // > string, boolean, number, object, function
}

for (let [key, value] of map.entries()) {
    console.log(key, value);
}

WeakMap

WeakMap 对象相比 Map 对象,其key是弱引用的,因此只能是对象,当这个key指向的对象被删除的时候, 这个weakmap里关联的value会被自动清除。在进行一些dom相关的cache时,尤为有用。

[2015-11-05]

基于 Github 搭建增强版的 jekyll 博客系统

Github 的 gh-pages 相信大家都不陌生,使用 jekyll 来实现静态博客系统,代码完全存放在github上,不用担心服务器的配置和迁移问题, 然而相比起 wordpress,纯静态的gh-pages并不能很好的满足需求,比如:

  • 没有搜索功能
  • 没有后台编辑系统
  • 无法上传图片

本文将简单介绍本博客是如何使用前端技术来解决上述问题的。

1. 搜索功能

利用jekyll的特性,能将全部文章的标题、tag、category等信息输出成 JSON 数据用于实现基于上述信息的关键字匹配搜索, 然后前端通过 Github 请求上述数据实现搜索,jekyll 代码如下:

输出搜索数据代码

最终的搜索效果如下图所示:

搜索效果图

2. 文章编辑系统

首先需要进行 Github Oauth 认证来获取权限,点击导航栏左侧 “Gitub 登录” 按钮完成登录之后,可看到编辑按钮

编辑按钮

利用 Github 的API,可以获取到文章的源代码,利用 Octokat , Ace Editor, marked 等开源库,就可以实现一个前端的markdown可视化编辑系统, 效果如下图所示:

可视化文档编辑

这块稍微比较麻烦的是 accesstoken 的处理,由于 oauth 的特性,需要 server 端请求获取 accesstoken 之后,再重定向 回博客地址来获取 access_token。

本博客利用 Github API 来更新的效果如下所示: 保存成功

3. 图片上传

利用之前实现的基于HTML5的图片粘贴上传工具http://labs.hellofe.com/image,只需截屏之后,将图片粘贴到网页上,就能自动上传 到服务器并获取外链地址,大大缩短图片处理时间。

  • filters http://jekyllrb.com/docs/templates/
[2015-04-28]

使用 CORS 进行跨域

CORS 协议

在浏览器的响应头里面添加Access-Control-Allow-Origin: *, 能够实现跨域 ajax 请求

CORS 下的 OPTIONS 请求

http://stackoverflow.com/questions/1256593/jquery-why-am-i-getting-an-options-request-instead-of-a-get-request

CORS 下的 cookie

CORS 下的请求默认不会带上 cookie,也就无法获取用户的登录信息,那么如何带上 cookie 呢?jQuery 的 ajax 请求中加上下列配置

$.ajax({
    type: "POST",
    ...
    xhrFields: {
        withCredentials: true
    },
    crossDomain: true,
    ...
});

但是报以下错误:

XMLHttpRequest cannot load http://121.40.212.161:8000/data/design/edit?act=add&_dt=0.9176968049723655. A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true. Origin 'http://localhost:63342' is therefore not allowed access.

因为浏览器的安全策略,当允许 credentials 的时候,Access-Control-Allow-Origin 值不能是 *,而必须是一个指定的域名,例如上面例子必须是http://localhost:63342, 那么如何针对不同的访问设定对应的跨源允许域呢?

HTTP 中的 Referer 能很好地实现该需求,当在一个域下发起一个 CORS 请求时,HTTP 请求头的 Referer 值会自动被设置为当前页面域,此时只要在服务器端读取 Referer 值,构造出相应的 Access-Control-Allow-Origin 值即可,PHP 代码如下所示:

$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false;

if($referer){
    $urls = parse_url($referer);
    $url = $urls['scheme'] . '://' . $urls['host'];
    isset($urls['port']) ? $url .= ':' . $urls['port'] : '';
}else{
    $url = '*';
}

header("Access-Control-Allow-Origin: " . $url);//跨域访问
header("Access-Control-Allow-Credentials: true");

注意对于设置了 withCredentials 值的请求,其响应头中的 Access-Control-Allow-Credentials 值必须为 true, 如此跨域的请求也能携带 cookie 参数了

[2014-12-28]

window 对象上的只读属性

在对封装 localStorage 的库进行单元测试的时候,为了模拟 localStorage 不可以用的情况,想到了把 localStorage 进行复写来模拟 localStorage 不可用的情况,但是跑了单元测试才发现复写 localStorage 并没有生效,因为 localStorage 是 window 对象上的一个只读属性。

于是用下面的程序对 window 对象上的只读属性进行了筛选

(function(){
    var i = 0,
        ri = 0,
        wi = 0,
        readOnly = [],
        writeAble = [];

    for(var k in window){
        i++;
        k !='location' && k != 'console' && (window[k] = '__');

        if( window[k] === '__' ){
            writeAble.push(k);
            wi++;
        }else{
            readOnly.push(k);
            ri++;
        }
    }

    console.log('--- read only ---');
    console.log(readOnly.slice(0,80));
    console.log(readOnly.slice(80));
    console.log('--- write able ---');
    console.log(writeAble);
    console.log('total:' + i, ' read:' + ri, ' write:' wi);
})();

打开 console , 点我查看测试结果

以上:

  • 放在闭包里面是为了避免 i,deleted 对象挂载到 window 上而在后面的遍历中被删除
  • 重写 location 页面会被跳转
  • 重写 console 就无法输出有用的信息了

最终:

  • 只读的属性如下所示,共102(104 - console - location)项, 只读的属性

  • 可写的属性如下所示,共75 (73 + console + location) 项 可写的属性

总结

window 对象上可访问到的属性远不止(102+75 = 177)项这么多,只是通过 for ... in 的方法只能遍历出来这些属性,而实际上通过 Object.getOwnPropertyNames(window) 方法获取到的 window 上的属性有 500 项目之多

[2014-12-23]

使用appium进行移动端的自动化测试

appium 的核心是一个暴露了 REST API 的 web 服务器(使用 Node.js 实现),接受服务器的连接请求,等待命令输入,然后在移动设备上执行这些命令,并通过 HTTP 响应来返回命令的执行结果。使用 Client/Server 的模型提供了很多可能性:

  1. 能够使用具备有 http client API 的各种语言来编写测试代码。
  2. 将测试服务器和运行测试程序的机器放在不同的机器上
  3. 仅编写测试代码然后依赖类似 Sauce Labs 之类的云服务来接收和执行测试命令

客户端通过发送 POST /session 请求,提供"desired capabilities"对象(一系列的 k-v ,用于告诉 Appium 服务器需要启动怎样的测试 session),服务器然后创建一个自动化测试的 session,并返回 sessionid 用于后续的测试。

[2014-12-18]

window.onerror 回调中的 Script Error line 0 报错分析

在监控前端页面报错的时候,希望通过 window 的 onerror 事件来获取报错的内容,文件名以及代码所在行(线上代码一般都是压缩了因此没啥用)。

遗憾的是想要通过这种方法获取非本域下的 js 文件的报错时,得到的永远是Script Error,空的文件名和0的行号,因为浏览器的Same Origin Policy。当另外一个域下的 js 报错时,拿到的永远是 Script Error。

Webkit dispatchErrorEvent 同源限制源代码,如下所示 Webkit dispatchErrorEvent 同源限制源代码

Firefox dispatchErrorEvent 同源限制源代码

为什么要限制报错信息?

假如有个 script 标签:<script src="yourbank.com/index.html">,如果这个页面没有屏蔽报错信息,那么可能返回的信息是"Welcome Fred..."或者"Please Login ...", 于是当用户访问 A 页面时,就可以拿到用户在B 页面的一些敏感数据。

解决方案

那么,怎么样才能绕开同源策略得到正确的报错信息呢?设置 js 文件的 Access-Control-Allow-Origin: * 响应头,同时引用文件的时候加上 crossorigin 属性, 如script.crossOrigin = 'crossorigin',就能拿到正确的报错信息,如下所示 cross origin onerror 获取跨域报错信息示意图

参考链接

[2014-12-17]

script 标签的非阻塞加载

浏览器对于 script 的默认处理方式:阻塞并等待加载完毕,然后接着解析后面的html,其时序图如下所示: script

如果文件加载速度很慢的话,有没有什么办法能避免出现这种长时间的白屏呢?

方法一:动态创建 script 标签

对于动态创建的 script 标签,浏览器会在不阻塞页面执行的同时加载 js 文件,当文件加载完毕之后进行执行,其特点是动态创建的 JS 执行顺序没有保障,即同时创建多个 script 标签加载外链 js,先加载完毕的 js 先执行。

方法二:async 属性

async 属性的表现类似于动态创建 script 标签,其时序图如下所示: script-async 使用 async 带来的好处是不阻塞 html 的解析,带来的问题就是这些外链js 文件不再保持原先的顺序执行,因此如果有先后依赖的话就会报错。

方法三:defer 属性

defer 属性不阻止页面解析的同时,并不立即执行单个加载完毕的 js 文件,而是等页面完成解析并且全部 defer 标签加载完毕之后,再顺序执行,使用 defer 带来的好处是既不阻塞页面解析又保证了文件的执行顺序,其时序图如下: script-defer

Reference

[2014-12-15]

前端自动化测试之selenium

术语解释

  • selenium IDE,firefox 下的一款插件,用于录制 test case, 用于回放自动测试
  • Selenium WebDriver,即 Selenium 2,使用专用 driver 直接调取浏览器 API 来进行测试
  • Selenium RC, 即 Selenium 1 (是一个http代理程序)。因为使用 JavaScript 来进行测试,因此需要本地起一个http代理来避免跨域问题
  • [RemoteWebDriver], RemoteWebDriver 由 Server 和 Client 两部分组成
    • RemoteWebDriver Server, 是一个 Java 写的服务器,始终运行在需要跑测试代码的浏览器所在的机器上面。然后远程机器C1就可以通过 webDriver 协议访问这台机器S,并在这台机器S上运行C1的测试代码。可以通过命令行运行java -jar selenium-server-standalone-{VERSION}.jar启动 RemoteWebDriver Server 或者使用对应语言的 API 启动。
    • RemoteWebDriver Client, 启动测试的客户端程序。能分离进行测试的机器和运行测试代码的浏览器所在的机器,因此可以对当前操作系统不支持的浏览器进行测试。但是需要测试服务器上一直运行,测试服务器返回的文字结束符可能会有问题,会对测试造成一定的延迟。
  • Selenium-Grid,

Links

[2014-12-12]

移动端代理请求方案

不同于传统电脑上的代理,可以通过修改 host,使用 switchSharp 等手段方便的映射线上文件到线下,那么移动端上该如何去做这件事情呢?

HTTP proxy

使用 HTTP proxy,直接代理全部的HTTP请求,然后通过 HTTP proxy server 根据情况去转发请求或者返回待映射的文件

优势

只需维护好 server 转发规则即可

劣势

全部流量都需要经过 server 转发,如果请求量很大而 server 出口带宽不够的话,速度会是个问题

PAC(Proxy auto-config)

代理自动配置(Proxy auto-config)是一种网页浏览器技术,用于定义浏览器该如何自动选择适当的代理服务器来访问一个网址。一个PAC文件包含一个JavaScript形式的函数FindProxyForURL(url, host)。并设置网页服务器将这个文件的MIME类型声明为 application/x-ns-proxy-autoconfig, 如下所示:

function FindProxyForURL(url, host) {
    if (shExpMatch(url, "http://play.baidu.com/*") ||
            shExpMatch(url, "http://weibo.com/enimo*")) {
        return "DIRECT";
    }
    return "PROXY 172.22.114.19:9527";
}

优势

不需要全部流量转发,仅仅适配相应的请求

劣势

兼容性不是很好,比如有些手机如小米等并不支持 PAC,PAC文件的缓存更新并不由系统控制,而是由浏览器来控制

[2014-12-12]