JS 性能测试

开发基础库的时候,总是免不了纠结一下性能的问题,如何简单快速的进行代码性能测试显得非常有意义。

JS 篇

getElementById > getElementsByClassName > querySelector > querySelectorAll http://jsperf.com/dom-operation

[2014-12-09]

谁动了JSON.stringify ?

最近碰到一个比较恶心的 case,客户引了 prototoype.js 的库在 Object, Array 等原型链和对象上扩展了很多方法, 导致 Object.keys 和 JSON.stringify 的结果都产生了异常...

Object.keys

该方法被复写导致的 ‘TypeError: Property description must be an object: undefined’ 错误:

if (Object.keys) {
  Object.keys(supplier).forEach(function(property) {
    Object.defineProperty(receiver, property, Object.getOwnPropertyDescriptor(supplier, property));
  });
}

上面这段代码,存在什么问题?看起来没有任何问题,其实最大的问题在于 Object.keys 有可能被复写。

当 Object.keys 被用 for ... in 遍历对象来实现,而且没有加 hasOwnProperty 来过滤掉继承过来的属性时,问题就来了: Object.getOwnPropertyDescriptor 获取一个对象不存在的属性时返回 undefined,直接传给 Object.defineProperty 就会报 TypeError: Property description must be an object: undefined 错误

JSON.stringify

发现 JSON.stringify 结果不符合预期时,第一时间是怀疑该函数是不是被改写了?结果发现 chrome console 的输出是 function(){ [nativ code] }, 然后又怀疑是不是其修改了 JSON.stringify 之后,又修改了其 toString 方法导致再 console 中看起来一样, 原来是 toJSON 在搞鬼的缘故...

JSON.stringify(value[, replacer [, space]])

参数说明

replacer :

  1. function --> 被序列化的值的每个属性都会经过该函数的转换和处理
  2. array --> 只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中

space:指定缩进用的空白字符串

注意事项

如果一个被序列化的对象拥有 toJSON 方法,那么该 toJSON 方法就会覆盖该对象默认的序列化行为

在涉及到重要数据使用 JSON.stringify 时,一定要判断 Object, Array 等对象的原型链上是不是被挂了 toJSON 方法, 不然很有可能导致结果异常

[2014-12-04]

media 中 device-width 和 width 的区别

@media 标签可以说是响应式网页的开发基础,W3C中的定义看这里。其主要由媒体类型(Media Type)媒体特性(Media Query)两部分组成。

Media Type

设定后面规则生效的展示类型,包括all(全部),screen(屏幕),print(页面打印或打邱预览模式)等等

Media Query

设定后面规则生效的条件

注意事项:

  • width 指的是可视区域的宽度,当页面 scale 被设置成 0.5 时,其值会放大一倍。
  • device-width 是设备实际的宽度,不会随着屏幕的旋转而改变,因此并不适合开发响应式网站。

比如 iphone5s 的屏幕分辨率宽为 640,由于 retina 显示策略,当 scale 设置为1的时候,对应的media 中取到的 width 为320,当 scale 设置为0.5的时候,width为640,而 device-width 始终是320。

总结

  1. device-width 只和设备的分辨率有关,一般是分辨率/2,且不会随着手机旋转而改变其值
  2. width 会和 viewport 的 scale 属性相关,为页面的可视区域的宽度
Android 某些版本的Webview,screen.width 取到的是设备的物理分辨率,相应的 device-width 也变成了物理分辨率,因此在 Android 平台下面使用 device-width 是一个坑非常多的解决方案(因为统计器 chrome 浏览器又表现正常)。没有任何意义。
[2014-11-26]

LF 和 LRLF 与正则表达式造成的坑

最近接手的一个项目里面判断 AMD 模块是 define 还是 require 用到了这么一个表达式:

