日志

  • Ubuntu 16.04 搭建 LNMP 开发环境

    Ubuntu 16.04 搭建 LNMP 开发环境

    前天帮人配了台机器,未来可能还要帮人配。在学会用 Docker 之前,先写一篇记录下怎么搭建环境吧。

    这篇收费!¥4.99,请阅后自觉打钱。

    (更多…)
  • 使用 Webpack 时需避免循环引用

    使用 Webpack 时需避免循环引用

    开发日历控件的时候,对方变更了一下需求,基本上将最终产品分成两个:

    1. 选择连续时间段
    2. 选择多个不连续时间

    那么我们知道,对于这种大部分功能一致,只有若干函数逻辑不同的产品,最合适的就是状态模式。于是很自然的,我就拿“2”作为标准模式,“1”作为新模式,将其重构成父类和子类,大概关系如下:

    // 父类
    // DatePicker.js
    
    import RangeDatePicker from './RangeDatePicker';
    
    class DatePicker {
      ....
      static getInstance(el, options) {
        if (options.scattered) {
          return new DatePicker(el, options);
        } else {
          return new RangeDatePicker(el, options);
        }
    }
    
    
    // 子类
    // RangeDatePicker.js
    
    import DatePicker from './DatePicker';
    
    class RangeDatePicker extends DatePicker {
    
    }
    

    因为这个类只有两个成员,所以我把工厂方法 .getInstance() 放到了父类里面,通过判断参数确定应该返回哪一类实例。代码写完,测试的时候却报错:

    Super expression must either be null or a function, not undefined

    这个意思很明显,被继承的父类不能未定义。然则 DatePicker 明明是定义了的,只是验证两个类文件的话,均未出现任何语法错误。

    遇事不决先 Google,还真找到很多结果,不过大多数都和 React.Component 有关,翻了半天一无所获,只好自力更生。打开 Chrome 开发者工具,勾上“Pause on Exceptions”,观察发生异常时的状况,一遍又一遍,我渐渐意识到,发生这个错误的时候,DatePicker 还未能在 webpack 的环境中完成注册。问题找到了!

    与其它编译类语言不同,JS 是动态语言,所有 JS 代码都是放到统一的环境里跑的,类的代码如此,import 也是如此。所以对于其他语言,比如 ActionScript、Java,循环引用,即 A 引用 B,B 也引用 A,是没问题的,因为类的代码都会编译到执行文件,执行的时候,都已经在环境中;而 JS 是边执行边置入环境,具体到我这里,在将父类 DatePicker 放入环境时,会先 import 子类 RangeDatePicker 的代码,而子类又会要求 import 父类的代码,父类的代码正在引入中,于是便产生了问题。

    想明白这点,后面就好办了,直接创建一个工厂类,把工厂方法放到里面执行,问题便解决了:

    import DatePicker from './DatePicker';
    import RangeDatePicker from './RangeDatePicker';
    
    export default {
      createDatePicker(el, options) {
        if (options.scattered) {
          return new DatePicker(el, options);
        } else {
          return new RangeDatePicker(el, options);
        }
      }
    }
    

    PS:当年写依赖注入和包管理工具的时候,就卡在这个地方,怎么都想不通,于是一直也没写完。没想到这些个浓眉大眼有头发的,也都这么不负责任,这种问题都不解决就搞出来让全世界人用了。

  • 一个超级诡异的 iOS Safari `position: fixed` 失效问题

    一个超级诡异的 iOS Safari `position: fixed` 失效问题

    今天前同事李某找我咨询 Hybrid 开发的问题,想起来大前天搞这个问题搞了一天,赶紧记下来,省得忘记。

    先说需求。东家让我做个日历组件,在手机 Web 上用。组件的样式是这样的,很多地方都可以见到,比如南航国航的客户端。

    日历控件需求图

    看起来并不复杂,事实上也是,基本上顺顺利利的开发完成,准备交付。这里有个伏笔,开发中我按老习惯,使用桌面 Chrome,和实际生产环境不太一样。不过我自然要去真机上测试,结果一测问题就出来了。

    因为组件需要全屏展示,所以我设置了如下的CSS:

    .date-picker {
      position:fixed;
      top:0;
      left:0;
      right:0;
      bottom:0;
      background-color: white;
      z-index:1024;
    }
    

    同时,对原本的 <input name="date">,我给它加上 readonly,避免弹出虚拟键盘。理论上,这样的就可以了。但实测时,不滚屏的时候,组件弹出时尺寸是准确的,盖满全屏;然则一旦滚屏,组件就会占据从页面最上方到当前最下面这截位置。大约相当于 position:absolte;top:0 的效果。

    Safari 截图
    手机截图
    如图,可以看到组件占据了全屏,但实际是从页面最上面开始的,定位有问题。用桌面 Safari 调试也可以看出来它的高度是 968,远大于正常的 667。

    这很诡异,上下左右全为0,是上古巨兽 IE6 都支持的做法。iOS Safari 虽然 Bug 多多,不应该连这个都有毛病啊。以 ios safari position fixed 为关键词 Google 之,结果 iOS Safari 历史不清白,当年 iPhone 刚出的时候的确有定位问题,于是虽有满屏的结果,但都不适用。

    然后我想到找其它库,比如 Bootstrap,它的 Modal 组件也是类似的效果。但是怎么测都正常,于是我只好一个样式一个样式修改,仍然没有结果。

    时间慢慢流逝,转眼已经凌晨2点了,就在我几欲放弃之际,突然发现,虽然组件弹出的时候定位有问题,但只要我点掉下面的完成,定位就会立刻恢复正常。

    手机截图
    注意,就是那个“完成”。

    问题至此已经明朗:在 iOS Safari 里,即使 <input> 设置了 readonly,它仍然可以获取输入焦点。获取输入焦点之后,虽然没有弹出虚拟键盘,但仍然是待输入状态。

    此时页面各种交互都是正常工作的,比如点击、滚屏。唯独 position:fixed 定位有问题。点击“完成”离开输入状态,Safari 自动刷新页面元素,定位就正常了。

    于是我在组件弹出后,自动 input.blur(),使其失去焦点,组件的尺寸便正常无误了。


    总结

    移动端 Web 开发总有各种各样稀奇古怪的问题。有些好解决,有些不好解决,比如这个问题,很难定位:

    1. 历史不清白,搜也搜不到
    2. 组件要求全屏,需要避免虚拟键盘,所以会改变默认行为
    3. 其它情况下都是好的

    我能想到的方案,就是想办法,用所有能用的工具,排除掉所有其它问题,最终还是能搞出来的。

  • 二次游日本

    二次游日本

    其实上次的游记还没写完……

    最早动来日本的念头是前几年,人民币汇率喜人,大家冲到日本买买买。不过那时候前司业务蒸蒸日上,确实也没有太多时间,拖着拖着拖到2015年,当时觉得时机合适了,还发了招募贴。结果年底查出来糖尿病,老婆脖子上也发现个瘤子,于是自然就放弃了。

    后来经过大半年的治疗调理,我俩都算渡过难关。于是国庆期间,去了九州。

    前两天种种因缘际会,被我发现海航往返札幌840的特价机票,于是没忍住,剁手,便有了这次旅行。


    这次旅行用的基本是2015年做的攻略,没料到赶上札幌雪祭、日本国庆节,酒店非常难定,机票省下的那点钱全交给酒店了。不过反过来说,确实住得不错,比九州那趟强多了。房间基本摆的开行李,大多数都有专门的温泉,泡汤泡得非常舒服。

    北海道的冬天没有想象中的冷。也许是两年多没在北京,有点忘记北方冬天的感受,衣服带太厚,其实根本用不上。这边的雪不是一般多,每天都在下,有时候多有时候少,下一会儿就放晴,过一会儿又是鹅毛大雪纷飞。可能正是因此才会有“粉雪”的美誉吧。

    小朋友似乎还太小,只爱疯玩傻玩,换言之,找坨雪,就够他玩 high 了。反倒是我们细心安排的行程,比如逛动物园、看雪灯,他觉得兴趣索然,无法全身心享受。让我有些失望


    玩耍过程中,心心念的,却是上班。如今离职2个半月,回顾刚离职时定下的计划,完成的不到一半,让我不禁对前途有些忐忑。出来玩很开心,但是玩耍中处处需要钱——还需要不少钱。想到将来更多想去的地方,不由得暗暗怀念起有班上的日子。

    回去之后好好工作,嗯嗯。

  • 七牛 Node SDK 会导致 Electron 启动新实例

    七牛 Node SDK 会导致 Electron 启动新实例

    如题,暂时不确定是哪里导致的。

    总之,在 Electron 的 main process 里调用七牛云 SDK qiniu.io.putFile(),会启动一个新实例,原本的上传会暂停。这个时候关掉新实例,上传会继续。当前文件上传完成后,下个文件又会启动一个新实例。如此反复。

    文档中的代码如下:

    qiniu.io.putFile(uptoken, key, localFile, extra, function(err, ret) {
          if(!err) {
            // 上传成功, 处理返回值
            console.log(ret.hash, ret.key, ret.persistentId);       
          } else {
            // 上传失败, 处理返回代码
            console.log(err);
          }
    });
    

    已开 issue

    估计要等春节后修复了。

    暂时可以用社区版 SDK 先顶上。

  • Electron + Vuex 导致视图无法自动刷新的问题

    Electron + Vuex 导致视图无法自动刷新的问题

    前几天开发 Meart 的过程中,碰到一个棘手的问题:

    1. 修改数据,视图不变
    2. 切换视图,再回到原来的视图,数据刷新
    3. 通过 Vue devtool 查看 Vuex,有数据提交
    4. 点击最新提交点,或者 “commit all”,之后所有操作正常

    因为我对 Electron 和 Vue 都不是很了解,所以这个问题困扰我很长时间。我尝试了各种办法,包括 .splice()let data = this.data; this.data = data;,但是都没有效果。后来实在没办法了跑到 Vue 论坛里发帖求助

    没想到很快就得到回复,对方虽然没用过 Electron,但答案看起来方向是对的:remote.getGlobal('var') 得到的对象不是一般意义上的对象,Vue 没有办法给它加上 getter/setter,所以无法实现响应式。

    我尝试了一下果然如此,用 require('data.json') 替换从主线程中取值,就一切问题都解决了。或者把 .getGlobal('data') 取出来的值,深度 Object.assign({}, source) 一下也可以。

    看来有必要补一下 ES5 里面 Object 新增函数的知识了。

  • FontAwesome 通过组合创建新图标

    FontAwesome 通过组合创建新图标

    FontAwesome 提供了很多好看的图标,使用 WebFont 嵌入页面更是简单又好用,所以我基本上一直用它。不过有时候还是觉得不太够用,这就需要复合使用多个图标。

    下面是个例子,我在图标的右下角增加一个圆形加号,表示增加、创建。

    See the Pen add icon by Meathill (@meathill) on CodePen.

    这里有两个选择:

    1. FontAwesome 提供了一个堆叠图标的样式:fa-stack,可以堆叠任意多的图标。
    2. 因为它实际上只占用了 :before,所以也可以使用 :after 来容纳新增的图标。如果只需要两个图标,这个方式更简单。
  • 第四次游台湾

    第四次游台湾

    前天从台北回到广州,准备过年。这是我第四次游台湾,原本计划一家五口一起,后来老婆家里出事,没走成,变成我带二大一小出游。运气不错,大部分行程都符合甚至超过预期,顺顺利利开开心心玩完九天八夜,回到家,总结一下。

    有些朋友问,“你都去几次台湾了?”“为啥老去台湾啊?”,这里也算一并作答。

    风景秀丽

    合欢山

    清水断崖

    台湾有很多秀丽的风景,比如清水断崖的碧蓝海水,合欢山的瑰丽雪峰。这些风景可能说不上举世无双艳压寰宇,但也绝对值得一去,值得一看。

    所谓“宝岛”,绝非浪得虚名,台湾全岛,旅游资源开发到位,无论去到哪里,都可以轻松安排下三四天的行程;如果跑在路上嫌累,也有很多地方可以呆着度过几天闲适的时光。

    软硬件条件上佳

    太鲁阁燕子洞

    路旁的绯野樱

    国内的自然风光当然不逊海外,然而把人文的软硬件都算上,国内就没啥优势了。

    先说硬件,台湾景区公路发达,规格高设施全;园区内道路平整,无障碍设施齐备,厕所干净无异味有卫生纸。文明社会应有尽有。

    再说软件,台湾收费的地方并不多,即使收费,多半也并不昂贵,景区门票几十块,里面基本上不再二次收费。吃饭更是让人安心,价格可能比市区稍贵一些,但绝对在合理范围内,不用担心被坑被宰。

    PS:目前我觉得性价比最低的是阿里山,以及阿里山小火车……

    便宜

    台铁便当

    其实这点才是最重要的……

    大约因为离得近,而且去的人相对较少(尤其是最近),机票相当便宜,单人往返含税从广州香港走大约不到1.5k。

    住的话台北台中大城市不会太便宜,价格基本参考国内同档次的各级酒店,丰俭由人;旅游区多半住民宿,价格就很亲民了,400、500就能住的不错,700、800就很好了,愿意多花点钱的话,1000+可以住非常大或者非常有特色的房间,配以非常漂亮的窗外景色。

    吃饭也不贵,低端盒饭十几块,高端点的20、30就能吃饱,而且菜品质量我觉得要更胜国内。喜欢吃就换着花样找不同饭店去尝试,大可以放心点菜,我觉得价格远低于国内。

    自由,多元

    补习班的野望

    考试和补习班是海峡两岸学子共同的命运

    与国内各种管制不同,台湾是自由社会,尤其在文化方面。我去台湾必做的事情之一,便是逛漫画书店。这里要插一句,诚品书店当然值得一逛,不过买过几次书都比较失望,比如会竖排版,或者翻译质量很差,所以我还是更喜欢杂志疯这种主营漫画的书店。

    因为自由,所以就不必摆出道貌岸然的样子去扫黄,所以就会有18x的书架,每每令人流连……不过都好贵啊……

    其他

    城隍庙灯市

    我老婆喜欢去买衣服,貌似比国内便宜,而且服务很好,售货员会很认真的帮你挑选、搭配衣服,所以一不小心就买很多。

    我大姨子每年去龙山寺许愿求签还愿。说到这个求签啊,目前求了几次都很灵,很灵很灵,全中。

    我爸感慨:“国外月亮确实比较圆啊……”

    我妈:“还是有钱好啊,可以出来玩。”


    好,大概这么多。未来,我想还会保持一年一次的频率。有兴趣去台湾的朋友可以联系我,我可以帮忙推荐路线和包车司机。


    一些碎碎念

    过年了,吃好喝好

    对比国庆去日本,台湾还是性价比更高,订酒店的时候就各种觉得便宜。蔡英文上台后,与大陆互相看不顺眼,造成陆台关系紧张,直接影响了陆客前往台湾旅游。打车的时候,出租车司机都在骂。

    这个世界,仍然在按照自己的规律发展。人类的道德,主观期望,在这规律面前,不值一提。所以即使我喜欢台湾,我也不得不承认,大陆更有希望;游玩几日之后,必须回到大陆,继续忍受糟糕的空气、脑残的司机、脏兮兮的环境,一边尽力工作,想方设法把生活过得更好。

    希望台湾能一直坚持这样美好下去。希望大陆能学习改造,变得美好。

  • node.js 复制文件最快的方法

    node.js 复制文件最快的方法

    Subway

    最快的方法

    var fs = require('fs');
    
    fs.createReadStream('test.log').pipe(fs.createWriteStream('newLog.log'));
    

    改进使其可以接受回调

    function copy(source, to, callback) {
      var read = fs.createReadStream(source);
      source.on('error', function (err) {
        done(err);
      }
    
      var write = fs.createWriteStream(to);
      write.on('error', function (err) {
        done(err);
      }
      write.on('finish', function () {
        done();
      }
      read.pipe(write);
    
      function done(err) {
        if (err) {
          throw err;
        }
        callback();
      }
    }
    

    继续添加 Promise,并且用 ES2015 的写法

    function copy(source, to) {
      return new Promise( resolve => {
        let read = fs.createReadStream(source);
        source.on('error', err => {
          throw err;
        });
    
        let write = fs.createWriteStream(to);
        write.on('error', err => {
          throw err;
        });
        write.on('finish', ()=> {
          resolve();
        });
    
        read.pipe(write);
      });
    }
    
    // example
    copy('a.txt', 'b.txt')
      .then( () => {
        console.log('copy success');
      })
      .catch( err => {
        console.log('copy error: ', err);
      });
    

    来源:StackOverflow

    Fastest way to copy file in node.js

  • Bootstrap 4 发布 alpha 6

    Bootstrap 4 发布 alpha 6

    这两天直播写 Meart 有点上瘾,博客好久没更了,公更私更都没更。刷推看到这则公告,小更一下吧。
    图文无关,马上又要去台湾了,翻出来一张台湾公安局的照片阵楼。

    Bootsrap 团队宣布 Bootstrap 4 推出 alpha 6 版本

    如题。Bootstrap 4 alpha 已经是第六个版本了,我最近一直在用 alpha 5,感觉没有太大问题。

    这次更新主要包含三个部分:

    1. 拥抱 Flexbox

    这个版本放弃了 IE9,从而可以彻底拥抱 Flexbox(之前的版本中,Grid 多半使用 display:tablefloat 实现,如果要启用 Flexbox,需要单独引用一个 bootstrap-flex.css)。

    根据 CanIuse 显示,目前除了 IE 系列,大部分浏览器均已全面支持 Flexbox。

    这次修改之后,绝大部分组件均已全面转向 Flexbox。

    2. 加强布局和显示样式

    增加了一批用于调整布局和显示的 class,这样从 Bootstrap 3 迁移也会方便一些。

    这块儿我暂时没用到,没啥可说的。

    3. 提升 Grid 布局

    增加了更多的可控样式,可以调整 Grid 布局中的边距。

    4. 升级导航栏

    之前的版本中,导航栏还是个半成品,现在虽然不敢说是“完成版”,单也差不多了。

    这个用途可能比较大,不过还没研究。


    下次再发布就是 beta 版了,感觉离正式推出不远了。

    Staticfile.org 搜索仓库还没更新,文件已经更新了。


    感谢原作者和贡献者的努力,让我们得以用上越来越好用的开源框架,节省大量时间和精力,可以集中于更有产出的事情。


    原文:Bootstrap 4 Alpha 6