Mongodb是时下非常流行的数据库,而Rockmongo可以说是最普及的web版gui了。但是Rockmongo在显示utf-8中文数据时一直有问题,最新版本v1.1.2仍然没有解决。今天专门腾了点时间debug并将其解决,将过程记录一下供大家参考。
问题现象
1、插入一条包含中文的数据
2、保存之后,中文数据不见了
DEBUG
审查元素发现,从服务器端返回时中文已经不见了,排除问题在浏览器端的可能性
最近项目上有一个需求,要对存储在MongoDB中的用户行为数据定期进行统计分析。
先使用PHP实现原型,发现因为数据量很大,大量时间都花在MongoDB服务器和Web服务器之间的数据交换上。考虑到这一点,必须在MongoDB服务器上进行本地计算,将结果保存起来,再使用PHP访问并展示给用户。
查阅文档得知,MongoDB可以执行JS脚本,这样思路就清楚了,用JS脚本实现统计的功能,再用crontab定期执行。
现在和大家分享一下在完成这个任务的过程中,遇到的一些问题和解决思路。(必须要说的是,MongoDB的官方文档对服务器端JS编程的文档极度缺乏,很多命令都是通过Google才找到的。)
输出字符串:
print( 'Hello World' );
输出对象:
var obj = { 'key' : 'value' }; printjson( obj );
在Mongo JS Shell中,切换当前数据库的命令是:use xxx,这是一个magic helper。遗憾的是,在JS脚本中没有use可用。
如果运行mongo时没有加上–nodb参数,那么会自动connect到指定的服务器和数据库,并把句柄保存为db。如果运行时没有指定服务器会默认连接本机127.0.0.1,如果没有指定数据库会连接到test数据库。
你可以用conncet命令重新发起连接:
var userDB = connect( '123.123.123.123/user' ); userDB.user.find();
收集自:http://ss64.com/bash/syntax-keyboard.html
注意:如果要在mac的终端下使用Alt组合键,需要在终端的偏好设置里选中“使用option键作为meta键”
其实nginx的conf结构本来设计的不错,但是从默认安装的配置文件离实际可用状态还有一些距离。比如设置fastcgi、分离server部分到多个文件等。
一般来说,部署完毕之后conf已经是面目全非了。众多参数缺乏有效的管理组织方式。如果有多个站点,那么各个站点的配置文件中可能还存在大量重复。
inginx.conf(github)项目诞生的目的是:明晰nginx配置文件的结构,让维护多个站点的配置、部署新站点变的更加简单。
注意:请先备份nginx/conf目录
1、自动部署:下载自动部署脚本deploy.sh并运行,脚本会自动检测nginx/conf的位置并部署完成(需要wget和unzip)。
#全自动部署
./deploy.sh
#也可以指定nginx/conf的位置
./deploy.sh /etc/nginx/conf
2、手动部署:下载zip包后解压缩,然后将conf目录复制到nginx/conf目录。
cp conf /usr/local/nginx/conf
1、进入conf/servers目录,将server.example复制一份,命名为自己的站点(注意后缀应为.conf) Continue reading 用inginx.conf分级管理nginx配置文件
我现在的开发环境,php、php-mongo、php-mysql、php-redis、xdebug等都是通过macports安装的。一直使用都没有问题,直到今天编辑php.ini的时候发现,这些extension居然都没有在php.ini中定义。奇怪了,那它们是怎么加载的呢?
首先怀疑我编辑的php.ini不是php实际使用的配置文件。通过查看phpinfo(),在Configure Command中可以确认–with-config-file-path为/opt/local/etc/php54,编辑的php.ini没有问题。
既然php.ini中没有定义,而extension又实际上被加载了。那么可以初步判断,通过macports安装的php,extension的相关配置是存在php.ini之外的一个地方,那么究竟在哪呢?
继续分析phpinfo(),发现这么一条–with-config-file-scan-dir=/opt/local/var/db/php54。进入这个目录一看,mongo.ini、mysql.ini、redis.ini、xdebug.ini等都在这里。于是乎真相大白,原来extension相关的配置文件都被分离到这里了。
究其原因,macports这样做是为了方便维护extension。好处如下:
1、extension的配置与php.ini隔离,方便维护(直接修改php.ini会引发各种问题)
2、各个extension的配置文件相互隔离,互不影响(安装卸载只要添加删除文件即可)
MVC架构中的model层主要负责和数据直接交互,如果没有使用ORM框架,那么很多原子操作(CRUD等)需要自己实现。一般来说一个系统会有多个Model,每一个Model都需要重复实现这些原子操作,而这些代码往往除了表名外都是一样的:
class User { public static function findById ( $id ) { return DBProxy::getDb( 'user' )->findOne( array( '_id' => new MongoId( $id ) ) ); } } class Video { public static function findById ( $id ) { return DBProxy::getDb( 'video' )->findOne( array( '_id' => new MongoId( $id ) ) ); } }
当我们增加一个Model类的时候,比如Article,就要从其他类复制这些原子操作(比如find、findOne、findById、update、save、count等等)过来。如果有一个方法需要变更,比如增加一个参数或者添加log日志,就得修改所有Model类中的同名方法。这实在是一件很痛苦很无奈的事情。
《重构》一书告诉我们,重复的代码是坏味道,那么我们该怎么改良呢? Continue reading 在不同php版本中实现model层重复代码的去除
刚在mac上配置好php环境,运行项目代码就出现错误:
Parse error: syntax error, unexpected end of file in xxx.php on line 21
检查出错的文件,21行是该文件的最后一行,是一句很简单的php代码,没有发现任何问题。于是我准备用排除法来定位问题,将该行删除后刷新页面,错误信息变成:
Parse error: syntax error, unexpected end of file in xxx.php on line 20
错误行变成20行,而该行是一个html标签。这样就很奇怪了,非php的代码怎么会引发php的语法错误?
再继续排查,该文件中还有一段php代码:
<?php foreach ( $tags as $tag ) { ?><a href="<?= $tag[ 'url' ] ?>"><?= $tag[ 'name' ] ?></a>
虽然觉得不太可能,但还是将这段不能再简单的代码删除测试一下,居然不报错了!
继续测试,结果发现只要把循环体彻底删除,就不会报错。而只要保留循环体,哪怕是?><?,也会报错。脑中浮现出之前google时见过的一个诡异问题,莫非⋯⋯
打开php.ini,搜索short_open_tag,好吧,不知道出于什么目的,在用port装的php54里这个值居然是Off!貌似我用yum、apt-get乃至手动编译php,这个值都是On啊。
果断改之,重启php,问题顺利解决。
现在很多人都有国外的VPS了,那么我们可以利用它建立SSH隧道代理来实现翻墙,解决很多国外网站上不了或间歇性不能访问的问题。
1、首先到http://nihilex.com/secret-socks去下载SecretSocks并安装。
2、运行之后,在SSH hostname中输入自己vps的ip,在Port number中输入ssh的端口(如果你没有改过的话是22),然后在Username中输入可用ssh登录的用户,在Password中输入密码,然后点击Connect按钮。如果看到绿色的对号,说明连接成功了。
3、配置浏览器中的代理,不过我的chrome和safari都自动发现了代理,不需要特别配置就可以使用了。如果你使用的浏览器没有,那么就手动设置一下代理服务器为127.0.0.1:9999。
需要注意的事项:只在需要翻墙的时候Connect,不需要翻墙时记得Disconnect,否则访问国内的网站会很慢或打不开,而且VPS的流量一般是有限的,需要节约使用。
不知从何时起,我开始记录自己在各个游戏上的经验心得,写了一篇又一篇的教程。将自己脑中思索已久的想法、探索的经历、实践过的经验写出来,实在是一件很快乐的事情。为了这种快乐,我宁愿花无数个小时去码字,排版,配图……
我不禁要问自己,这种快乐的来源是什么?
人的本性是自私的,我也不会例外。为什么把自己辛苦得来的经验分享出来,会让自己得到快乐呢?是为了获取别人敬仰的目光,满足自己的虚荣心吗?还是好为人师,想体验那种优越感呢?
我找到了另一个答案:为了证明自己的存在。
那些分享经验心得的文章,证明我曾在这些领域努力过,证明我在那段时间里存在过。这些文章可能会帮助到一些人,也可能会误导一些人,但我的思考和实践会对他们有一定的参考价值,可能会帮助他们解决问题,减少他们犯同样错误的可能性,或者给他们不同思路的启迪。
在一个人的生命中留下你的足迹,使他的生活变得更美好(节省时间、减少痛苦、欣赏美丽……),哪怕施加的影响是极其微小的,这就是你存在的价值。如果你能持续这种影响力,那么你的生命就是不朽的,你就在世间永存。
人类之所以有别于动物,就是可以通过语言和文字将自己的经验和思想传承下来,而子孙后代通过学习可以快速掌握几代人穷尽一生才探索到的知识。一个人的生命是有限的,而其人生的价值就在于留给后人的东西。
相信玩游戏的朋友们都知道Save / Load大法,其实人生也是一样。
我永远无法知道明天会发生什么事情,自己的生命可能在任何一秒钟终结。而当我死去的时候,我的肉体,拥有的能力,掌握的知识,以及脑中的各种思想都将不复存在。这个世界上我所存在的证据,就是我创造过的一切。我写的文章,我画的画,我写过的程序……
我不希望在死去前那一刻,想到自己还有很多想法没有说出来,还有很多事情没来得及告诉别人……那是多么绝望和痛苦的感觉?那种感觉就像写文章写了几千字,突然停电了才想起没存盘一样。
所以我工作时会建立文档,记下只有自己掌握的知识和工作流程,在必要时可以随时交接。写代码时要经常重构,保证可读性并把自己的思路注释清楚,保证自己可以随时离开。
当我把自己知道和想到的东西都写下来时,我心中感到无比地踏实和满足。就像刚刚离开一个存盘点,在未知的领域迈出新的一步一样,我不惧怕包括死亡在内的任何危险,因为我的经历和成就已经保存下来了。
“随时存盘”是一个好的习惯,游戏一样,工作一样,人生也一样。
更新:根据在微博上的讨论,我收回关于六色底影响比赛公平性的观点。感谢@铯版 @樊轶群 @冰菓不是水果 @Lazy_锋 @反叛的撸撸咻专四跑题给跪 @陳丹陽_占星 @阳光丶灿烂了 等朋友参与讨论。
所谓六色底,是指由双色底(又称对色底)发展而来,并由现三阶速拧世界纪录保持者Feliks Zemdegs发扬光大的一种技术。具体来说就是在开始还原魔方前,从六种颜色中选择一个作为底色进行还原。
目前六色底正在全世界范围流行,我个人也曾练习过一段时间,并尝试在几次比赛中使用六色底。接下来我想谈谈六色底的本质,对比赛公平性的影响,以及自己对这种技术的一些看法。
就目前最流行的CFOP还原法来说,整个还原过程中最重要,也是最不稳定的部分就是十字(Cross)以及和第一组F2L的衔接。其余的三组F2L,以及接下来的OLL、PLL对一个公式量丰富的玩家来说,是相对稳定的部分。
十字的难易程度取决于打乱case,是完全随机的。如果十字步数少,手法顺,对case的扰动少,第一组F2L就容易计算和观察,也容易直接和做十字的流程连贯起来。反之如果十字步数多,对case的扰动多,第一组F2L的计算观察难度就会大大提高。
因此十字的难度会直接影响到第一组F2L的衔接,进而影响本次还原的成绩。
在多色底技术普及之前,魔方玩家一般都只使用一种固定的颜色作为底色(比如我使用蓝色)。同一个打乱case对使用不同底色的玩家来说其实是完全不同的两个case。
对同样的一个case,白底的十字好做,出好成绩的几率就高,蓝底的十字难做,出成绩的几率就低。这也往往表现在比赛中,采用某种底色的选手们在某个case上的成绩普遍提升,而采用其他底色的玩家则发挥平平。
不知是哪位玩家灵机一动,想出了双色底这种技术。比如使用白色底的玩家,通过练习可以很容易掌握黄色底。这样在打乱case对白色底十字不理想时,可以换用黄色底还原,因为黄色底的十字会完全不一样。
这其实在无形中满足了很多玩家的梦想“这个打乱不好,要是能换一个就好了”。虽然WCA比赛规则是不允许选手提出无故换打乱这种要求的,因为这样会对其他选手不公平。
然而,如果你使用多色底技术,在15秒的观察时间里,你可以自己换打乱!
从公平意义上来说,所有选手在同一轮比赛中应该面对同一个打乱case,这样选手的成绩才可以进行比较。
按照目前的WCA比赛规则,打乱公式随机生成,并以“白上绿前”的初始状态进行打乱。表面来说每个选手拿到的是同一个case,但是对使用不同底色的选手来说,其实面对的是不同的打乱,比赛的公平性值得商榷。而允许多色底技术在比赛中使用,会进一步影响比赛的公平性。
在此我建议WCA对比赛规则做出以下改进:
1、选手在比赛前需要申报自己使用的底色。 2、打乱员根据选手申报的底色进行打乱,保证每位选手最终面对的是同一个打乱。 3、裁判则需要监督选手是否按照申报的底色进行还原,否则成绩作废。
我相信WCA在最初制订比赛规则时,没有想到会有人使用不同的底色,更没有想到会有人能同时使用多种底色,所以比赛规则出现了一些漏洞。
所谓的多色底技术,其实是利用比赛规则漏洞进行的一种投机行为。而在有选手开始使用双色底后,WCA对这种现象没有任何警惕,以至于愈演愈烈,直至发展到现在的六色底。
诚然多色底是一种很有趣的玩法,自己平时练习娱乐都没有问题。但是在比赛中不应该允许使用多色底,因为它对使用单一底色的玩家不公平。
在一个项目刚起步时,往往除了jquery之类的库外,只有一个js文件。
然而随着功能的不断增加,js代码也会不断膨胀,当多到一定程度后(500行以上),维护难度就会大大增加。组织逻辑混乱,debug时很难迅速定位到想修改的地方,写新function时也很难抉择插入的位置。整天在一个长长的js文件里滚来滚去,想想都让人头疼。
一个可选的方案是使用依赖库,诸如requireJS、SeaJS、In.js等等。但需要引入更多的代码,要抽出精力来维护依赖关系,还会增加额外的http请求。
在大多数情况下,js代码不会膨胀到必需引入依赖库的地步。有没有办法可以在不增加新代码和http请求的前提下,对js代码进行细粒度的维护呢?
在目前的项目中,除去jquery等类库后的js代码约有120kb(压缩后58kb),我们使用这样的方案:
1、按模块或者按性质将js拆分成多个文件,比如将和用户相关的部分抽取出来成为user.js、共用的方法抽取出来成为helpers.js等。
2、编写sh脚本(linux)或bat脚本(windows)合并所有js为all.js,并调用yuicompressor进行压缩生成all-min.js。
//windows下的bat脚本 del all.js all-min.js type source\*.js > all.js java -jar yc.jar --preserve-semi -o all-min.js all.js
3、在开发环境使用all.js(方便deug),生产环境使用all-min.js。
我们都试过用开始菜单、桌面图标、快速工具栏、辅助桌面管理软件等来管理常用软件,在Win7下还可以直接固定到任务栏,然而这些方法都有或多或少的瑕疵:
开始菜单:第一级可用位置太少,多级的话查找太慢
桌面图标:软件多了之后太乱,难以整理
快速启动栏 / 固定到任务栏(Win7):挤压占用任务栏空间
辅助桌面管理软件:需要安装多余的软件,可定制性差
试验过以上所有方法后,我选择了使用StokeIt以及Win+R法来管理和启动常用软件。这篇文章就来简单介绍一下我使用Win+R法的一些经验。
通过在环境变量PATH中添加一个目录,然后将常用软件及文档的快捷方式(.lnk文件)重命名为1~2个字母缩写放入该目录,要启动软件时按Win+R启动“运行”对话框,然后键入自定义的缩写再按回车即可。
详细的入门教程请看善用佳软的文章,本文只总结自己的一些经验。我的习惯是在D盘下建立一个Command目录,然后将所有快捷方式都放在此目录下。
这个目录要经常维护,所以先要为自己建立一个快捷方式,我将其命名为go,并放到Command目录下。之后要维护快捷方式直接Win+R go回车即可。
在“运行”对话框里是可以直接打开环境变量PATH中定义过的目录下的子目录的,所以在Command目录下可以建立目录对常用软件进行分组。比如我因为工作需要安装了大量的浏览器,而这些浏览器只有在测试的时候才用到,所以我不想给每个浏览器都建立一个缩写,而是建立一个br(browser的缩写)目录,并将所有浏览器的快捷方式都放在br目录下。在需要测试的时候Win+R br,然后选择启动需要的浏览器。
在Command目录下编辑BAT批处理(类似Unix下的shell脚本),可以实现更强大的组合功能,比如创建一个这样的test.bat,使用start命令来一次运行多个软件:
start cr.lnk start ie.lnk start ff.lnk
这样通过Win+R test可以同时打开chrome、ie和ff三个浏览器(当然前提是要在Command目录下建立好cr、ie、ff这三个快捷方式)。注意在命令行模式下是不能省略.lnk后缀的。
第一步:在“运行”中启动mstsc,点击“选项”,输入ip地址,取消勾选“始终要求凭据”。连接并登录一次。
第二步:在C:\Windows\System32目录下找到mstsc.exe,创建一个快捷方式到Command目录,改一个合适的名字(建议以ip最后一节命名,比如ip为10.0.0.88,则命名为88)。右键编辑其属性,在“目标”中的System32\mstsc.exe后加上格式为“/v:IP地址:端口号”的参数,比如“System32\mstsc.exe /v:10.0.0.1:3389”(如果没有修改过远程端口号,也可以省略后面的冒号和端口号)。当然也可以创建BAT文件,里面写上start mstsc /v:10.0.0.1:3389)。
之后只要Win+R 88就可以连接到远程桌面了。
有的网站可能一年半载去不了一次,但是还是需要注册登录(比如csdn下载站-_-)。大多数情况下不同网站下的用户名都会有所不同(比如zhangsan123,zhangsan1986等),一般对这种不重要的网站我们都会使用比较容易输入的短密码,但由于各网站密码规则的不同,最终密码还是会不一样。记忆这些用户名和密码纯粹是浪费脑细胞!
这时最好的方法莫过于建立一个密码本了,在需要登录的时候打开看一眼或者直接Copy & Paste。我的习惯是在Command目录下建一个p目录,然后在里面建立txt文件,文件名为网站域名(注意去掉www.),内容为用户名和密码。
这样当我要登录一个之前注册过的网站时,只要Win+R p,然后输入域名前几位,就可以迅速定位到对应的文件并打开,拿到帐号和密码了。
当然了,你轻松,别人也轻松。比较重要的密码(如qq、邮箱、网银)不能放在这里!
StrokeIt是一款鼠标手势软件,可以将用鼠标右键画出的手势绑定到许多常用的操作,如关闭/最大化窗口、启动程序、触发快捷键等。
下载地址和新手入门可见善用佳软的这篇文章,我就不再重复了。
这里和大家分享一下我使用StrokeIt的一些心得吧。刚接触这款软件时,有点走火入魔,把所有常用软件都绑定到手势,恨不得把所有的内置手势都利用上,甚至还自创了一些手势。结果是大部分的手势都很不常用,加之后来采用了WIN+R法启动常用软件(随后撰文另述),启动软件的手势慢慢就被淘汰了。
最终经过时间的考验,存活下来的手势只有一屏而已:
简单说明一下吧:
C Reversed(从下往上画的C):启动上图中的命令编辑器(Command:StrokeIt – Command Editor)。这在刚开始使用StrokeIt的那段适应期里是使用很频繁的功能。。
C (从上往下画的C):关闭窗口(Command:Keys – Hotkey,Alt+F4)。直接关闭当前窗口,主要用于多标签支持的程序。
\Up(从右下到左上):关闭一个窗口或标签(Command:Keys – Hotkey,Alt+F4)。在单一的程序窗口执行的是Alt+F4,即直接关闭当前窗口。在有多标签支持的程序里(如浏览器、Eclipse、PS等,在tabs分类中进行了重载)执行的是Ctrl+F4,即关闭当前标签。这样省去了鼠标定位关闭按钮(一般都很小)的时间。
\Down(从左上到右下):打开QQ消息(Command:Keys – Send Keystrokes,[F1])。恩没错,我用的是F1,这样左手也可以很方便得启动。你也可以设成Ctrl+Alt+Z或者你自己的快捷键。不用Hotkey的原因是在Hotkey里按F1会弹出帮助,无法记录下按键,下面使用Keystrokes的情况都是这个原因。
/Up(从左下到右上):最大化窗口/将窗口恢复正常(Command:Windows – Maximize or Restore)。这在窗口管理中是使用频率仅次于关闭的功能,就不解释了。
/Down(从右上到左下):最小化所有窗口并显示桌面(Command:Keys – Send Keystrokes,[WIN_DOWN]d[WIN_UP])。相信这个也不用解释了吧。
T(从左上到右上,再到右下):启动记事本(Command:Run -Run Program,C:\Windows\notepad.exe)。这个是我最初绑定的大量启动程序类手势中唯一保留的一个,平时用来快速记点什么东西、格式化文本转纯文本等,很方便快捷。
Left(从右向左):返回父级目录(Command:Keys – Hotkey,Alt + Up)。在Windows的资源管理器里按Backspace或者后退,是到历史记录里的上一条,而不是到当前目录的父级。这样我们通过直接输入目录地址,以及通过“打开文件位置”进入的目录,就要按Alt+Up这个很少有人知道的快捷键。
V(左上到下,再到右上):访问指定目录(Command:Run – Run Program,C:\Windows\explorer.exe)。通过手势打开目录的方法就是启动资源浏览器explorer.exe,并把要打开的目录地址(这里是\\server)通过参数(/e, “目录地址”)传递给它。
最后再说一下怎么给分类增加程序(用来重载)。比如我们有一个支持多标签的新软件要加入到tabs分类中,那么先选中tabs分类,然后点击右边的Add按钮:
启动你要添加的程序,把Finder Tool下面的那个图标拖动到你要添加的程序窗口上(如果是全屏运行的程序,那就只能通过File Name进行添加了,需要到程序安装目录下找出主程序文件名)。
获取到Window Class后按OK,在Application Identifiers里出现了新的CLASS,这个程序就成功地添加到分类里了。如果要从分类中移除该程序,将CLASS删除即可。如果忘了是哪个CLASS,可以通过Finder Tool重新查看。
Saolei.net这个域名是我在2006年在新网的一家代理商注册的,最近由于众所周知的原因,我需要将域名转移到Godaddy。下面就分享一下我和新网进行的交涉以及和ICANN沟通的过程。
在参考了很多朋友通过在ICANN投诉转移域名的帖子后,我决定还是走正规渠道,先和新网进行联系。果不其然,新网坚持要求提供身份证扫描件,话说我注册的时候你也没问我要啊?我向工作人员表明按照ICANN的规定,域名转出是不需要提供身份证的。工作人员坚持说是公司的规定,也拒绝出示任何法律法规。
解释不通只好去ICANN投诉,内容是已经准备好的: Continue reading Saolei.net域名从新网转出到Godaddy的过程记录
从去年到今年,陆陆续续看完了《代码大全》、《重构》、《代码整洁之道》、《程序员修炼之道》以及《The Art of Readable Code》,获益匪浅。下面就分享几条我赞同并信奉的编程哲学,顺便废话几句。
请先思考,评价一段代码优劣最重要的标准是什么?
有个著名的图,相信大家都见过,讲的是Code Review时被骂WTF的次数越少,代码的质量就越高。这虽然有点无厘头,但是却不无道理。
《The Art of Readable Code》中告诉我们,评价一段代码的质量的最佳标准是可读性,即别人理解代码意图所需要的时间。
可读性有什么用,我写出一段代码,能完成目标,能通过测试不就行了吗?
不管你是团队开发,还是一个人单干,只要项目还在运作,代码总是要有人来维护的。如果有一天你不在或者离开了,别人应该能很轻松的看懂你的代码,而不是猜来猜去,最后还要来问你,甚至弃用整段代码重新再写。作为一个负责任的开发者,应该尊重自己的劳动成果,也尊重别人的时间。
当你写下一段代码时,脑子里有清晰的逻辑(比如这里必须这么做,因为……这里不能那么做,因为……)。而别人看这段代码时,脑中是一片空白(这个变量是干什么的?为什么要多加一层判断?这个数为什么是 2 不是 3 ?),除非能完整复现你当时脑中的逻辑,否则就不能理解这段代码。
所以我们应该做的是,把写代码时脑中想到的逻辑的每一个细节,尽可能地写到代码以及注释中,从而帮助后来的阅读者快速复原整个逻辑。简单来说可以有以下的方法:
在一段代码中出现 if ( level > 3 ) …的时候,也许你会想:为什么是 3 不是 4 呢?这就是所谓的魔术数。通过上下文也许你可以判断出这个 3 是指最高等级,但是可能这段代码中 3 出现了好几次。你会迷惑:它们是不是都是一个意思?当我要把最高等级改成 4 的时候,是不是应该修改所有的 3 ?
如果代码的作者定义一个常量MAX_LEVEL = 3,同时还有一个常量USERS_PER_PAGE = 3,并在不同的地方使用不同的常量,就不会有这样的混淆了。
doit();
$return = $str2 . ':' . $str3;
return $return;
这样的代码,简直就是人工混淆过的。你会发现这些代码根本不能帮助你理解代码的含义。
也许你该写成这样?
handleError();
$error_message = $error_code . ':' . $error_status;
return $error_message;
$('#item').html(''); //清空item的内容
这样的注释有意义么?用自然语言重新描述一次代码的行为,除了徒增维护时的工作量外没有任何价值。你应该说明为什么这样做,以供别人看到这段代码时明白你是怎么想的,并决定如何修改或者对待这段代码。
$('#item').html(''); //先清空容器的内容,否则可能导致内容重复
这个规则的使用性太强了,我简单说说减少逻辑层次和缩小函数体这两个方面吧。
当逻辑层次超过三层时,理解这段代码的难度会急剧上升。我相信谁也不喜欢去读一个n层括号的表达式,或者面对n层缩进的条件判断/循环。
对于复杂的表达式,通过提取中间变量来降低表达式的逻辑层次,保证每个表达式的逻辑层次不超过二层。
对于多层条件判断,大多数情况可以用防御式编程将其简化成单层的条件判断,尽早return或者exit。此外,单行的if-else判断往往可以用三元操作符替换。如果判断实在太多,也许你该重新设计一下结构了。
记得有一位语言的创始人说过:“我不喜欢比我的头还大的函数”。
事实上,大家都喜欢短小精干、一眼就能看到底的代码。简洁明快的代码有助于别人迅速理解代码的意图,也方便快速定位问题。如果一个函数要滚动屏幕才能看全,那你往往要不断地来回滚动,并强迫自己记住一些信息,再返回去看另一部分,这样做会非常累。
随着函数体不断膨胀,理解它所需要的时间随之增加,出错的几率也会大大提升。而且越大的函数,可维护性和复用性越差。当部分代码逻辑需要修改时,不能快速定位到要修改的位置,也难以确定函数体其他位置是否也需要对应的修改。
当函数尺寸失控时,首先要想到的是,有没有其他方法,用更少的代码完成这个任务?能不能用正则表达式?能不能用查表法?有没有内置的库函数可以利用?
面对一个无法再简化的流程,将其拆分成细粒度的步骤,将每个步骤的相关代码分离出来,提取成子函数,再给子函数起一个漂亮的名字。这样可以降低理解主流程的难度,在做修改时也可以通过函数名快速定位,而且因为相关的代码都在一起,不容易漏改。
也许你会怀疑调用函数所造成的性能损失,我想说现在这个时代,手机都马上四核了……
看过《重构》一书后,我看到代码中任何重复的地方都如见眼中钉。
重复是万恶之源,当你发现你在对代码的不同部分进行同样的修改时就要警惕了。改的地方越多,就越可能出错。也许忘了改一个地方,也许错改或者删除了周边的代码……永远不要让这种事情发生!
$('#count').text( + $('#count').text() + 1 );
...
$('#count').text( + $('#count').text() + 1 );
...
如果一段相同的代码出现两次,基本上你还会第三次用到它,所以很有必要将其提炼成子函数。这样不仅可以减少代码量,还可以降低维护的难度。
countPlusOne();
...
countPlusOne();
...
function countPlusOne() {
$('#count').text( + $('#count').text() + 1 ); //以后修改只用改这里就好了
}
如果有几段代码很相似,往往可以提取其共性逻辑,使用不同的调用参数进行区分。
countChange( +1 );
...
countChange( -1 );
...
function countChange( change ) {
$('#count').text( + $('#count').text() + change );
}
$item[ 'id' ] = $_POST[ 'id' ];
$item[ 'name' ] = $_POST[ 'name' ];
$item[ 'mail' ] = $_POST[ 'mail' ];
$item[ 'qq' ] = $_POST[ 'qq' ];
如果有大块逻辑雷同只有一两个地方更改的代码,往往可以用循环来解决:
foreach ( array( 'id', 'name', 'mail', 'qq' ) as $key ) {
$item[ $key ] = $_POST[ $key ];
}
qwerty布局大家应该都很熟悉了,全世界最普及的键盘布局。
截止到去年接触并使用dvorak布局之前,我使用了十几年qwerty布局,在http://speedtest.10fastfingers.com/测试的最高CPM(每分钟击键数)为475。
相信阅读本文的读者中,有99%以上都在使用qwerty布局。
本热区图是使用http://www.patrick-wied.at/projects/heatmap-keyboard/对apache2的license.txt(36k)进行分析的结果,下同
1、普及率和兼容性接近100%
2、有史以来的各种软件的快捷键都默认为此布局设计
3、最常用的快捷键(^a, ^s, ^c, ^v, ^x, ^z, ^w)都可用左手完成
1、高频键大多不在中排,都需要移动手指才能按到,导致需要大量的手指移动
2、高频键左右分布不均衡,左手负担比右手重
3、经常出现单手连续击键(尤其是左手)的情况,容易疲劳也容易出错
4、常用标点符号;/’都要用小指输入,容易疲劳也容易出错
这个布局知道的人就很少了,和qwerty布局相比,它将元音字母aoeui移到左边,将最常用的辅音字母移到右边,同时调整了一下标点符号的键位。
自从去年接触到它后,我用了一个月的时间适应,然后就抛弃qwerty布局了。不仅输入速度得到了提升(在http://speedtest.10fastfingers.com/测试的最高CPM为535),而且长时间coding也很少出现疲劳的情况了。
Windows:在语言设置里添加“美国英语-Dvorak”,推荐使用http://code.google.com/p/dvorak-qwerty/
Linux:在终端输入loadkeys dvorak(GUI请在键盘设置里找),推荐使用http://code.google.com/p/dvorak-qwerty/
Mac:在语言设置里选择“Dvorak-标准”
更多系统下启用Dvorak布局请见:http://dvorak.mwbrooks.com/support.html
1、将高频键都分布在中排,大大降低手指移动距离
2、尽可能使左右手交替击键,均衡负担
3、布局优雅,精心设计了右手负责区域的键位,使辅音字母组合(如th nt gh wh rn)输入非常顺手
4、将常用的标点符号’ , . ; = – 移到更舒服的位置,输入更方便(对程序员来说巨爽)。
1、布局变化过大,几乎所有的字母和标点符号都改变了,上手困难
2、到qwerty布局的电脑上丧失盲打能力
3、别人很难再使用你的电脑了,可以再装一个qwerty布局切换
4、本来单手可以操作的快捷键要两手操作。不过MacOS下有天然的“Dvorak-标准”布局,Windows和Linux下可以用这个http://code.google.com/p/dvorak-qwerty/来使用原有键位的快捷键,但是还有一些BUG(不支持ALT快捷键、有个别字母如s w v z在开启Capslock时输入仍是小写等)
5、部分跳过键盘布局直接取键盘码的软件仍然是qwerty布局,比如QQ拼音和搜狗输入法(经测试紫光和谷歌输入法可以正常使用,只是可惜了我的QQ同步词库),还有QQ和部分网银的密码输入框(解决办法是看着键盘按qwerty布局输入),还有一些远程桌面软件,还有一些游戏(比如魔兽,这点挺好,DOTA无障碍了,哈哈)
6、高频键 r 和 i 仍需要移动才能按到,而 u 和 h 不是很高频的键却放在了本位(不需要移动就能按到)
估计某个程序员发现用dvorak布局很爽,就将之变得更加极限。于是出来这么一个布局。
这个布局大胆的用常用符号替换了数字,同时考虑到在程序中;比”出现得更频繁,调换了两者之间的位置。
1、包括上述dvorak键盘的所有缺点
2、优化了程序中常用符号如$ ; ( ) { } [ ] =等,用来coding巨爽
1、包括上述dvorak布局的所有缺点
2、输入数字只能按住shift进行-_-
这个布局比dvorak更激进一些,通过热区图可以看到,8个高频键完美地安排在了本位。
坚持使用colemak数日后,实在无法接受别扭的标点输入进行coding,只好放弃colemak,用回dvorak了,也许colemak对普通人来说更适合一些。
Windows:http://colemak.com/wiki/index.php?title=Windows
Linux:http://colemak.com/wiki/index.php?title=Unix
Mac:http://colemak.com/wiki/index.php?title=Mac
更多资料请见:http://colemak.com/
1、高频键真正做到了完美分布,手指移动距离更小
2、qwzxcvbm以及标点符号键都没有移动,而且除了e和p外,字母键都在原来的手控制之下,更容易上手
3、常用的快捷键(^a, ^c, ^v, ^x, ^z, ^w)仍能正常使用
4、万年不用的capslock被换成了高频键backspace,巨爽
5、有资料详尽的官方网站http://colemak.com/,甚至针对各种打字练习软件设计了练习包
1、键分布没有逻辑,不如dvorak优雅
2、没有优化常用标点符号
不久前发生一起严重的事故,误删了生产MongoDB服务器上的一个collection,由于没有定时备份,导致几天的数据丢失。认真反省之后,写下这篇文章,希望能给大家一些警示。
当时我正双开两个终端窗口,分别用mongo连接开发环境和生产环境的MongoDB。由于本地的数据库因为不断的开发测试已经被污染了,我从生产环境dump了最新的数据并下载到本地。接下来要做的是把本地MongoDB中的对应collection给drop掉,然后restore回去。
这时我不知道被什么事情打断了,当我继续这个过程时,由于Mongo Shell中没有像Redis那样显示服务器的IP,我并没有注意到自己点开的是生产服务器的终端,并执行了drop命令……
1、在对生产数据库做删除之类的危险操作时,一定要两个人double check;
2、明确区分生产环境和开发环境,在执行各种删除操作前对所在的环境进行确认;
3、在执行重要操作时,不接受其他事务的打断;
在生产环境和开发环境都建立一个~/.mongorc.js文件,内容如下:
var prompt = function ( ) { var host = db.getMongo().toString().replace( 'connection to ', '' ); var database = db.getName(); return host + '/' + database + '> '; }
然后连接到Mongo Shell时就会显示服务器IP了,如:
[zhangshenjia@mac: ~]$mongo MongoDB shell version: 2.4.4 connecting to: test 127.0.0.1/test>
在测试中发现iPad上的Safari总会把长串数字识别为电话号码,文字变成蓝色,点击还会弹出菜单添加到通讯录。
别的地方倒也罢了,如果在用户名中出现数字(手机注册新浪微博的话用户名就是“手机用户xxxxxxxx”),版式会很恶心。
经过测试在a标签中的长串数字不会识别为电话,于是给出现用户名但没有链接的地方嵌套一个无动作的a标签,临时解决了这个问题。
但是这样增加了额外的标签,代码的语义性变得很差,而且对大段文字不能用这个方法。
今天无意中撞进Safari的官网,发现了safari有个私有meta属性可以解决这个问题:
官网的说明如下:
In Safari on iPhone, phone numbers are automatically detected and transformed into links that dial the phone number when tapped. If you have strings of numbers in your webpage that should not be automatically detected as phone numbers, you can choose to disable this feature on the entire page by adding the <meta> tag shown in Listing 12.
今天群里灰大出了个题:
function getRelativeURL(url, baseURL) { // 获取url相对baseURL的相对路径 // url和baseURL都是绝对路径或都是相对路径的情况下,有可能计算出结果,否则扔Error // 如果都是绝对路径,但不在同一个域、同一个协议、同一个端口下,即无法计算相对路径,扔Error }
用了点时间写了一下:
function getRelativeURL ( url, baseUrl ) { var path, basePath, i = 0, compare = {}, r = { 'proto' : /(^[a-zA-Z]{1,4}):\/\/(.*)/, 'domain' : /(^[-_a-zA-Z.]+)((?:\/|:|$)?.*)/, 'port' : /(?:^:)(\d+)((?:\/|$).*)/, 'word' : /[^, ]+/g, }; compare[ 'url' ] = parseUrl( url ); compare[ 'baseUrl' ] = parseUrl( baseUrl ); if ( different( 'proto' ) || different( 'domain' ) || different( 'port' ) ) return false; //协议、域名、端口不同 path = compare.url.path; basePath = compare.baseUrl.path; while ( path.length && path[ 0 ] == basePath[ 0 ] ) { //去除相同父目录 path.shift(); basePath.shift(); } if ( path.length == 0 && basePath.length == 0 ) return './'; //两个url相同 if ( path.length == 0 ) return parent( basePath.length ); //baseUrl是url的子目录 if ( basePath.length == 0 ) return path.join( '/' ) + '/'; //url是baseUrl的子目录 return parent( basePath.length ) + path.join( '/' ) + '/'; //url和baseUrl互不包含 function different ( name ) { return compare[ 'url' ][ name ] != compare[ 'baseUrl' ][ name ]; } function notValidUrl () { return compare[ 'url' ][ 'proto' ] == '' || compare[ 'baseUrl' ][ 'proto' ] == ''; } function parseUrl ( url ) { var parsed = {}; 'proto,domain,port'.replace( r.word, function( name ){ var match, reg = r[ name ]; if ( !reg.test( url ) ) { parsed[ name ] = ''; return; } match = url.match( reg ); url = match[ 2 ]; parsed[ name ] = match[ 1 ]; }); parsed[ 'path' ] = url.replace(/\/+$/,'').split('/'); return parsed; } function parent ( level ) { console.log(level); var result = []; while ( level-- ) result.push( '../' ); return result.join(''); } };
使用单元测试如下:
test("url和baseURL并非都是绝对路径或都是相对路径,没有结果", function() { expect(4); ok(!getRelativeURL('http://aoeu/ao','/aoeu'),'绝对,相对'); ok(!getRelativeURL('/aoeu','http://aoeu/ao'),'相对,绝对'); ok(getRelativeURL('http://aoeu','http://aoeu/ao'),'绝对,绝对'); ok(getRelativeURL('/aoeu','/ao'),'相对,相对'); }); test("不在同一个域、同一个协议、同一个端口下,没有结果", function() { expect(3); ok(!getRelativeURL('http://aoc.com','http://aoeu.com'),'不同域'); ok(!getRelativeURL('https://aoc.com','ftp://aoeu.com'),'不同协议'); ok(!getRelativeURL('https://aoc.com:88','ftp://aoc.com:89'),'不同端口'); }); test("是否以/结尾不影响结果",function() { expect(3); equal(getRelativeURL('http://aoeu.com/aoeu/','http://aoeu.com/'),getRelativeURL('http://aoeu.com/aoeu','http://aoeu.com'),'都以/结尾'); equal(getRelativeURL('http://aoeu.com/aoeu/','http://aoeu.com'),getRelativeURL('http://aoeu.com/aoeu','http://aoeu.com'),'url以/结尾'); equal(getRelativeURL('http://aoeu.com/aoeu','http://aoeu.com/'),getRelativeURL('http://aoeu.com/aoeu','http://aoeu.com'),'baseUrl以/结尾'); }) test("相同目录",function(){ expect(3); equal(getRelativeURL('http://aoeu.com','http://aoeu.com/'),'./','根目录'); equal(getRelativeURL('http://aoeu.com/aoeu','http://aoeu.com/aoeu'),'./','单级'); equal(getRelativeURL('http://aoeu.com/ao/eu','http://aoeu.com/ao/eu/'),'./','多级'); }) test("url和baseUrl中有一个为根目录",function() { expect(4); equal(getRelativeURL('http://aoeu.com/aoeu','http://aoeu.com'),'aoeu/','url为baseUrl的单级子目录'); equal(getRelativeURL('http://aoeu.com/ao/eu','http://aoeu.com'),'ao/eu/','url为baseUrl的多级子目录'); equal(getRelativeURL('http://aoeu.com','http://aoeu.com/ao'),'../','baseUrl为url的单级子目录'); equal(getRelativeURL('http://aoeu.com','http://aoeu.com/ao/eu'),'../../','baseUrl为url的多级子目录'); ! }) test("其他",function() { expect(2); equal(getRelativeURL('http://aoeu.com/aoeu','http://aoeu.com/ueoa'),'../aoeu/','单级'); equal(getRelativeURL('http://aoeu.com/ao/eu','http://aoeu.com/eu/ao'),'../../ao/eu/','多级'); })
如有更好解法或发现错误,请不吝赐教:)