ichuan.net

自信打不死的心态活到老

glob.js

熟悉 linux 命令行环境的人对 glob 应该不陌生,比如有时会用到的 ls jquery.*.jsps -fe | grep [h]ttpdls *.[pP][nN][gG]。glob 也就是通配符匹配,主要由 *, ?, [] 及转义符 \ 组成,用来做匹配很方便。

最近需要在 js 中用到 glob,找了一些库都不太满意,于是自己写了个,暂时用着还行:https://github.com/ichuan/glob.js

可以用 bower 安装:bower install glob.js,能工作在 node 或浏览器环境下。用法如下:

匹配单个字符

glob.match('ab?d', 'abcd') // ok
glob.match('ab\\?d', 'ab?d') // ok

匹配多个字符

glob.match('ab*d', 'abcaeg@3d') // ok
glob.match('ab\\*d', 'ab*d') // ok

匹配一个分组

glob.match('ab[cd]e', 'abce') // ok

具体用法参见 README

解决 angular.js ngList 指令 bug

ngList 指令

在不使用 ngList 指令时,假如需要实现一个 tags 功能。用户在界面上用逗号分隔输入一些 tag,然后在提交时你需要先在 controller 中人工把该字串分隔成一个数组。类似:$scope.tags = $scope.tags.split(',')

假如在输入控件上使用了 ngList 指令,这件事就不用你操心了,angular 会随时把用户输出分隔后再保存到 $scope 中,所以你使用 $scope.tags 时它就已经是个数组了。

bug

ngList 有个悬而未决的 bug:UI => $scope 没问题,但 $scope => UI 会把变量数组用逗号合并后展示。ngList 保存用户输入到变量时可以指定分隔符(如:ngList="/;/"),但显示变量内容时却永远使用逗号拼接数组为字串,检查源码后发现是硬编码的:

enter image description here

可以看看这个 demo 。在 Test1 的文本区域输入内容,每个一行。可以看到 $scope.names 正确保存为了数组。但在 Test2 中,$scope.names2 原先有内容,显示时却以逗号分隔,而不是 ngList 指定的换行符。

解决方法

这个 bug 由来已久,官方一直不能确定最佳修改方式而留着它。在我们项目中,最简单的解决方法就是修改 angular 源码,但这种方法太暴力。最好能在外部修改。但 angular 源码打包后都隐藏了内部实现,没法 hack ngList 的代码;指令不支持继承,也无法基于 ngList 写个新的指令。

幸好 angular 指令支持 stacking,我们可以写个新的指令,和 ngList 放到同一控件上。由于这样就和 ngList 共享同一 controller,可以在 controller.$formatters 上做手脚,使浏览器显示出我们需要的格式。代码如下:

angular.module('testApp')
  .directive('myList', function () {
    return {
      restrict: 'A',
      require: 'ngModel', 
      link: function postLink(scope, element, attrs, ctrl) {
        var match = /\/(.*)\//.exec(attrs.ngList)
          , separator = match && match[1] || attrs.ngList || ','
          , escapes = {'\\n': '\n'}
          , val;
        separator = escapes[separator] || separator;
        ctrl.$formatters.unshift(function () {
          val = ctrl.$modelValue;
          return angular.isArray(val) && val.join(separator) || undefined;
        });
      }
    };
  });

说明:

  1. 使用 require: 'ngModel'可以使 link 函数获得第四个参数:共享的 controller
  2. ngList 使用的分隔符是正则,所以对换行、tab等这类转义符要特殊处理
  3. $formatters 会被 angular 从后往前调用,所以我们用 unshift 把自己加到第一个,也就最后一个执行了

再来看个例子:Test2 中可以正确以换行显示了;Test3 中换其它分隔符也可行

javascript 数组的稳定性排序

javascrpt 数组的排序

Array.sort 是 javascript 数组的排序方法,默认按字串形式排序,可以传入一个自定义的排序函数 function (a, b) {},按返回值决定元素排序后的位置。

enter image description here

稳定性排序

