Tag Archives: javascript

Hello World

桑基图的绘制

    首先明确一点,这里说的桑基图,是参照百度echarts的样式的这种桑基图。这种图,既可以标示数量多寡,又可以标示相关之间的关联关系,比单纯的饼图和折线图可以承载更多的信息

    刚开始准备用python来绘制,但搜了很多资料都有些不着边际,好不容易找一个看起来靠谱的里做了一版,绘制出来了,样式却和百度这个差别很大
    于是继续找资料,找到了一个能实现需求的包,装上以后,运行。。。生成的居然是一段html,然后打开看,实际还是调的echarts。。。
    既然逃不过echarts索性改改思路吧:python处理数据,生成数据json串,保存到服务器
    然后用php做一个承接页,读取这个json串
    将承接页的链接加到统计邮件里,点链接打开统计图,还带了交互功能,体验更好,嘿嘿
    示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//nodes: 所有会出现的节点
//links: 所有的关系线,source是左边节点,target是右边节点
var dataJson = {
  "nodes":[{"name": '玉米'}, {"name": '大豆'}, {"name": '蛋白质'}, {"name": '淀粉'}, {"name": '脂肪'}, {"name": '水'}, {"name": '能量'}],
  "links": [
      {"source": '玉米', "target": '蛋白质', "value": 10},
      {"source": '玉米', "target": '淀粉', "value": 78},
      {"source": '玉米', "target": '水', "value": 2},
      {"source": '玉米', "target": '脂肪', "value": 10},
      {"source": '大豆', "target": '蛋白质', "value": 30},
      {"source": '大豆', "target": '淀粉', "value": 10},
      {"source": '大豆', "target": '脂肪', "value": 57},
      {"source": '大豆', "target": '水', "value": 3},
      {"source": '蛋白质', "target": '能量', "value": 40},
      {"source": '蛋白质', "target": '水', "value": 60},
      {"source": '淀粉', "target": '能量', "value": 60},
      {"source": '淀粉', "target": '水', "value": 40},
      {"source": '脂肪', "target": '能量', "value": 50},
      {"source": '脂肪', "target": '水', "value": 50}
   ]
}
echarts.init(document.getElementById("myChart")).setOption(
    (option = {
      title: {
        text: 'xxxx'
      },
      tooltip: {
        trigger: 'item',
        triggerOn: 'mousemove'
      },
      series: [
        {
          type: 'sankey',
          data: dataJson.nodes,
          links: dataJson.links,
          emphasis: {
            focus: 'adjacency'
          },
          lineStyle: {
            color: 'gradient',
            curveness: 0.5
          }
        }
      ]
    })
  );

交互体验可以直接去echart官网试试:https://echarts.apache.org/examples/zh/editor.html?c=sankey-simple
———-
转载请注明出处:http://www.jiangkl.com/2022/01/sankey

Hello World

h5多图上传功能实现

    这几年,前端技术日新月异,浏览器/h5获得的权限越来越多。这里使用h5页面做一个相册的上传功能,读取手机相册、上传到服务器,要求能够一次选择多张、上传前能看到预览、并能显示各个照片的上传进度。看到这里,有朋友肯定说:这有何难,input-file标签早就已经有multiple属性,加了它就能选多图了。呵呵,看起来简单,实际做起来问题还挺多。这里按顺序列一几个卡主的地方和解决方法:
1. 多图上传
    看到这里,肯定有人说了,不是有multiple属性吗?
    确实有,可实际用起来,不一样的
    iphone好办,全都支持一次选择多张图片
    Android手机有点麻烦,部分手机、部分浏览器不支持一次选多图;而且即便支持选多图,交互方式也不一样。比如我手头这台华为/荣耀,微信内置浏览器只能一次选一张图,改用系统浏览器,虽然可以选多张,但是需要长按,对于不熟悉的用户,可能发现不了这个功能,这就需要在应用里加指引;旁边同事的锤子就还好,微信内置浏览器就能选多图,和iphone的体验类似