var isRequire = contents.search(/require\s*\(\s*\[(.|\r\n)*\],\s*function\s*\(/gi)!=-1;

各位看官有没有发现其中可能存在的问题?

随着入口文件依赖的模块增多,果断进行了换行重新排版,结果“重排”之后的构建工具失败了..., 对比历史文件,发现原始文件的换行符是 CRLF,而新文件的换行符却是 LF(因为用了 editorconfig,所以我的编辑器的配置都是统一的)。

反复排查发现就是这里的判断出了问题,明明我写的是 require, 却被识别为非 require. 自然后面的处理会出问题。

仔细一看才发现这里对换行符的判断使用了\r\n,这样一来unix 和 mac 下的换行\n就因为缺少了\r导致识别失效,加上\n分支之后程序正常运行,而原来的同学用的是windous系统并不会暴露这个问题。

总结

  1. 项目统一使用 editorconfig 工具配置编码和格式,确保跨平台不出问题
  2. 使用正则表达式匹配换行符的时候,要考虑不同平台下的情况,综合使用 \n\r\n

PS

The Carriage Return (CR) character (0x0D, \r) moves the cursor to the beginning of the line without advancing to the next line. This character is used as a new line character in Commodore and Early Macintosh operating systems (OS-9 and earlier).

The Line Feed (LF) character (0x0A, \n) moves the cursor down to the next line without returning to the beginning of the line. This character is used as a new line character in UNIX based systems (Linux, Mac OSX, etc)

The End of Line (EOL) sequence (0x0D 0x0A, \r\n) is actually two ASCII characters, a combination of the CR and LF characters. It moves the cursor both down to the next line and to the beginning of that line. This character is used as a new line character in most other non-Unix operating systems including Microsoft Windows, Symbian OS and others.

[2014-11-25]

深入浅出 GruntJS

Links

使用 load-grunt-task 来自动 loadNpmTasks

该插件会自动扫描项目的 package.json 文件,解析出配置的全部node_modules, 然后匹配出grunt-开头的全部模块,如下所示:

load-grunt-task 工作原理示意图

随后自动调用 grunt.loadNpmTasks 来加载这些模块,避免在 GruntFile.js 去手动维护这些命令

var pattern = arrayify(options.pattern || ['grunt-*']);
var config = options.config || findup('package.json');
var scope = arrayify(options.scope || ['dependencies', 'devDependencies', 'peerDependencies']);
...
multimatch(names, pattern).forEach(grunt.loadNpmTasks);

API

Grunt 在调用在 GruntFile.js 配置文件时,会自动将 grunt 作为第一个参数传入,这是全部 API 的 root scope

grunt 基本信息相关

  • grunt.option 命令行中传入的 grunt 运行选项
  • grunt.package grunt 所在的包中 package.json 的内容
  • grunt.version === grunt.package.version grunt 的 version

配置

  • grunt.initConfig === grunt.config.init

创建 task

  • grunt.registerTask === grunt.task.registerTask
  • grunt.registerMultiTask === grunt.task.registerMultiTask
  • grunt.renameTask === grunt.task.renameTask

加载 task

  • grunt.loadTasks === grunt.task.loadTasks
  • grunt.loadNpmTasks === grunt.task.loadNpmTasks

警告和错误

  • grunt.warn === grunt.fail.warn
  • grunt.fatal === grunt.fail.fatal
[2014-11-21]

使用 JSDOC 来规范代码注释

代码的注释其实是比较重要又容易被忽视的环节,正好最近接手的项目可能需要进行重构,于是决定从注释开始,采用 JSDOC 来完善注释。

  • JSDoc 的目的是为 JS 应用或者框架建立 API 文档,它假定你希望为 namespaces,classes,methods,method parameters 这些事情生成文档。
  • JSDoc 必须要紧位于需要建立文档的代码前,且以/**开头,其余的任何形式和位置的注释都是无效的(因此必须要对需要输出到文档中方法添加`/`标记**)

关于 @module 和 @exports

module 和 exports 都用于对模块进行注释,两者的区别就是:

  • @module 主要用于注释 CommonJS Module 和 Node.js Module,同时如果** AMD 里面返回的变量名字是exports**的也可以。
  • @expoert 用于返回的变量名不是exports或者module.exports时,显示声明模块返回的对象,主要用于 AMD。
  • @exports, howto-commonjs-modules

针对模块的注释

http://usejsdoc.org/howto-commonjs-modules.html

CommonJS Module (node)

在文件顶部使用@module 标记,jsdoc 会自动识别挂载到 exports 对象上去的属性,包括module.exports =exports =两种方式。

注释 AMD 模块(requirejs)

  1. 使用 @exports 注明返回的对象
  2. 在第一行使用 @module 注明模块名字 xxx,然后再在返回的对象上使用 @alias module:xxx

一些注意事项

主要参考的是 Gooogle javascript code style

Google 不推荐使用下列的 tag:

  • @argument => @param
  • @link => @see
  • @augments Indicate this class uses another class as its "base."
  • @borrows 注明把另一个类的成员当成该类的成员
  • @class 和 @constructor 类似,但是允许后面跟上类的描述,而后者只是标明函数是一个构造函数,介绍是单独出来的
  • @constant 常亮
  • @constructs
  • @default 指明变量的默认值
  • @event 标示具有相同名字的事件触发时,该函数会被调用
  • @example 使用举例,=> @description
  • @field 标示一个对象为非函数,即使它是一个函数
  • @function 标示一个对象为函数,(因为使用函数返回一个对象时,无法区分是对象还是函数)
  • @ignore 让 jsDoc 忽略该变量
  • @inner 类似于@private,是一个函数内部定义的函数
  • @memberOf 标示一个变量引用了一个类的成员
  • @name 强制覆盖 name 属性
  • @namespace
  • @property
  • @public
  • @requires
  • @returns
  • @since
  • @static
  • @version

PS: A list of every tag

[2014-11-14]

《Build Your Own AngularJS》读书笔记

Scopes And Digest

Angular scopes 本身只是纯的对象。通过 dirty-checking 和执行 digest cycle 使得 scope 具备了监测 scope 中数据变化的能力。

$watch 和 $digest 实现对象属性监测

$watch 和 $digest 相当于一枚硬币的两面,一起构成了 digest cycle 的核心目的:反应数据的变化

$watch

该方法对 scope 绑定一个 watcher,当 scope 发生变化时,watcher 会收到通知,通过传递 watch funtion 和 listener function 给$watch来创建 watcher,前者定义了监测的数据片段,后者是对应的回掉函数。

通常使用 watch expression 来代替 watch function 来定义监测的数据片段,比如"user.firstName", Angular 内部会将这些表达式解析并编译成 watch 函数(后面会实现这些功能)。

$digest

该方法遍历绑定到一个 scope 上的全部的 watcher,依次调用他们的 watch 函数(传入当前 scope 作为第一个参数)和对应的 listener 函数。

通过对比一个 wather 的 watch 函数当前的返回值和上一次的返回值,如果发生变化了,那么该 watcher 就是 dirty 状态,对应的 listener 回掉函数需要被调用(传入 newValue, oldValue, scope 作为参数)。

Angular 通过存储 watchFn/listenerFn 函数的对象(watcher)的 last 属性来记录上一次的返回值

  • 为了区分初始值为 undefined 的情况,在初始化的时候,将该 watcher 的 last 属性指向内部定义的一个函数,这样就不会和任何初始值冲突(因为只有当引用了同一个函数时,两个变量才相等,外部变量无法访问到该内部函数也就不会与其相等造成初始值相等冲突)。
  • 为了避免将该内部函数首次作为 oldValue 返回,在调用 listener 的时候会对 oldVlaue 进行判断,如果指向了初始值,则纠正为 newValue 作为 oldValue 返回。

遍历 watcher 的方式看起来挺好,但是当前面的 watcher 关心的值在被 $digest 调用完之后再发生改变(比如在后面的 watcher 的 listener 中改变),则并没有达到 watcher 目的。因此诞生了 Angular 的 dirty check(至少遍历全部的 watcher一遍,如果过程中发生了变化,在遍历完后重新再遍历一遍,直到没有改变为止)。

假设有100个 watcher,那么当第一个 watcher 发生变化时,按照上面的方法需要调用200次 watch 函数,Angular 对此进行了优化,记录了最后一次发生改变的 watcher,如果再一次遍历到最后一次发生改变的 watcher 时,说明已经完成了一轮没有 dirty 状态的 check,就可以提前退出而不必等待遍历结束,此时,当遍历到第二遍的第一个watcher 时,发现就是为最后一次发生变化的 watcher,此时就可以退出,而无需检查剩下的99个 watcher,这个时候就只调用了101次。

结论

  • 在 scope 中设置属性本身并不会造成任何性能影响,如果没有 watcher 来监测其属性,是否在 scope 中存储数据没有任何影响。Angular 并不会遍历 scope 的全部属性。Angular 遍历的是 scope 中的 watchers
  • 在每一个 $digest 过程中, 每一个 watch 函数都会被调用,因此需要注意 watcher 的数量以及 watch 函数的性能。
[2014-11-12]

ngnix 使用 php-fpm 来运行php

参考教程 Install LEMP Server (Nginx, MariaDB, PHP) On CentOS 7

要点

  • /etc/php.ini 软链到了 /usr/local/php5/etc/php.ini 而实际上 php5的目录不存在,是/usr/local/php/etc/php.ini,删掉原来的软链再 cp 。直接 cp 会报not writing through dangling symlink错误。
  • 默认的PHP-FPM 运行在9000端口,配置文件位于 /etc/php-fpm.d/www.conf需要将文件中的 user 和 group 设置成 ngnix 中使用的 user/group, 最好是文件所在目录的拥有者,在 centos 上尝试了 N 次一直都是Primary script unknown
  • 使用 unix socket 能提高通信性能,在 fpm 配置文件中设置 listen = /var/run/php-fpm/php5-fpm.sock,对应的 nginx 配置文件中设置 fastcgi_pass unix:/var/run/php-fpm/php5-fpm.sock;systemctl restart php-fpm重启 php-fpm 和 ngnix 服务。

正确设置 nginx 和 php-fpm 的 user group 属性

nginx 和 php-fpm 的 user 和 group 最好设置成文件所在目录的拥有者(比如 www),两者必须一致。否则,nginx 会报 403 错误,PHP-FPM 会报Primary script unknown(该错误也有可能是 fastcgi_param SCRIPT_FILENAME 没有错误导致的,因此调试了半天,而事实上是没有相应目录的访问权限导致的!!!!)。

不同版本的 PHP 对 require 的路径会做不同处理

我把 Conf 错误拼写成了 conf ,在 mac 上(PHP 5.5.14)会自动处理大小写关系,正常完成程序加载,而在 centos 上(未装 PHP 直接用的 php-fpm)会直接报错。

添加 $_SERVER['PATH_INFO'] 环境变量

在Apache中, 当不加配置的时候, 对于PHP脚本, AcceptPathInfo是默认接受的, 会自动设置PATH_INFO。

但是 Nginx 默认的配置文件对 PHP 的支持是很基础的,会提示404,找不到文件出错(因为 ngnix 拦截了到 php 前的大量请求)。这对于一些使用PATH_INFO来传递关键信息的PHP框架来说(比如Kohana, Thinkphp), 简直是致命的. 百度音乐后端使用的 Dapper 框架也正是使用了 PATH_INFO 来进行路由分发,导致出来的一直是默认的首页。

首先将 location ~ .php$ { 改成 location ~ .php {, 交给 php 来处理。然后是添加 PATH_INFO 环节, 有两种方法:

方法一,通过 PHP 的 fix_pathinfo(将 PATH_INFO=/index.php/helloworld 修正为 PATH_INFO=/helloworld)来实现:

  1. 打开 php.ini 中 cgi.fix_pathinfo 配置项, PHP 会根据 CGI 规范来检查 SCRIPT_FILENAME 中哪些是访问脚本和 PATH_INFO, 并进行相应设置。例如 /index.php/helloworld 的 PATH_INFO 字段为 helloworld。
  2. nginx 配置文件中添加 FASTCGI_PARAM fastcgi_param PATH_INFO $fastcgi_script_name;

方法二,在 nginx 中使用正则匹配出 PATH_INFO,直接设置fastcgi_param PATH_INFO $var_path_info;,此时,不再需要 PHP 来进行 pathinfo 的 FIX。详细参考此文 Nginx(PHP/fastcgi)的PATH_INFO问题在 Dapper 框架中,使用第二种更简洁,具体配置如下:

location ~ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_param SCRIPT_FILENAME $document_root/webroot/index.php$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_script_name;
    include fastcgi_params;
}

session 存储目录权限问题导致 phpMyAdmin 无法登录

  1. 进入 phpMyAdmin 登录后,发现总是重定向回登录页面且带了个 token: index.php?token=xxx, 无法正常登录
  2. 改用 HTTP 验证的方式能正常登录但是进去之后的操作都报 error: Token mismatch, 发现需要设置 session.save_path
  3. 照着修改了/etc/php.ini之后发现 phpinfo() 输出的 session.save_path 还是/var/lib/php/session, 为何 php-fpm 并没有使 /etc/php.ini 中的该项配置生效 ?只好先 chmod 777 -R /var/lib/php 使得该目录具有可访问权限,果然解决了无法登录的问题
  4. 随后发现使用php-fpm时会在/etc/php-fpm.d/www.conf文件里面复写部分 php.ini 中的配置,里面有一行 php_value[session.save_path] = /var/lib/php/session,因此 phpinfo()出来的并不是 /etc/php.ini中的配置。 参考此文
  5. 此外还碰到了 mysql 允许 locahost 匿名登录导致本地的非 root 帐号无法登录的问题,毕设的时候就碰到过

ngnix 配置

server {
    listen 8000;
    server_name localhost;

    root /Users/wy/www;
    index index.html index.htm index.php;

    location ~ \.php$ {
        # fastcgi_pass 127.0.0.1:9000;
        fastcgi_pass  unix:/var/run/php-fpm/php5-fpm.sock;
        fastcgi_index index.php;
        # 该处一定要是 $document_root$fastcgi_script_name,否则报
        # FastCGI sent in stderr: "Primary script unknown" 错误
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }                                                                                                      
}

unix socket

UNIX Domain Socket是在socket架构上发展起来的用于同一台主机的进程间通讯(IPC),它不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

UNIX Domain Socket有SOCKDGRAM或SOCKSTREAM两种工作模式,类似于UDP和TCP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱

参考资料: Linux下的IPC-UNIX Domain Socket Performance of unix sockets vs TCP ports

[2014-11-11]

使用 github 的 webhooks 来实现持续集成

Links

配置的 POST 如果使用了 applicaiton/json 的 Content Type, 则在 express 中仅需读取 req.body 对象即可

{ ref: 'refs/heads/master',
  before: 'a8121f1e5bdc46accff045d06448335b43d4475a',
  after: 'c385358f429cdb06fb7ffaddf3264f31ce836324',
  created: false,
  deleted: false,
  forced: false,
  base_ref: null,
  compare: 'https://github.com/wangyuzju/labs/compare/a8121f1e5bdc...c385358f429c',
  commits: 
   [ { id: 'c385358f429cdb06fb7ffaddf3264f31ce836324',
       distinct: true,
       message: 'test github hooks',
       timestamp: '2014-11-10T16:36:41+08:00',
       url: 'https://github.com/wangyuzju/labs/commit/c385358f429cdb06fb7ffaddf3264f31ce836324',
       author: [Object],
       committer: [Object],
       added: [],
       removed: [],
       modified: [Object] } ],
  head_commit: 
   { id: 'c385358f429cdb06fb7ffaddf3264f31ce836324',
     distinct: true,
     message: 'test github hooks',
     timestamp: '2014-11-10T16:36:41+08:00',
     url: 'https://github.com/wangyuzju/labs/commit/c385358f429cdb06fb7ffaddf3264f31ce836324',
     author: 
      { name: 'wangyuzju',
        email: 'wangyu0248@gmail.com',
        username: 'wangyuzju' },
     committer: 
      { name: 'wangyuzju',
        email: 'wangyu0248@gmail.com',
        username: 'wangyuzju' },
     added: [],
     removed: [],
     modified: [ 'labs/routes/index.js' ] },
  repository: 
   { id: 26399364,
     name: 'labs',
     full_name: 'wangyuzju/labs',
     owner: { name: 'wangyuzju', email: 'wangyu0248@gmail.com' },
     private: false,
     html_url: 'https://github.com/wangyuzju/labs',
     description: 'the source code of labs.hellofe.com',
     fork: false,
     url: 'https://github.com/wangyuzju/labs',
     forks_url: 'https://api.github.com/repos/wangyuzju/labs/forks',
     keys_url: 'https://api.github.com/repos/wangyuzju/labs/keys{/key_id}',
     collaborators_url: 'https://api.github.com/repos/wangyuzju/labs/collaborators{/collaborator}',
     teams_url: 'https://api.github.com/repos/wangyuzju/labs/teams',
     hooks_url: 'https://api.github.com/repos/wangyuzju/labs/hooks',
     issue_events_url: 'https://api.github.com/repos/wangyuzju/labs/issues/events{/number}',
     events_url: 'https://api.github.com/repos/wangyuzju/labs/events',
     assignees_url: 'https://api.github.com/repos/wangyuzju/labs/assignees{/user}',
     branches_url: 'https://api.github.com/repos/wangyuzju/labs/branches{/branch}',
     tags_url: 'https://api.github.com/repos/wangyuzju/labs/tags',
     blobs_url: 'https://api.github.com/repos/wangyuzju/labs/git/blobs{/sha}',
     git_tags_url: 'https://api.github.com/repos/wangyuzju/labs/git/tags{/sha}',
     git_refs_url: 'https://api.github.com/repos/wangyuzju/labs/git/refs{/sha}',
     trees_url: 'https://api.github.com/repos/wangyuzju/labs/git/trees{/sha}',
     statuses_url: 'https://api.github.com/repos/wangyuzju/labs/statuses/{sha}',
     languages_url: 'https://api.github.com/repos/wangyuzju/labs/languages',
     stargazers_url: 'https://api.github.com/repos/wangyuzju/labs/stargazers',
     contributors_url: 'https://api.github.com/repos/wangyuzju/labs/contributors',
     subscribers_url: 'https://api.github.com/repos/wangyuzju/labs/subscribers',
     subscription_url: 'https://api.github.com/repos/wangyuzju/labs/subscription',
     commits_url: 'https://api.github.com/repos/wangyuzju/labs/commits{/sha}',
     git_commits_url: 'https://api.github.com/repos/wangyuzju/labs/git/commits{/sha}',
     comments_url: 'https://api.github.com/repos/wangyuzju/labs/comments{/number}',
     issue_comment_url: 'https://api.github.com/repos/wangyuzju/labs/issues/comments/{number}',
     contents_url: 'https://api.github.com/repos/wangyuzju/labs/contents/{+path}',
     compare_url: 'https://api.github.com/repos/wangyuzju/labs/compare/{base}...{head}',
     merges_url: 'https://api.github.com/repos/wangyuzju/labs/merges',
     archive_url: 'https://api.github.com/repos/wangyuzju/labs/{archive_format}{/ref}',
     downloads_url: 'https://api.github.com/repos/wangyuzju/labs/downloads',
     issues_url: 'https://api.github.com/repos/wangyuzju/labs/issues{/number}',
     pulls_url: 'https://api.github.com/repos/wangyuzju/labs/pulls{/number}',
     milestones_url: 'https://api.github.com/repos/wangyuzju/labs/milestones{/number}',
     notifications_url: 'https://api.github.com/repos/wangyuzju/labs/notifications{?since,all,participating}',
     labels_url: 'https://api.github.com/repos/wangyuzju/labs/labels{/name}',
     releases_url: 'https://api.github.com/repos/wangyuzju/labs/releases{/id}',
     created_at: 1415545804,
     updated_at: '2014-11-10T04:10:18Z',
     pushed_at: 1415608609,
     git_url: 'git://github.com/wangyuzju/labs.git',
     ssh_url: 'git@github.com:wangyuzju/labs.git',
     clone_url: 'https://github.com/wangyuzju/labs.git',
     svn_url: 'https://github.com/wangyuzju/labs',
     homepage: null,
     size: 0,
     stargazers_count: 0,
     watchers_count: 0,
     language: 'JavaScript',
     has_issues: true,
     has_downloads: true,
     has_wiki: true,
     has_pages: false,
     forks_count: 0,
     mirror_url: null,
     open_issues_count: 0,
     forks: 0,
     open_issues: 0,
     watchers: 0,
     default_branch: 'master',
     stargazers: 0,
     master_branch: 'master' },
  pusher: { name: 'wangyuzju', email: 'wangyu0248@gmail.com' },
  sender: 
   { login: 'wangyuzju',
     id: 1708809,
     avatar_url: 'https://avatars.githubusercontent.com/u/1708809?v=3',
     gravatar_id: '',
     url: 'https://api.github.com/users/wangyuzju',
     html_url: 'https://github.com/wangyuzju',
     followers_url: 'https://api.github.com/users/wangyuzju/followers',
     following_url: 'https://api.github.com/users/wangyuzju/following{/other_user}',
     gists_url: 'https://api.github.com/users/wangyuzju/gists{/gist_id}',
     starred_url: 'https://api.github.com/users/wangyuzju/starred{/owner}{/repo}',
     subscriptions_url: 'https://api.github.com/users/wangyuzju/subscriptions',
     organizations_url: 'https://api.github.com/users/wangyuzju/orgs',
     repos_url: 'https://api.github.com/users/wangyuzju/repos',
     events_url: 'https://api.github.com/users/wangyuzju/events{/privacy}',
     received_events_url: 'https://api.github.com/users/wangyuzju/received_events',
     type: 'User',
     site_admin: false } }
[2014-11-10]

TDD BDD DDD 的一些特性和适用场合

TDD: Test Drive Development

过程:先针对每个功能点抽象出接口代码,然后编写单元测试代码,接下来实现接口,运行单元测试代码,循环此过程,直到整个单元测试都通过。

不适用:业务模型复杂、内部模块之间的相互依赖性非常强的项目。(导致程序员在拆分接口和写测试代码的时候工作量非常大)

BDD: Behavior Drive Development

实际上BDD可以看作是对TDD的一种补充,考虑它的行为,也就是说它应该如何运行,然后抽象出能达成共识的规范。

DDD: Domain Drive Design

领域驱动开发。它关注的是Service层的设计,着重于业务的实现,因此不可避免的以贫血模式为基础而存在。

它最大的特别是将分析和设计结合起来,不再使他们处于分裂的状态,这对于我们正确完整的实现客户的需求,以及建立一个具有业务伸缩性的模型,是有很大帮助的。

[2014-11-10]

使用断点来审查JS数据结构

开发网页复制系统剪切板图片功能的时候,通过绑定 paste 事件然后输出 event.clipboardData 对象来进行调试,但是发现之前输出的对象值等我点击展开之后,都变成空了

原来是复制事件触发的时候,console.log 出来的只是一个对象的引用,chrome 的 console 输出不会遍历其余属性缓存起来,因此当后续点击展开按钮的时候,该对象实际上已经不存在了(因为 paste 操作已经完成),也就没办法再展开对象属性了。

那么怎么调试异步被销毁的数据结构呢?打个 chrome 断点就能解决该问题

[2014-11-07]

网页实现复制系统剪切版中图片功能

使用 github 写博客的一个问题就是图片上传不太方便,于是想要做一个自己的图片服务,方便地将剪切板中的图片上传到服务器并返回一个可以使用的外部地址,以方便在博客中插入图片。只要解决了如何在网页中复制图片即可

这篇 HTML5 JavaScript Pasting Image Data in Chrome 11年的文章就已经做到了,主要是利用了 FileReader 来加载图片内容。

实现参考

  • [将 dataURL 格式的图片发送给服务器]http://stackoverflow.com/questions/6850276/how-to-convert-dataurl-to-file-object-in-javascript()

背景技术

Blob 对象:一个Blob对象就是一个包含有只读原始数据的类文件对象.Blob对象中的数据并不一定得是JavaScript中的原生形式.File接口基于Blob,继承了Blob的功能,并且扩展支持了用户计算机上的本地文件. 创建Blob对象的方法有几种,可以调用Blob构造函数,还可以使用一个已有Blob对象上的slice()方法切出另一个Blob对象,还可以调用canvas对象上的toBlob方法.

test

FormData 对象:利用FormData对象,你可以使用一系列的键值对来模拟一个完整的表单,然后使用XMLHttpRequest发送这个"表单".

After you've obtained a File reference, instantiate a FileReader object to read its contents into memory. When the load finishes, the reader's onload event is fired and its result attribute can be used to access the file data.

FileReader includes four options for reading a file, asynchronously:

  1. FileReader.readAsBinaryString(Blob|File) - The result property will contain the file/blob's data as a binary string. Every byte is represented by an integer in the range [0..255].
  2. FileReader.readAsText(Blob|File, opt_encoding) - The result property will contain the file/blob's data as a text string. By default the string is decoded as 'UTF-8'. Use the optional encoding parameter can specify a different format.
  3. FileReader.readAsDataURL(Blob|File) - The result property will contain the file/blob's data encoded as a data URL.
  4. FileReader.readAsArrayBuffer(Blob|File) - The result property will contain the file/blob's data as an ArrayBuffer object.
[2014-11-07]