分类: js

有关 JavaScript 的技术文章和行业分析文章。

  • Safari 下 Date 不支持”2018-01-01 00:00:00“

    Safari 下 Date 不支持”2018-01-01 00:00:00“

    前两天发现一个小程序的问题,Android 正常,iPhone 出错。我们都知道,Debug 的关键在定位,如果是某些特殊环节,不常见的错误,就会浪费很多时间。

    这个 Bug 也是如此,反复拉锯之后终于发现,问题出在下面这句:

    let a = new Date(`${date} 00:00:00`);
    

    date 是服务器端返回的值,我是把它和后面的 00:00:00 连起来,记作某天零点零刻,和今天的零点零刻做减法,计算日期差,并按照日期差来决定接下来的逻辑。这段代码在开发工具(包括 Mac)、Android 手机上运行都正常,只有在 iPhone 上不正常,于是我打开 Safari——苹果这点做得不错,桌面版 Safari 环境和 iOS 几乎没有差别,该出的问题一定会出——果然复现了这个问题。

    按照规范,中国的日期格式是:“2018/01/01”,Safari 只支持这个格式。而 2018-01-012018-01-01T00:00:00 ISO 格式,Safari 也支持,但是会以格林威治时间为准,和我们有8小时的时差。Chrome 和 Android 内嵌的 WebView(基于 Blink 或者 Webkit)则都支持,所以在本地和 Android 手机上没有问题。

  • 解决 [Vue warn]: You may have an infinite update loop in a component render function

    解决 [Vue warn]: You may have an infinite update loop in a component render function

    今天写着写着,突然发现控制台里有错误:

    [Vue warn]: You may have an infinite update loop in a component render function
    

    这个问题很奇怪,之前从来没有遇到过。如果是我自己主导的项目,倒也好办,慢慢 debug 就是;偏偏在公司的项目里遇到这个问题,而公司项目的体系结构很复杂,我还没完全掌握。更恼火的是,因为体系复杂,debug 也非常困难,再加上尚无测试框架,这个难搞啊……

    好死不死的,当时是下午3、4点钟,正好到了肚饿的时刻,结果又落入低血糖状态,真是屋漏偏逢连阴雨,船小又碰顶头风,饿得我脑仁生疼……

    不过终于还是被我 Google + debug 出来。事实上是这样的,在 v-for 循环当中,如果用方法或者计算属性对 vm.$data 的属性进行操作,理论上,可能因为修改到循环对象,诱发无限循环。此时 Vue 就会发出警告(并不是真的已经无限循环了)。

    例如这样一个组件,它里面是用 :checked + <label> 实现的一组按钮。它有以下功能:

    1. 为了能够分组,需要设置它们的 name 属性
    2. 为了能够用 <label> 控制 <input>,需要给 <input> 设置 id
    3. 按钮可以被删除

    于是我选择这样做:

    <template>
    <div>
      <template v-for="(item, index) in items">
        <input type="checkbox" :name="'my-component-' + selfIndex" :id="getID">
        <label :for="getID(false)">
        <button type="button" @click="remove(index)">&times;</button>
      </template>
    </div>
    </template>
    
    <script>
    let count = 0;
    
    export default {
      data() {
        return {
          selfIndex: 0,
          itemIndex: 0,
        }
      },
      methods: {
        getID(increase = true) { // 注意,问题就出在这里
          if (increase) {
            this.itemIndex++;
          }
          return `my-component-${this.selfIndex}-${this.itemIndex}`;
        },
      },
      beforeMount() {
        this.selfIndex = count;
        count++;
      }
    }
    </script>
    

    这里,为了能生成唯一 ID,我选择每次循环都对 vm.itemIndex++,这就会出现前面说的问题,存在隐患。

    解决的方案有两种,一种是把 itemIndex 也放在局部变量里,使它不直接关联在组件上;另一种则是写一个全局的唯一 ID 生成函数,然后引用进来。原理都是一样的。


    这两天听评书《乱世枭雄》,学到一句话“拉屎脸朝外”,形容讲义气,不知道咋联系的……

  • MediaElement 笔记

    MediaElement 笔记

    贵司官网需要放视频,于是需要用播放器。然后就产生如下需求:

    1. 协议好,支持免费商用,最好 MIT
    2. 支持 SRT 字幕
    3. 支持尽可能多的平台

    经过筛选,最后选定 MediaElement,一看 WordPress 在用基本就放心了。然后就开始踩坑之旅。当然,有些是我的问题,不过也有不少是文档没说清楚。这里记录一下。


    全屏和弹性宽度

    当前版本 4.2.5 有个 Bug,如果设置宽度为 100%,那么从全屏恢复到内嵌状态时,会留着全屏的宽度,撑开页面。如果设定一个特定的宽度就不会。但我们的页面是响应式的,需要允许它随页面变化而变化。

    这个时候只好手工来做,我的选择是让它 100%,然后侦听从全屏返回的事件,发生后就把宽度恢复。另外,因为 fullscreenchange 事件还没有统一标准,所以不同浏览器里的事件名称也不一样,更不爽的是,IE 下是驼峰,其它浏览器是全小写,我也懒得再写函数转化了,反正就3种情况,全写一遍好了。

    // fix MediaElement's issue
      let fitVideo = function() {
        setTimeout(() => {
        $('.mejs__container').width('100%')
          .find('video').width('100%')
          .end().find('.mejs__layers')
          .children().width('100%');
      }, 50);
    };
    if ('onwebkitfullscreenchange' in document) {
      document.onwebkitfullscreenchange = function() {
        if (!document.webkitFullscreenElement) {
           fitVideo();
        }
      };
    } else if ('onmozfullscreenchange' in document) {
      document.onmozfullscreenchange = function() {
        if (!document.mozFullScreenElement) {
          fitVideo();
        }
      };
    } else if ('MSFullscreenChange' in document) {
      document.MSFullscreenChange = function() {
        if (!document.msFullscreenElement) {
          fitVideo();
        }
      };
    }
    

    默认字幕

    Web 标准是有默认字幕的,<track defualt> 即可。但是 MediaElement 没有,最后在 StackOverflow 上找到答案,原来有个未记录在文档里的初始化属性可以控制:

    let player = new MediaElementPlayer('intro-video', {
      startLanguage: 'cn', // 这里的名字应该和 <track> 种的 srclang 属性一致
    });
    

    插件与生态

    MediaElement 把一些有用但不太常用的功能抽出来做成了插件,放在 MediaElement Plugins。不过看起来这个项目有些属于维护,使用的时候很多问题。比如,直接在 features 里开启特定功能,即使对应的插件没有加载,也不会报错,就是没反应,很令人迷茫。

    避免发起又取消请求

    网站上线后,老板发现一个问题:从 Chrome 的 Network 面板可以看到,网页打开后,对视频文件发起了一到多个不等的请求,并且都取消了。

    经过研究,我认为这个请求是 <video> 发起的,因为 preload 的默认值是 auto(参见:MDN),也即打开页面就预加载。而此时 MediaElement 被初始化,为了正确显示 UI,它会把 <video> 标签挪到自己创建的 <div> 里,这个过程就会导致“预加载 -> 取消”。因为视频默认不播放,所以我给 <video> 加上了 preload="none",问题解决。


    其它大体上文档都能找到,就不多说了。

  • Vue 里 $mount 方法的作用

    Vue 里 $mount 方法的作用

    今天由同学问道:“vue文档里说如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。这个方法在项目中有什么用呢,是一个组件可以挂载到多个dom上就这一个作用吗?”

    我的答案如下:

    有些时候你创建 vm 的时候 DOM 可能还没准备好,或者写 vm 的时候不确定它要挂在哪个 DOM 上,这个时候就有用了。

    当项目大的时候,很多情况是开发的时候估计不到的。比如 A 类里面想使用一个 B 类,但是 B 类有 10 个子类,对应不同场景,你写 A 类的时候根本不知道产品中会使用哪个 B 的子类,所以就要想办法把控制权交出去,这就叫“控制反转”——即控制权从开发者的手上,交到使用者的手上。

    我觉得 $mount 这里也是类似的,就是你写了一个 Vue 类,但是不能控制使用场景。所以就要有一个 $mount 方法,由使用者控制,绑到目标 DOM 上。

    他又问:“懂了,还有一个问题是一个组件能不能利用$mount属性挂载到多个dom上,我刚刚试了一下是可以渲染,这种用法常见吗??”

    我答:

    我没这么做过,不过我认为是合理的。

    我觉得这个跟 Vue 的定位有关。Vue 和 React、Angular 不一样,后者是大公司企业级产品。Vue 其实很想取代 jQuery,所以这方面其实跟 jq 的插件是很相似的。

  • Vue 笔记:事件

    Vue 笔记:事件

    onload

    需要侦听 load

    <template>
      <img src="/path/to/img" @load="onLoad">
    </template>
    
    <script>
    export default {
      methods: {
        onLoad(event) {
          // do something
        }
      }
    }
    </script>
    

    drop

    需要阻止 dragover 的默认行为,不然不触发。

    <template>
      <div class="drop-area" @dragover.prevent="onDragOver" @drop="onDrop">
    </template>
    
    <script>
    export default {
      methods: {
        onDragOver(event) {
          // 此时仍然可以处理
        },
        onDrop(event) {
          console.log(event.dataTransfer);
        }
      }
    }
    </script>
    
  • JavaScript 异步开发全攻略

    JavaScript 异步开发全攻略

    之前在 GitChat 做过一次分享:《JavaScript 异步开发全攻略》。在我看来,原始内容可能不够完美,但通过后来的维护,可以把它打磨得越来越好。昨天在 SF 上回答了一个 Vuex 的 action 里使用 Promise 的问题,然后就想去补充一下这方面的内容。结果发现 GitChat 竟然不支持编辑文章,只能把内容发给运营人工修改。

    索性把内容放到 Gitbook 上好了,反正 GitChat SEO 也不做,文章也自然移动到第二页了。试了试,没有被墙,很好。于是简单整理了一下,上传。

    欢迎阅读,欢迎分享,因为我会不时更新新内容,请关注 star:

    JavaScript 异步开发全攻略

  • Node.js 8 中的 util.promisify

    Node.js 8 中的 util.promisify

    Node.js 8 于上个月月底正式发布,带来了很多新特性。其中比较值得注意的,便有 util.promisify() 这个方法。

    如果你已经很熟悉 Promise,请继续往下看。如果你还不熟悉 Promise,可以先跳过去看下下章:Promise 介绍

    util.promisify()

    虽然 Promise 已经普及,但是 Node.js 里仍然有大量依赖回调的异步函数,如果我们把每个函数都封装一遍,那真是齁麻烦齁麻烦的,比齁还麻烦。

    所以 Node.js 8 就提供了 util.promisify() 这个方法,方便我们把原来的异步回调方法改成支持 Promise 的方法,接下来,想继续 .then().then().then() 搞队列,还是 await 就看实际需要了。

    我们看下范例,让读取目录文件状态的 fs.stat 支持 Promise:

    const util = require('util');
    const fs = require('fs');
    
    const stat = util.promisify(fs.stat);
    stat('.')
      .then((stats) => {
        // Do something with `stats`
      })
      .catch((error) => {
        // Handle the error.
      });
    

    怎么样,很简单吧?按照文档的说法,只要符合 Node.js 的回调风格,所有函数都可以这样转换。也就是说,只要满足下面两个条件,无论是不是原生方法,都可以:

    1. 最后一个参数是回调函数
    2. 回调函数的参数为 (err, result),前面是可能的错误,后面是正常的结果

    结合 Await/Async 使用

    同样是上面的例子,如果想要结合 Await/Async,可以这样使用:

    const util = require('util');
    const fs = require('fs');
    
    const stat = util.promisify(fs.stat);
    async function readStats(dir) {
      try {
        let stats = await stat(dir);
        // Do something with `stats`
      } catch (err) { // Handle the error.
        console.log(err);
      }
    }
    readStats('.');
    

    自定义 Promise 化处理函数

    那如果现有的使用回调的函数不符合这个风格,还能用 util.promisify() 么?答案也是肯定的。我们只要给函数增加一个属性 util.promisify.custom,指定一个函数作为 Promise 化处理函数,即可。请看下面的代码:

    const util = require('util');
    
    // 这就是要处理的使用回调的函数
    function doSomething(foo, callback) { 
      // ...
    }
    
    // 给它增加一个方法,用来在 Promise 化时调用
    doSomething[util.promisify.custom] = function(foo) { 
      // 自定义生成 Promise 的逻辑
      return getPromiseSomehow(); 
    };
    
    const promisified = util.promisify(doSomething);
    console.log(promisified === doSomething[util.promisify.custom]);
    // prints 'true'
    

    如此一来,任何时候我们对目标函数 doSomething 进行 Promise 化处理,都会得到之前定义的函数。运行它,就会按照我们设计的特定逻辑返回 Promise 实例。

    我们就可以升级以前所有的异步回调函数了。

    (更多…)

  • 小试 Element UI

    小试 Element UI

    不确定写多长,写先结论吧:暂时不推荐使用。原因如下:

    1. 影响使用的小 Bug 有点多
    2. 需要重新学习一门语言


    接下来详述。

    从前司离职之后,我开始更新技术栈。离开惯用的 Backbone,考虑再三,投入 Vue 怀抱。选择 Vue,而不是竞品 Angular、React 有三个理由:

    1. 文档友好,社区活跃。
    2. 模块拆分的很好,学习曲线平缓。
    3. 基于标准化技术,可以最大限度的避免浪费。

    不过实操之后发现,Vue 与我惯用的 Bootstrap 有些冲突,主要在于:

    1. Bootstrap 对过渡效果和切换的操作依赖于样式,比如 .active.in。Vue 在处理模板时会把当前样式先缓存起来,然后根据数据增删绑定的样式。此时就可能出问题,tab 页切不动或者动画突然打断之类的。
    2. Bootstrap 会广播特定的事件,这些事件无法被 Vue 捕获,只能在 mounted() 的钩子里手工绑定。

    于是我觉得,既然根基(jQuery)变了,最好把整条线都更新了吧。左右看了看,准备先试下 Element UI。这是饿了么推出的基于 Vue2.0 的组件库,目测组件齐全,文档详细,而且直接以 2.0 为基础,符合我追新的想法。

    实际用了之后……唉……有点……遗憾。项目地址

    首先,Element UI 把所有组件都封装了,包括布局,比如 <el-row><el-col>,我觉得这样太过了。从现实经验来看,布局元素几乎不可能够用,别人总要补充一些。封装的元素我不太知道最终生成的代码是什么样的,也就不好操作,总不能审查元素一个一个看吧?——对了,Element 的文档里缺少样式列表,也是个问题。

    封装的另一个问题,所有元素都要通过后期渲染,总让我感觉不舒服。以及,我几乎无论干什么都要查文档,几乎没法直接动手,这和我选择 Vue 的初衷是相违背的。

    接下来,小 Bug,有点多。除去布局和提示之类,我只用到3个组件:<el-button><el-table><el-pagination>,结果就遇到4个 bug,浪费很多时间去调试,有两个我给他们开了 issue,还有两个懒得弄了。这里列一下吧:

    1. <el-button :loading="scope.row.fetching"> 无法把 loading 绑定到数据的 .fetching 属性上
    2. <el-pagination> 设置 total 不更新视图
    3. <el-pagination> 更新 total 之后再次广播 current-change 事件,导致重复刷新
    4. <el-table> 里每行的 ref 属性没法正确生成数组

    2017-04-04 更新:

    经过 issue 沟通,我的理解的确有些问题,

    1. Vue 绑定属性的时候,如果该属性层次较深,比如 a.b.c,就不会修改它的 getter/setter,于是会失去响应式更新的特性;同时也不会报错。
    2. <el-table> 不是通过 v-for 渲染的,自然 ref 的表现也就正常了。

    可能别的组件很健壮吧,我运气不好。

    总之,我觉得就目前这个版本,1.2.5,来看,Element UI 还没到让人放心用并且用得好的程度。

    下一次我可能会选别家的再试下,或者继续用 Bootstrap 然后自己拼些小组件出来——我这次就是想找个有 loading 的 button 才找 UI 库的。


    啊,最后,还是感谢 Element UI 团队,感谢饿了么。希望你们再接再厉,相信将来这套库会更好。

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

  • 七牛 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 先顶上。