2. 上传前的预览
    这个功能说白了,就是怎么拿到本机图片文件、放到img标签的src里。前端浏览器的权限限制一直很严格,对于图片上传当然也是如此,肯定不会把文件路径给你。试了几个方案,最终用的是FileReader读取、转base64显示。第一次用前端的FileReader功能,还算好用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var imgB64Arr = [];
//fileInput是input标签
fileInput.change=function(){
  var files = this.files;
  for(var j = 0; j < files.length; j++){
    var reader = new FileReader();
    reader.readAsDataURL(files[j]);
    reader.onload = function(){
      var v64 = this.result;
      imgB64Arr.push(b64);
      //...预览显示逻辑...
    };
  }
};

    这个方案将图片的base64编码放在了imgB64Arr数组里,以方便后续上传处理。不过考虑到现代手机拍照的大小,和多选功能,如果一次选的图很多,会占用大量的内存,所以这里流畅度待验证,可能需要优化
3. 逐个上传
    分开上传的目的有二:
    1). 递归上传,降低有网络问题带来的上传失败的概率
    2). 分开显示上传进度,这样前端体验会更好
    这里我偷了个懒,没用递归,而是定时轮训imgB64Arr数组,有内容就上传,上传完删掉,呵呵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  var uploadingImg = false; //标示当前是否有正在上传的东西
  setInterval(function(){
    if(!uploadingImg && imgB64Arr.length > 0){
      var nowUp = imgB64Arr[0];
      uploadingImg = true;
      var imgName = 'xxx';//文件命名策略
      subimtInageFile(imgName, imgBa64s[nowUp.img], function(retu, err){
        if(err){
          //TODO: 错误处理
          return;
        }
        //上传完成相关数据维护
        imgB64Arr = imgB64Arr.length > 0 ? imgB64Arr.slice(1) : [];
        uploadingImg = false;
        //TODO: 显示更新逻辑
      });
    }
  }, 300);
  function subimtInageFile(imgName, ba64, callback){
    var formData = new FormData();
    formData.append('up_photo', convertBase64UrlToBlon(ba64));
    //这里服务端要留意:没找到怎么把名字写到上传文件里,所以将文件名直接作为单独一个post参数提交
    formData.append('file_name', imgName); 
    $.ajax({
      url: '...',
      type: 'POST',
      data: formData,
      dataType: 'json',
      processData: false, //否则jquery会报错
      contentType: false,
      success: function(sd){
        callback && callback(sd);
      },
      error: function(xhr, err){
        callback && callback(false, err);
      }
    });
  }
  //图片处理,base64欢迎成图片blob
  function convertBase64UrlToBlon(base64Str){
    var bs = window.atob(base64Str.split(',')[1]);
    var ab = new ArrayBuffer(bs.length);
    var ia = new Uint8Array(ab);
    for(var i = 0; i < bs.length; i++) ia[i] = bs.charCodeAt(i);
    var bb = new Blob([ab], {type: 'image/png'});
    return bb;
  }

———-
    写到这里,大概有朋友要说了:这么麻烦干嘛,微信有wx.chooseImage()不香吗?没错,要是能调微信api的需求,当然就不用上面这么麻烦了O(∩_∩)O~
over
转载请注明出处:http://www.jiangkl.com/2022/01/h5_file_multiple

Hello World 他山石

defineProperty

学习VUE“双向绑定”的实现,DOM变化时,同步给js变量,这一层比较容易理解,使用DOM的事件绑定就好了;但是当js变量发送变化时,是如何监控到、然后更新到DOM的,这点就比较有意思的。
看VUE代码,里面使用了ES5新加入的“Object.defineProperty()”,属性拦截器

看名字就比较好理解,使用defineProperty定义的属性,可以针对getter,和setter操作做监控、拦截,比如下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let o = {};
Object.defineProperty(o, 'b', {
  configurable: true,
  enumerable: true,
  get: function(){
    console.log('get run');     //log1
  },
  set: function(v){
    console.log('set run', v);  //log2
  }
});
 