指的是当数组里两个元素相同时,排序后它们的相对位置不应该发生改变。以上面的图片为例,数组有两个 3,假如排序后两个 3 的位置发生了调换,这个排序方法就是不稳定的。

ECMAScript 规范里有这么一句:

The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their original order)

也就是规范并没有规定各个实现者要实现稳定的排序,这就导致了一些问题。有人做过测试,得出了 Array.sort 在各浏览器中的稳定性列表:

enter image description here

很惊讶地发现 IE6 都可以达到稳定性排序,而最新版本的 Chrome 中竟然还是不稳定排序

Chrome 中重现

今天很偶然的遇到了 Chorme 中不稳定排序的 bug(需要浏览器环境,可在 Chrome Console 中执行):

enter image description here

在排序中我使用了返回值 0,即表示两个元素相等。期待的结果应该是 y 数组各元素位置不变,但明显第一个元素的就发生了变化。

解决方法

解决方式是换个排序方法,不用 Chrome 原生的 Array.sort。网上普遍使用一个 merge sort 作为替代。算法一直都是我的痛点,我想换个更简单点的解决思路。既然不稳定排序只发生在两个元素相等时,那如果我们记录每个元素原先的位置,在两个元素相等时利用原先位置做比较,不就是修复了这个 bug 了么?于是有了以下自定义的 Array.stableSort 方法:

Array.prototype.stableSort = function (fn) {
  if (!fn) {
    // 默认排序还按原生的
    return this.sort();
  }
  // 用新数组记录位置
  var newArr = this.map(function (i, j) { return {i:i, j:j}; });
  return newArr.sort(function (a, b) {
    result = fn(a.i, b.i);
    if (result === 0) {
      // 按原先元素位置排序
      return a.j - b.j;
    }
    return result;
  }).map(function (i) { return i.i; });
};

再来看下效果:

enter image description here

实现稳定排序了。至于为什么那个 DOM 结构的数组会触发这个 bug,暂时还是未知。

参考链接

  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
  • http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.11
  • http://stackoverflow.com/a/3027715/265989
  • https://code.google.com/p/v8/issues/detail?id=90
  • http://stackoverflow.com/questions/1427608/fast-stable-sorting-algorithm-implementation-in-javascript

我们的博客

去年年初,我遇到了萌萌。到现在,经历了相识、相知,还有 10 天就要结婚了。过去的一年半时间,我的生活发生了翻天覆地的变化,萌萌是我成长的催化剂,也成为了我生命的意义。

去年圣诞节想送萌萌一个礼物,于是选了个域名:52mandy.com,在 tumblr 上开始写博客,记录我们之间生活中一些细小的快乐事情。

萌萌很高兴,我们开始一起写博客。但 tumblr 互动性不好,可定制性还不够,还没有相册功能,于是我们决定自己做一个。在 djblog 的基础上,我们自己设计了页面(草稿),我加入了相册功能,最终做出来了满意的样子: 52mandy.com

欢迎访问我们的博客!

120行代码的“web shell”

工作是无聊的,为工作而写代码的过程是枯燥、令人气馁的。但工作又是必须的,该做的工作是要做的。我的权衡之策就是在条件允许的情况下,把一些新的方式、新的技术用到工作代码中,这样在编码的过程中会学到新知识并应用起来,编码的过程也不会都是些无聊的体力劳动了。查阅文档和研究 bug、寻求较完美的解决方案,这些经历都会使工作变得有趣起来,最终的结果也会让人充满成就感。

最近一个项目需要这样一个功能:把后台的不定期的输出反馈到 web 界面上。我首先想到了最简单是在前端用 ajax 轮询,不过这样做就太土了。于是想到了用 websocket 实现后台实时推送到前台。正好这个项目前台是用 express 来做的(之前一直用 django,这次也是图新鲜第一次用 nodejs),自然而然可以用 socket.io 来做 websocket。

socket.io 解决的问题是推送。在 websocket 出现前 http 推送主要用的是 comet 技术:

comet

当 websocket 不可用时,socket.io 中可以利用 flash socket, iframe, jsonp 等方法实现准实时的客户端服务器双向通信。

