标签: js

  • 第一场 GitChat 总结

    第一场 GitChat 总结

    开始之前,先做广告吧。

    GitChat 分享 《JavaScript 异步开发全攻略》

    为解决异步函数的回调陷阱,开发社区不断摸索,终于折腾出 Promise/A+。它不增加新的语法,可以适配几乎所有浏览器;以队列的形式组织代码,易读好改;捕获异常方案也基本可用。这套方案在迭代中逐步完善,最终被吸收进 ES2015。不仅如此,ES2017 中还增加了 Await/Async,可以用顺序的方式书写异步代码,甚至可以正常抛出捕获错误,维护同一个栈。可以说彻底解决了异步回调的问题。 现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,更有各种降级适配策略。Node.js 7+ 则实装了 Await/Async。如果您现在还不会使用,那么我建议您尽快学习一下。

    下次直播分享 前端面试攻略:JavaScript 排序与搜索

    从事前端开发的同学很多从页面仔入门,比如说我,自学比例很大,有些时候会无意中忽视一些基础,比如算法、数据结构。这些欠缺在某些时候就会显得很致命,比如说面试,或者处理大量数据的场景。所以希望这样的一场分享能够帮助大家夯实原本不太扎实的基础,将来的开发之路更加顺畅。

    目前早鸟票发送中,7月13日前门票5折,19日前75折,开播当日恢复全价。

    (更多…)

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

  • 助力Handlebars

    助力Handlebars

    单页应用中,模板引擎是必备工具。这方面的选择比较丰富,旧轮子不断获得改进,新的轮子也不断被发明出来。我比较喜欢Handlebars,原因如下:

    1. 模板语言简单。继承自mustache,用{{content}}标记出要替换的内容即可。
    2. 包含逻辑判断和循环。在mustache基础上加强了这个部分,使得代码更易读。
    3. 不支持复杂的逻辑,尤其是嵌套JS。我认为这是模板引擎的关键,任何复杂的逻辑放在表现层都会使得系统不稳定。
    4. 支持预编译。可以加快实际运行的速度。
    5. 提供扩展功能,可以方便地增加自定义功能。这也是本文重点。

    简单,是Handlebars的优势;但是原始版有点过分过头,它既缺少Angular模板中的过滤器,难以格式化输出数据;也没有足够的逻辑判断。使得我们必须在使用模板前进行大量预运算,逻辑据处理成模板能辨识的标记,把数据格式化成用户能识别的内容,这与代码复用的原则相违背。所幸Handlebars提供好用的扩展接口,给我们增添功能,适配业务逻辑的机会。

    (更多…)

  • 导出 Table 数据并保存为 Excel

    导出 Table 数据并保存为 Excel

    最近接到这么个需求,要把 <table> 显示的数据导出成 Excel。类似的需求并不稀罕,过去我通常用 PHP 输出 .csv 文件。不过这次似乎不太合适:作为数据源的表格允许用户有一些筛选和排序的动作,与原始数据显示有区别,传递操作比较麻烦;另外 .csv 文件的功能受限严重,难以扩展。所以我准备尝试下别的做法。

    Google之,发现 HTML5 又成了一座分水岭。之前在IE浏览器下,用户可以利用 ActiveXObject 创建 Excel.application 对象来处理。后来 Excel 开放标准,可以导出 xml 格式的文件,dataURI 就有了用武之地,导出 <table> 数据并保存为 Excel 有了更好的选择。

    (以下内容与 StackOverflow中的答案有重合,那个3条赞同的我认为是最佳答案,可惜我没法顶他……)

    准备工作

    1. 创建一个空白的Excel文档
    2. 另存为“XML表格”,XML 格式
    3. 好了,模版搞定

    或者,直接复制下面一段(这一段我使用了Handlebars模版,以便将来填充数据)

    template = ‘<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>{{worksheet}}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>{{#each tables}}<table>{{{this}}}</table>{{/each}}</body></html>';

    复制表格数据

    复制数据比较简单了。如前面模版所示,这里我很野蛮的直接复制 <thead><tbody> 的全部代码,填充内容。当然为了体现用户操作,我只复制显示的 <tr>。这里需要注意的是,jQuery 判断一个dom 是否处于显示状体基于以下3点:

    1. display:none
    2. 表单元素,type="hidden"
    3. 宽高为0
    4. 父级以上节点不显示,自己也不会显示

    所以,不能先 .clone().find(':hidde').remove(),因为添加到主 Dom 树之前,节点宽高都是0,也就会被认为还没显示,这下就都干掉了。

    输出内容

    套用模版之后,我们就有了完整的表格数据。接下来,我们需要把其转换成 Base64 格式,以便套用 dataURI 输出。于是便要使用 btoa 这个函数(将二进制数据转换成 base64 格式的字符串),不过注意,这个函数不能直接转换普通 unicode 字符,不然大多数浏览器都会抛出异常。所以需要先经过两步转换:

    function base64(string) {
      return window.btoa(unescape(encodeURIComponent(string)));
    }

    (MDN 还推荐了 另外一种做法,通过 Typed Array 做中介,我没有实操,有兴趣的可以试下。)

    然后配上 data 头和 mimetype,就可以触发下载了:

    var uri = 'data:application/vnd.ms-excel;base64,';
    location.href = uri + base64(template(tables));

    提升体验

    貌似到这里就完成了,不过作为一名挂职产品总监的码农,我很难容忍下载的文件文件名是“下载”,而且还没有扩展名(Windows 8 下,Windows 7 和 Mac 下会有.xls的扩展名,我认为和装的软件注册 mime 类型有关)。

    这是个用在内部管理后台的需求,我之前曾要求大家必须使用 Chrome 访问后台;而且我知道,Chrome 已经支持 <a> 里的 download 属性。那么这就好办了,因为 onclick 事件会先于系统默认行为触发,所以我可以在这个事件的处理函数中将生成的 Base64 放在被点击按钮的 href 里,并将其 download 属性设为容易理解的“某年某月末日至某年某月某日广告数据分析.xls”。至此,此项功能宣告圆满。

    HTML部分(使用了Bootstrap和Handlebars):

    <a href="#" title="点击下载" class="btn btn-primary export-button" download="{{start}}至{{end}}广告数据分析.xls"><i class="icon-download-alt icon-white"></i> 导出</a>

    JavaScript 部分

    tableToExcel: function (tableList, name) {
      var tables = []
        , uri = 'data:application/vnd.ms-excel;base64,'
        , template = Handlebars.compile('<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>{{worksheet}}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>{{#each tables}}<table>{{{this}}}</table>{{/each}}</body></html>');
    
      for (var i = 0; i < tableList.length; i++) {
        tables.push(tableList[i].innerHTML);
      }
      var data = {
        worksheet: name || 'Worksheet',
        tables: tables
      };
      return uri + base64(template(data));
    },
    exportHandler: function (event) {
      var tables = this.$('table')
        , table = null;
      tables.each(function (i) {
        var t = $('<table><thead></thead><tbody></tobdy></table>');
        t.find('thead').html(this.tHead.innerHTML);
        t.find('tbody').append($(this.tBodies).children(':visible').clone());
        t.find('.not-print').remove(); // not-print 是@media print中不会打印的部分
        t.find('a').replaceWith(function (i) { // 表格中不再需要的超链接也移除了
          return this.innerHTML;
        });
        table = table ? table.add(t) : t;
      });
      event.currentTarget.href = Dianjoy.utils.tableToExcel(table, '广告数据');
    }

    尾声

    说是圆满,其实也不尽然,因为 URL 有 2M 的长度限制,遇到真正的大表仍然可能出问题(我没实测)。

    最后例行吐槽:老板(领导)想提升工作效率,必须考虑员工的日常软件:不许用乱七八糟的浏览器,统一 Chrome;360 一定禁用(最近遇到 N 起升级 Chrome Dev 30 版导致各种 bug 的问题);全部装 Windows 8(自带杀毒,几乎所有外设秒配)。能做到这几点,公司办公效率提升1倍不止。

  • JavaScript实现命名空间(绑定在jQ)

    不支持命名空间一直是JS开发里比较严重的问题。不过大家想出了各种手段来绕过这个坎,比如YUI的namespace。可惜的是jQuery尚未提供一个合适的解决方案,不过这并不难,可以人肉给它添加这个方法。稍加搜索,找到两个地址介绍此方法,附在最后。

    (更多…)

  • 三门问题

    最近一段日子“三门问题”在NGA上闹得很凶,原题是这样的:

    参赛者会看见三扇关闭了的门,其中一扇的后面有一辆汽车,选中后面有车的那扇门就可以赢得该汽车,而另外两扇门后面则各藏有一只山羊。当参赛者选定了一扇门,但未去开启它的时候,节目主持人开启剩下两扇门的其中一扇,露出其中一只山羊。主持人其后会问参赛者要不要换另一扇仍然关上的门。

    问题:换另一扇门会否增加参赛者赢得汽车的机会率

    (更多…)
  • jQuery笔记

    jQuery笔记

    jQuery

    这篇日志用来记录jQuery使用过程中的心得体会。

    (更多…)

  • 禁用鼠标滚轮事件

    禁用鼠标滚轮事件

    接到一个需求,要在flash里面使用鼠标滚轮控制壁纸地板的替换,这本身是一个小需求,但是在网页测试中发现滚动鼠标滚轮时,网页也会跟着滚动,这样鼠标就没法固定在滚动元件的上面,对用户体验造成了不良影响。

    (更多…)

  • Chrome下填充10w个div实验

    Chrome下填充10w个div实验

    首先需要解决的是浏览器里有大量绝对定位的元素的效率问题。flash当中可以用一张位图来绘制所有的点击记录,为了能让html版尽量兼容更多的浏览器,采用div来承担这个任务是必要的。所以先来就是测试页面当中包含多少div,交互效果可以接受。

    (更多…)