setInterval(function(){
  o.b = new Date().toLocaleString();
  console.log('o.b, ', o.b);     //log3  undefined
}, 3000);

这样,每隔3秒,log2,o.b赋值时,log2,就会被打印出来
但是,下面的反应比较奇怪,getter也被拦截了,log3位置,打印出来的是“undefined”。。。
这是因为getter里没有返回值
但是如果你胆敢在getter“return this.b”,运行时就会死循环,因为如果this.b又会去调getger
(是的,这就是js。。。增加一个看似“美丽强大”的新特性的同时,还给你附加几个坑,等你往里跳。。。。。)
那么,还能不能让人好好使用这个拦截器哪?
当然可以,这里,我们可以加一个“中间变量”过渡,setter时,给中间变量赋值;getter时,return这个中间变量,就不会死循环了

当然,还可以借助经典的“function-Class”定义,对整个过程做封装,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function O1(){
  let v1;     //中间变量
 
  Object.defineProperty(this, 'v1', {
    get: function(){
      console.log('get run', v1);
      return v1;
    },
    set: function(v){
      v1 = v;
      console.log('set run', v);
    }
  });
}
 
var o1 = new O1();
 
setInterval(function(){
  o1.v1 = new Date().toLocaleString();
  console.log('o1.v1, ', o1.v1);
}, 3000);

———
转载请注明出处: http://www.jiangkl.com/2019/06/js_defineproperty

Hello World

js里,image onload事件无效问题的处理

使用image的src,做页面事件上报,上报成功,标记
但是奇怪的是,使用new Image()方式,注册的onload一直无效
使用前端开发工具,看img请求,返回,都正常
查了一下,有些人说,是onload,在src后面的问题,img如果加载很快,以至于在onload就加载完了,所以需要将onload放在src前面
。。。可以肯定的是,保证不是这个原因,因为js是事件驱动的,除非src和onload不在同一个代码段里
后来,突发奇想,测试onerror,居然会执行
于是翻出红宝书查,onload是在图片加载完成,并且图片解析无误时,才会触发。。。图片解析无误
。。。
这个请求,返回的是一段base64,很久以前的一段老代码,是一个1*1的图,但是,不知什么时候,估计是为了返回内容少一点,删了一段。。。出错了,图片解析不成功
于是重新搞了一个图片,echo出去~~ok了!!
。。。
重复一下上面一大段文字的中心意思:
onload是在图片加载完成,并且图片解析无误时,才会触发
———-
转载请注明出处:http://www.jiangkl.com/2018/04/image_onload_无效/

Hello World

来看一下不按常理出牌的js

某公众号上看了一篇“一道面试题引发的对JavaScript类型转换的思考”,里面提到的面试题是这样的:

实现一个函数,满足如下功能:

1
2
3
add(1)(2) // 3  
add(1, 2, 3)(10) // 16  
add(1)(2)(3)(4)(5) // 15

这么一个小功能,实现方法当然见仁见智,这里想说的js的function类型。
我面试前端的时候,经常问这么样一个问题:“jquery里的‘$’是个什么东西?”
对方不明白的话,就直接问“typeof $,会返回什么?”
能回答“function”的,就说明对方对js的数据类型是有了解的
对,$本身是个function,只是上面挂了各种属性和方法
这是第一点

第二点
是js的高阶函数
在学习闭包的时候,会用到高阶函数。所谓高阶函数,说白了就是一个函数,return的还是函数
上面的add(x)(x)(x),所以add肯定是高阶函数的形式

第三点
如果return的是函数,怎么得到相加后的数字
所以函数本身要带上toString(),或者valueOf()

好了,结合上面几点,下面是这个函数的实现,比预想的简单:

1
2
3
4
5
6
7
8
9
10
11
12
function add(){   //第一层add,第一次调用时执行
  var s0 = 0;     //闭包参数,记录累加值
  var f1 = function(){ //内层add,级联调用,实际上是调用的它
    for(var i = 0; i < arguments.length; i++) s0 += arguments[i];
    return f1;
  };
  f1.toString = function(){
    return s0;   //返回累加值
  }
  f1.apply(null, arguments);  //第一次执行时,先add一次
  return f1;
}