下面以做一个 "web-shell" 应用为例演示 socket.io 功能和用法。美化后的 html 代码和 js 代码总和大约 120 行。

此 "web-shell" 非彼 "web-shell",我们要做的是一个基于 web 的 shell,最终截图如下:

enter image description here

因为在浏览器可以输入命令发给服务器,服务器有输出了可以实时推送给客户端,所以使用 socket.io 可以实现。

首先新建个目录,安装 socket.io:

mkdir web-shell && cd web-shell
npm init
npm install socket.io

然后按照预想的样子编写 index.html 如下:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="/socket.io/socket.io.js"></script>
    <style>
      /* 篇幅原因省去了 css 代码,可以在尾部下载 */
    </style>
  </head>
  <body>
    <pre id="console"></pre>
    <input id="cmd" />
    <script>
      // 这块留出来写控制流程
    </script>
  </body>
</html>

然后参考 socket.io 的示例代码,编写服务端的 app.js:

var app = require('http').createServer(handler)
  , io = require('socket.io').listen(app)
  , fs = require('fs')
  , spawn = require('child_process').spawn;

app.listen(8080);

function handler (req, res) {
  fs.readFile(__dirname + '/index.html', function (err, data) {
    res.writeHead(200);
    res.end(data);
  });
}

io.sockets.on('connection', function (socket) {
  var shell = spawn('bash')
    , output = function (msg) {
        socket.emit('output', msg.toString());
      };

  shell.stdout.on('data', output);
  shell.stderr.on('data', output);
  shell.on('close', function () {
    output('Exit');
    socket.disconnect(true);
  });

  socket.on('input', function (data) {
    shell.stdin.write(data);
  });

  socket.on('disconnect', function () {
    shell.kill('SIGKILL');
  });
});

io.sockets.on('connection' 这一行是客户端建立连接后会执行的逻辑。对每个客户端,创建一个新的子进程(spawn('bash')),并且在客户端发来命令时(socket.on('input'))把命令传到 bash 的标准输入;在 bash 进程有输出时(shell.stdout.on('data')shell.stderr.on('data'))把输出反馈给客户端(socket.emit('output'))。最后一点,在 bash 进程退出时(shell.on('close'))断开客户端连接。

再看客户端逻辑,把如下代码加入 index.html 的 <script></script> 标记间:

var elConsole = document.getElementById('console')
  , elCmd = document.getElementById('cmd')
  , socket = io.connect(location.host);

var escapeHtml = function (html) {
  return html.replace(/&/g, '&amp;')
             .replace(/>/g, '&gt;')
             .replace(/</g, '&lt;');
};

var output = function (data) {
  elConsole.innerHTML += escapeHtml(data);
  elConsole.scrollTop = 9e5;
};

socket.on('output', output);

socket.on('connect', function () {
  output('连接成功\n');
  socket.emit('input', 'cat /etc/iss*\n');
});

socket.on('disconnect', function () {
  output('\n连接已断开\n');
});

elCmd.addEventListener('keypress', function (e) {
  if (e.keyCode === 13) {
    var data = elCmd.value + '\n';
    output('$ ' + data);
    socket.emit('input', data);
    elCmd.value = '';
  }
});

首先创建连接(io.connect(location.host)),然后在连接后先发送一个查看发行版信息的命令(cat /etc/iss*\n)。监听输入控件的 keypress 事件,在用户敲回车时把输入发送给服务端。在服务端有消息发回时更新展示控件(socket.on('output'))。

完整代码见 https://github.com/ichuan/yc-lib/tree/master/node/web-shell这里有下载。

还有改进空间,这里是个加入了命令历史记录和自动清屏功能的版本。当我想做 tab 补全功能时发现,bash 的有些输出在 tty,而不在 stdout 和 stderr,所以有些输出是没法在网页端看到的,例如 tab 补全,ssh 登录时的输密码提示。ssh 登录的输入也是在 tty,所以只有加入了公钥自动登录后,才可以在网页端 ssh 到其它机器。