我的技术和生活

  • 使用 Phantomjs 导出 PDF

    使用 Phantomjs 导出 PDF

    接到一个新需求,给用户导出电子协议,也就是 PDF 文档。因为我们后台使用 PHP,所以自然就去寻找 PHP 的解决方案。看了几个库,包括 Packagist 几万十几万下载的库,唉,不得不说,虽然 PHP 是世界上最好的语言,但是 PHP 开发者,审美水平真的,说好听点就是,没法看……

    第二个问题是,PHP 很多都要“拼” PDF,一行一行,一个元素一个元素,组装起一个完整的文档。相当费力,而且样式不好控制。从 HTML 转换也有类似的问题。研究了一会儿觉得蛋越来越疼,唉,算了,还是换 Phantomjs 吧。

    Phantomjs 是一个命令行 Webkit 工具,我们可以把它理解成不输出页面的浏览器,但它支持浏览器的各种功能,因为有 Webkit 嘛。所以渲染网页然后抓图就是小菜一碟了。

    用 Phantomjs 输出 PDF 非常简单:

    • 首先,约定好宽高(为方便打印,我们的电子协议要分页,而且有页头页脚),完成页面模板。
    • 完成 Phantomjs 脚本。因为只用它生成文档,所以不需要 Web 服务。
    • 用 PHP 调用脚本,生成 PDF 文档,然后 readfile 给用户下载。

    这样做的好处是我们随时可以预览效果,HTML 好读好改,PHP 替换其中的内容也很方便。而且代码非常简单,结合官方示例,很快就写出来了:

    'use strict';
    
    var page = require('webpage').create()
      , system = require('system')
      , args = system.args
      , url = args.length > 1 ? args[1] : 'http://www.dianjoy.com/'
      , filename = args.length > 2 ? args[2] : 'tmp';
    
    page.viewportSize = {
      width: 800,
      height: 1100
    };
    url = decodeURIComponent(url);
    page.open(url, function (status) {
      console.log(status);
      if (status === 'success') {
        page.render('/tmp/pdf/' + filename + '.pdf');
      }
      phantom.exit();
    });
    

    部署这段代码最大的问题反而是 GFW 导致 npm install phantomjs -g 失败,直接下载 zip 也不行(因为放在 Amazon S3 上)。于是继续给病魔加油,早日弄死方校长,及其它筑墙士。

    第二个问题则是 PHP 执行脚本老不成功。只看文档很简单:

    exec('/usr/local/phantomjs/bin/phantomjs pdf.js http://meathill.com/ meathill');
    

    但实际上既没有生成文档,也没有任何返回,调试半天,我突然想起来前阵子用 Apktool 解析安装包的时候也遭遇过类似的问题,于是在末尾加上 2>&1 问题竟然就解决了。

    Google 之,也不是很懂。回头再说吧。


    其它参考:

    shell_exec


    图文无关。其实是一位大学友人今日喜得贵子,放张她的奇怪照片祝贺她。

  • 最后一次修电脑

    最后一次修电脑

    我接触电脑比较早,学习机什么的不算,大约10岁还是11岁的时候,就有个长辈借给我一台他们单位淘汰的386,使用5吋高密盘和黑白显示器,运行 MS-DOS 3.2。其实这台电脑其实并没有发挥太大作用,我基本只用来学 DOS 命令和简单的 QBasic 编程,因为没有磁盘,甚至没法保存。后来父母给我买了第一台电脑,486,75M CPU,600M硬盘,4M内存。从那时起,我踏上了自己的电脑之路,也踏上了修电脑之路。

    我修过的电脑,多了不敢说,100台应该是有的。时间跨度二十年,地理跨度上至北京下至武汉,甚至第一次去老婆家重庆,也顺手修了两台还是三台电脑。我曾经帮运维同事解决电脑问题,也曾在装机店指点店员装机,就在我觉得可能普通用户遇到的问题我都能解决的时候,终于被自己的电脑教做人了。

    这台机器似乎是2011年离开201之前攒的,后来帮一个 AMD 的朋友做网站,受赠显卡一块,使用至今。

    早年我自诩电脑高手,基本上兵来将挡水来土掩,遇到问题都自己搞定,可是随着年龄加大,越发懒得花时间去搞电脑了;而且来到广州后,电脑的毛病也随之而来。先是不定期开不了机,表现为机器启动表现正常,但是显示器不亮,通过硬盘灯和开关(按下后进入睡眠)判断,大部分配件应该没问题,只是显卡不工作。将显示器插到主板集显接口(不拔除独显),可以正常启动。看起来就是显卡或者电源问题。一般重新插拔扫扫灰,也就能继续用。这个问题加上各种开发需求,我干脆买了台 iMac Retina 最为工作娱乐的主力机,PC 基本只用招行专业版和某些游戏。

    不过我还是委托我爸帮我买个新电源,顺便换个小机箱,他照做了。机箱不算太小,不过的确好了一阵子。然后就发展到二期。

    启动的时候,显示器不亮,风扇转,停顿一段时间之后,掉电重启。如是反复。我当然还是怀疑电源不行,不过对 PC 的需求不大,于是又委托给我爸。他检查后认为是 CMOS 设置问题,重置之后就可以正常使用了。我虽然怀疑这个观点,但是能用就行,也懒得深究。

    然后怪物猎人 Online 就发布了。炉石暗黑魔兽,Mac 都可以搞定,但是怪物猎人只能在 Windows 下玩。而且这个版本用的虚幻引擎,效果很好,但是 Surface 基本还是前线的水平,所以用台式机玩体验最好。

    克服了不定期抽风不启动的毛病之后,进入P3。机器会卡,表现为不管干啥突然卡住,鼠标能动,但是什么都干不了,过一会儿就好了;仔细听能听到硬盘盘片启动的声音,重复若干次。没几天问题愈发严重,启动后说找不到驱动程序,或者找不到系统关键组件云云,最后干脆不能启动。这么一来我当然怀疑是硬盘坏了,毕竟是耗材嘛。实话实说 iMac 的混合硬盘用起来感觉很好,价格也适中,于是就买了块 1T 的硬盘换上。结果也就是3周时间,这块硬盘也不行了,而且是一样的问题。

    然后我就怀疑是主板坏了,导致读写操作有误,继而损坏硬盘。算起来这套东西也用了将近5年,当时还是第二代酷睿,现在已经是第六代了,干脆换套新的得了。买买买!

    既然我把这称为“最后一次修电脑”,那过程当然是崎岖的。新配件装上后,正常启动,安装系统,例行更新。结果更新到32%,再也走不动了。然后启动也启动不了,于是我只有两块硬盘颠过来倒过去重装系统,中间还混着显卡不工作的问题,折腾好几天。突然我觉得,尼玛是不是还是电源的问题啊?于是换到以前没用到的,以 IDE 接口为主的那根线,一切都正常了。

    弄了半天还是电源的锅啊,有个爱省钱的爹真的是……

    买都买了,那就用呗。再买个新电源,就当升级了。

    正当我以为一切风雨都已过去,终于迎来大团圆结局的时候,新的问题出现了。而且这次我真的想不出是什么东西导致的:

    1. 开机,自检通过,能听到“嘀”的一声短响,片刻之后,连续5短鸣报警
    2. 显示器不亮,似乎是独显不工作,但是插板载显卡也不行
    3. 按电源键之后,可以正常进入睡眠,再开也一样
    4. 必须按住电源键15秒强制关机,然后再开机,同样会5短鸣报警,而且看不到启动画面,但是等一会儿,可以直接看到 Windows 的欢迎屏幕
    5. 如果重启或者正常关机,下次还是显示器不亮
    6. 更奇怪的是,打游戏正常,但是不能退出游戏,不然显示器就再也亮不起来;但是可以强制退出,强退没关系

    我现在身心俱疲,不想再折腾了。接下来有两个选择:

    1. 买一根长 DVI 线,以后就用集显,凑合玩,正常来说古龙我应该就打不动了
    2. 这么着凑合玩,终点和上面一样

    无论如何,我想这次都是我最后一次修电脑(装机),以后我只会买品牌机。

    啊啊啊,我的青春,又一个东西离我远去。

  • 自助攅机二十多年,这次之后就收山了,以后只买品牌机。

    立帖为证。

  • 解决 PHP 导出 CSV 的乱码问题

    解决 PHP 导出 CSV 的乱码问题

    项目当中遭遇一个奇怪的问题:

    导出 CSV,文本编码使用 UTF-8,使用 Mac + Numbers,Windows + WPS 打开都正常,使用 Windows + Office 就乱码(Mac + Office 没有测试)。用记事本打开另存为,编码的确是 UTF-8。

    后来发现,用 EditPlus++ 打开,然后另存为 “UTF-8 BOM”,就可以正常打开了。看来应该是这个 BOM 的问题。

    于是乎参考 StackOverflow 这个答案,给输出的开头加上 "\xEF\xBB\xBF",果然解决了问题。

  • 珠海东澳岛 Club Med

    珠海东澳岛 Club Med

    上上周我们一家三口去东澳岛 Club Med 来了趟亲子游。因为各种忙 + 拖延症,所以体验拖到现在才动键盘。

    为懒得往后看的人提供总评:四星,值得一去。交通方便,住宿环境好,餐饮也好,沙滩沙质较细,海湾内风平浪静。美中不足是沙滩不够大,酒店外可玩的东西不多。

    Club Med

    俯瞰 Club Med 的海湾

    从海滩看房间

    Club Med 中文叫做地中海度假村,是个全球连锁酒店,在中国有四家分店,分别位于哈尔滨、桂林、三亚,以及我们这次去的东澳岛。他们家主打的是一价全包的度假村内生活,除了个别服务,比如贵的要死的 SPA,或者单价较高的酒,都不再单收费,包括酒店业最黑的 mini bar。

    酒店里设施很多,不仅有住有吃,还有各种玩的东西。还有专门的儿童托管服务,四个年龄段从两岁管到十七岁,早上9点开班,晚上5点半接回来吃顿晚饭,然后7点半再送去到8、9点,让大人们有比较充足的自由时间。这也是他们家的主打——亲子游。

    交通

    我们选择的路线是开车到珠海香洲港,乘公共渡轮,2点40分开,航程大概45分钟,到东澳岛。下船之后就有 Club Med 的车和工作人员在码头迎接,直接上车入驻酒店。

    回程的时候也类似,12点开饭,吃完12点半上车去码头,1点开船,45分钟到香洲港,开车回家。

    住宿

    房间

    阳台

    可能因为岛上地便宜吧,房间还是很大的,标称60平。设施比较齐,有浴盆,不过全家一起泡不太容易,可能因为我体积太大了吧……

    所有房间都是朝海的,我们房间所在的楼层比较低,视野不算太好。

    餐饮

    酒店有两个餐厅,分别供应正餐和加餐;还有两个酒吧 + 一个沙滩吧,供应各种饮料和小食。

    他们家的食物挺好的,都是自助餐,荤素冷热中西搭配,每天都换不同花样。来的那天和走的那天相对一般,大约市值150元左右;中间那天赶上复活节,海鲜烧烤都有,味道品质都不错,估计市值250元吧。

    项目多,玩得很累,我又不敢吃太多,所以很容易饿。好在有专供加餐的餐厅,提供自选材料的面和点心、饮料。

    这方面没拍照片。

    玩的项目很多,分三大块:运动中心 + 海边 + 周边。具体内容可以去官网看我就不多说了,说几个我们玩过的。

    首先篮球场还蛮好的,不过凑人比较困难。射箭比较好玩,领导一不小心还射中个十环。往山上爬有个亭子,叫蜜月阁,是岛上的制高点,不过景致一般,下面各种简易房。海边项目比较好玩,沙比较细,也比较粘,杂质不多。可以坐帆船(还可以学开帆船,不过我们没有那么多时间),也可以自己划独木舟或者冲浪板。今年比较冷,没法下海游泳,不过我从冲浪板摔进去海里之后踩到海底,感觉还是干净平整的。

    儿童俱乐部

    儿童俱乐部是来这里的主要原因,酒店里几乎都是带着孩子的爸爸妈妈爷爷奶奶,各种规模各种组合。姆二过了两岁,可以参加迷你俱乐部,里面都是2~4岁的小朋友。相当于一日全托,送进去的时候需要带免疫症,拍照也行。

    里面有阿姨带着玩,吃饭,睡觉。感觉她们还是很专业的,很快就能和孩子玩到一起。小班有两个老师,六个小朋友。据说玩了玩具看了动画片游了泳。家长不放心可以躲远远的偷看。我们偷看过几次,没什么问题。

    酒店有配摄影师跟拍,照片洗出来是要钱的,7吋40/张,12吋90/张,不过拍得一般,洗出来的更差,色差严重,但是为了要底片,只好花钱了。我们那天,我们家孩子这两张是拍得最好的,哎……

    不足之处

    前面说的基本都是好话,当然他们家也不可能十全十美。接下来说点不好的。

    首先岛很小,可玩的东西比较少,基本上只能在酒店里。现在是淡季倒也问题不大,但是到旺季酒店资源紧张的时候恐怕还是会比较麻烦,而且岛上还会有其它客人(民宿啊,一日游啊,跳岛啊),也会来这边玩(只有这片沙滩最好),拥挤程度不难想象。

    其次要坐船去,受天气影响很大。我们第一次就赶上大雾,真他喵的大,几乎伸手不见五指。然后船就停航了,没去成,好在这种人力不可抗拒因素对方很主动的就给改签了。

    以及,SPA 真特么贵啊……


    走的那天,我们在海滩边玩,结实了北京来的一家子,孩子比我们家大一岁,昨天也在 mini club,很快两个小朋友就玩在一起。他爸爸长着季诚的模样,说话方式跟郭枫很像,给我一种魔幻的穿越感。他们这一站是从北京飞到珠海,先到 Club Med 里住两天,然后去长隆,再飞回北京。这条路线可以给北京的各位亲作为参考。

  • 32

    32

    今天是我的32岁生日。

    32,对我来说是一个特别的日子。首先,作为程序员,32=2^5;其次,今年是我工作第十个年头;再次,据我不太可靠的记忆,今天也是我入职我司四周年。

    年岁渐长,越发觉得时间过得飞快。我看到病例本上那个数字“31”,仍然觉得有些不可思议,我靠,我特么的都30+了?我还记得大学时候在寝室看《老友记》,有一集乔伊30岁生日,痛哭流涕,大喊:“30,我们说好了你不来找我的!”一晃眼,现在我也32了,周围都是90后小朋友,不知道我入行的时候,前辈看我的样子是不是跟我看他们的样子一样。


    前两天发了张出去玩的照片,附言:“国庆后第一次出游。最近压力大,眉毛又少了,周末放松一下。”

    大学老师看到了,回复:“搞那么累干嘛?钱挣不完,工作也干不完。”

    我不禁失笑,老师太看得起我了,对我来说,钱哪里是挣不挣的完的问题,完全是挣不挣的到的问题啊!!亏得我司还发得起工资,偶尔还能出去放松下,有些朋友都开始收欠条了……


    今年发生不少大事,比如 AlphaGO 战胜人类九段李世石,证明计算力和算法取得新的长足进步;还有引力波终于被证实,人类可以聆听宇宙的声音。我不知道接下来还会发生什么,也不知道再过十年人类社会会变成什么样子,只希望无论发生什么,我都能照顾好全家。


    儿子一天天长大,目前看来一切正常。希望他继续健康成长。


    希望下一个8年能实现财务自由。

  • 96公牛vs2016勇士

    皮蓬:当年的公牛会横扫这支勇士

    勇士打当年72胜的公牛基本上是毫无赢面的,不过多半也不会颗粒无收,运气好能赢下一场。

  • Template was precompiled with an older version of Handlebars

    Template was precompiled with an older version of Handlebars

    gulp-handlebars 对 Handlebars 的要求是 ^3.0.0,预编译器的版本是 6。Handlebars 4 之后升级预编译器到 7,所以如果使用最新版本 Handlebars,就会报 “Template was precompiled with an older version of Handlebars” 错误。

    这个时候有几个解决方案,比如 这个页面 中提到的先删再装。不过经我实测,npm update 可能导致失效,还要重弄,太麻烦。所以最简单的,就是直接用本地的高版本覆盖依赖中的版本:

    npm install handlebars --save-dev
    rm -rf ./node_modules/gulp-handlebars/node_modules/handlebars
    cp -r ./node_modules/handlebars ./node_modules/gulp-handlebars/node_modules
    
  • Gulp 中顺序执行任务

    Gulp 中顺序执行任务

    书接上文,Chrome 插件中无法直接使用 Handlebars 处理模板。两种方案,一是利用沙箱,将 .eval() 放在独立的环境中执行,好处是其它跨域的操作也能这样处理,坏处是写起来麻烦。另一种则是利用 Handlebars 的“预编译”功能,将模板提前编译好,直接在代码中引用。好处是写起来更顺畅,并且从发布插件的角度来看,早晚都要这样做。

    我决定选用后者,于是我需要把模板提取出来进行预编译,然后我准备写个 Gulp 任务来搞定这个。

    从页面中提取模板不算太难,写个正则就好,这里提前约定,模板使用 <script type="x-handlebars-template"> 标签包裹。

    gulp.task('template', function () {
      let promise = new Promise((resolve, reject) => {
        fs.readFile('popup.html', (err, content) => {
          if (err) reject(err);
          resolve(content);
        }
      });
      promise.then((content) => {
        content.replace(/<script([^>]*)>([\S\s]+?)<\/script>/g, (match, attr, template) {
          // 具体处理模板,略掉了
        });
      })
    });
    

    模板编译之后,还需要用 Webpack 打包才能使用,于是要增加 Webpack 的任务。这个比较简单,参考它的 文档 即可。不过很明显,必须等全部模板预编译完成才能打包。之前我已经试过用 run sequence 顺序执行任务,不过这里我用到 Promise,情况似乎有变。

    稍微 Google 一下,得知要能顺序执行任务有三种选择:

    使用 callback

    gulp.task('task', function (callback) {
      setTimeout(function () {
        // 任务执行完成后,调用 callback
        callback();
      }, 5000);
    });
    

    返回 stream

    这个好像是标准做法,也是我之前做的。

    gulp.task('task', function (callback) {
       returen gulp.src('*.js')
         .pipe(uglify())
         .pipe(gulp.desc('dist/');
    });
    

    返回 Promise 对象

    gulp.task('task', function () {
      return new Promise(resolve, reject) => {
        resolve();
      });
    });
    

    既然支持 Promise 那就好办了,在 .then() 前面加一个 return 就好。


    完整代码

    参考文章

    Handling Sync Tasks with Gulp JS