————-
转载请注明出处:昆仑的山头

Hello World

判断文本宽度有几种方法

很多时候,我们需要判断一个字符串扔到页面上显示时的宽度,这里,按照我自己的认知顺序说一下都有哪些判断方法

初级:str.length
这个不用说,大家都明白,最简单,也最不靠谱
但是有一个地方要注意:一个中文字符,js里的length是1,而php里,根据编码不同,可以是3或2

中级:正则判断支付类型,按不同的长度处理

1
2
3
4
5
6
7
8
9
10
11
  //判断字符串的长度,全角返回2,半角返回1
  var b2reg = /[\u0000-\u00ff|\uff61-\uff9f|\uffe8-\uffee]+$/;
  function byteLen(s){
    var c = 0;
    if(s && s.length > 0){
      var a = s.split("");
      for(i in a){
        if(!b2reg.test(a[i])) c += 2;
        else c++;
      }
    }

一般要求不严格的情况,我都是这么处理
但是,这个也不靠谱,中文没问题,但是英文字符的显示宽度,会受字体、浏览器的影响,比如“i”和“w”,很难确定这俩显示宽度是否相同

终结:实际显示宽度
有些复杂,不过思路也很简单:在页面的视口之外,做一个同样的显示区域,字体、样式什么的都一样,然后把文本写进去,再判断宽度;如果过宽,再截
这种方法还有一个经典的应用:输入框跟踪提示。
比如,在一个很大的输入框里,输入多个邮箱地址,要随着用户的输入,在输入点下方位置显示不同邮箱地址的提示,这里的一个难点就是:js里无法获得当前输入点的位置。于是就可以用上面的方法,时时获取显示宽度,然后用复杂的计算近似得到当前输入点的位置…

———
(好久不写技术文章了,一方面是懒,另外很关键的一点是…近半年都在做售货机屏幕的东西,缺少普适性)

Hello World

js调试之~打印调用堆栈

(很久没写东西,这里写一个自己熟悉的内容做2016的开篇吧)
相对于其他语言,js似乎很少有需要打印调用堆栈的时候,反正我是做了这么多年了前端,第一次用到,其他情况,基本都可以通过更好的方式来调试
今天面对的问题是这样的,有一个“ad_list”事件,在不停的触发,触发它的地方有N个,触发条件也不一样,靠简单的“console.log”,太多,可读性很差,所以想到了调用堆栈
不过,由于js里大部分是匿名函数,打印出来的堆栈并不像java、php一样简介。。。,打印名字不靠谱,而是,直接打印function内容,然后靠内容反查。。。。这就是很少有人用这一招的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  if(action == 'ad_list'){//打印特定条件的堆栈
    var caller = arguments.callee.caller, j = 0, s;
    while (caller) {
      j++;
      if(typeof caller == 'function' || typeof caller == 'object'){
        s = (caller + '');//function变成字符串
        //排除系统函数、框架函数
        if(s.indexOf('function (event)') < 0 && s.indexOf('function (packet)') < 0 && s.indexOf('fn.apply') < 0){
          console.log('【function】-----stack-----------', j, ': ', (caller + '').substr(0, 200));//打印funciton的前半截
        }
      }
      caller = caller && caller.caller;//取下一级
      if(j > 500){//限定数量  ---  这个一定不要漏了
        return;
      }
    }
  }

————
查了半天(真是半天)的问题,结果是自己给自己挖的坑:默认的图片/活动名称是按规约命名的,但是测试图省事没照这个规则做,才造成上面说的不停的刷新同一个东西。。。。。。

Hello World

搞定异步编程(一)——使用高阶函数管理并行程序

nodejs对外宣称的最大优势就是所谓的异步io,利用异步io,可以并行执行互相没有依赖的网络、数据库等操作,在某些场景下,这种方式可以极大的提高站点的响应速度
但是异步io也带来了代码本身逻辑的复杂性,比如一个应用,需要访问三个http数据源,然后综合比较三个数据源的结果,来得出最终的返回结果
三个数据源的回调执行顺序是不定的,这就需要一个计数器,在回调次数等于3时,再执行最终的处理程序
这里,借助js的高阶函数,来实现这个逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var _httpLib = require('./lib/http.js');  //简单的http辅助类,如:get(url, callback)
//高阶函数
var pending = (function(callback){
  var count = 0, //计数
    returns = {};
  return function(key){
    count++;     //注册一个回调
    return function(data){
      count--;   //执行一个回调
      //按照并行逻辑的key保存返回结果
      returns[key] = data;
   //   returns[key] = data.substr(0, 100);
      if(count === 0){   //所有回调都执行以后,运行最终的逻辑
        callback(returns);
      }
    };
  };
});
//。。。怎么来准确的称呼done哪?
var done = pending(function(returns){
  //最终的处理逻辑
  console.log('all-------over');
  console.log(returns);
});
//三个并行数据源
_httpLib.get('http://www.baidu.com', done('baidu'));
_httpLib.get('http://www.qq.com', done('qq'));
_httpLib.get('http://www.jiangkl.com', done('jkl'));

~~~呵呵,看到一坨return、function,如果你还没晕,那么,恭喜你。反正我盯着这段代码看了不下10分钟,然后又执行了几次才算大概弄明白的
整个的逻辑,有点类似aop的思想,即对多个并行逻辑的回调做一个拦截、记录,这样可以更好的让程序员将注意力集中到业务逻辑上
pending是一个公用的并行处理函数
done是通过pending得到的具体的业务执行function,其回调参数就是前面提到的并行逻辑“最终的处理程序”
done()执行时,会将pending闭包内的计数器加1,同时返回一个function,作为httpget的回调
done()()执行时,计时器减1,同时将httpget的返回值保存
计数器减到0,“最终的处理程序”开始执行

———–
参考:http://www.infoq.com/cn/articles/generator-and-asynchronous-programming

Hello World 点点滴滴

setTimeout的精度

前几天刚发了一篇利用setTimeout提高ui响应速度的文章,本来感觉自己对setTimeout的理解已经够深入了,但是昨天偶然听说setTimeout其实是有“bug”的:在setTimeout 0时,并不能精确定时,还说IE会有16ms的延时
虽然早就想到js这种东西定时肯定不会很精确的,而且这个时延对“提高ui响应速度”影响也不大,但是如果是做动画的话,影响可能就比较大了,还说抽空写了段小程序试了试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//定时长度
var OUT_TIME = 0;
//循环次数
var LOOP_COUNT = 100;
$(function(){
  $('a#start').click(function(){
    var t1 = mst();
    loopOut(LOOP_COUNT, t1);
  });
});
//递归循环
function loopOut(i, t1){
  if(--i > 0){
	setTimeout(function(){
	  loopOut(i, t1);
	}, OUT_TIME);
  }else{
    var t2 = mst();
    alert(t2 - t1);
  }
}
//返回当前时间点的毫秒数
function mst(){
  return new Date().getTime();
}

上面的程序,当点击“start”按钮后,首先会取当前时间t1,然后调用loopOut,当循环参数i大于0时,会通过setTimeout定时0ms递归调用loopOut,同时i会减1,形成循环终止条件,放循环满100次时,再次取当前时间,与t1相减,也就是100此setTimeout循环的用时
测试结果发现,“IE会有16ms的延时”,这话并不算错,IE7、8,最后的返回值大概是1530~1550,算起来,一次setTimeout大概要延迟DT=15.4ms
而且chrome/safari/firefox,返回值大概是420~430,即DT=4.2ms

当增大定时长度OUT_TIME时,只有增大到各浏览器的DT时,实际的定时长度才会有效,也就是DT可以看成“最小定时长度”
但是即便定时长度大于DT定时也是不精确的,非IE大概是0.2ms,IE就有点离谱了,比如OUT_TIME=50时,平均每次的定时长度大概是60ms,差了10ms(鄙视)

综上所述,虽然setTimeout传入的时间值是毫秒,但是是在并不精确,如果对精度要求较高,最好好好计算各浏览器的误差

下面讨论另外两种情况

1. 既然setTimeout有这种问题,那么与它类似的setInterval有问题吗?
将上面的“loopOut”方法稍加改进,即可setInterval:

1
2
3
4
5
6
7
8
9
function loopOut(i, t1){
  var si = setInterval(function(){
    if(--i == OUT_TIME){
      var t2 = mst();
      alert(t2 - t1);
      clearInterval(si);
    }
  }, OUT_TIME);
}

这是,IE再次表型了它的卓尔不群:当OUT_TIME=0的时候,setInterval的回调方法只会执行一次,只有OUT_TIME>0,这段脚本在IE里才能正常执行
除此之外,setInterval和前面setTimeout的表现基本相同

2. node.js的setTimeout精确吗?
将上面click部分的代码去掉,稍加修改,既可以让代码再node里执行,但是为了去除代码初始化的影响,这里用了另外一个setTimeout,让其在程序初始化100ms以后在执行

1
2
3
4
setTimeout(function(){
  var t1 = mst();
  loopOut(LOOP_COUNT, t1);
}, 100);

测试结果发现,服务器端脚本相对浏览器端的效率果然高出不少,在循环次数为100的时候,node.js的setTimeout 0ms的延迟大约在0.1ms左右,当把循环次数提高的100000后,平均的延迟降到了0.01ms

Hello World 点点滴滴

如何调试压缩后的js—— JavaScript Source Map的使用

随着webApp的流行,前端应用越来越复杂,js脚本越来越多,为了提高页面页面响应速度,我们常常需要对js进行压缩、整合
但是经过压缩后的js,调试起来会特别麻烦,基本是对于chrome/ff这些“现代”浏览器,如果压缩过的js出了错误,报的错误也是让你无所适从
之前的做法是,在开发环境不回js做压缩、整合,而且在上线前最后确认的verify以及线上环境对js做压缩,这么做虽然可以避免上面的问题,但又是js的加载顺序对功能会有影响,所以还是不能完全模拟线上的情况
更何况,线上可能还会出问题的
这里介绍一种可以调试压缩脚本的方法,就是所谓“Source Map”,大概的原理是这样的:
在使用compiler.jar压缩js的时候,同时生成一个“.map”文件,对压缩前的js诸如变量之类的内容做索引,页面加载min版的js,可以通过对应的.map文件找到压缩前的js文件,如果这是js再出错,可以直接将错误定位到压缩前的js文件内
怎么样,听起来是不是很棒
好,下面就详细讲一下用法
比如页面上有一个js文件:t.js,下面对t.js做压缩

1
2
3
4
5
java -jar /usr/local/compiler.jar \
	--js t.js \
	--create_source_map t.js.map \
	--source_map_format=V3 \
	--js_output_file t.min.js

其中,“create_source_map”就是生成.map文件的名字,“source_map_format”是source map的版本
完了页面,在页面调用t.min.js,故意制造个错误,这是你会发现,页面上的错误指示~~~还TM和以前一样

呵呵,因为还有两步我们还没做:
1. 手动往t.min.js结尾处加上下面这行,告诉浏览器.map文件所在的位置:
//@ sourceMappingURL=t.js.map

2. 开启浏览器的source map支持,悲剧的是,目前只有chrome支持source map,具体未知是在:
“开发者工具” -> “设置”(右下角齿轮) -> 勾选“Enable source maps”

ok了,这是再试试,一旦js报错,点击错误,就能定位到压缩前的js文件上
并且,在开发者工具里的“Sources”标签里,还能找到压缩前的js源码

————-
参考:http://www.csdn.net/article/2013-01-25/2813953-JavaScript-Source-Map
ps:关于.map文件内部各项内容的含义,我并没有仔细研究~~~反正只要设置正确,chrome就可以把错误定位到源js文件