分类 JS 下的文章

阿里云地图流量动画效果的实现分析

预览

阿里云的:
preview1.gif

实验品:
preview2.gif

原理

阿里云的流量动画是先画出一条线,然后改变每个关键颜色的offset,实现类似抛投的效果;渐变的结构如下:

7451DFEF-A99C-4777-B035-FB1F501F5093.png

实现

这里只画出一条简单的直线作为例子,如果需要画出曲线,d3的geo相关的函数可以很方便的作出。

C1B9DB0B-3364-404F-BCAE-682BB2FD942D.png

代码如下:

(function () {

    var svg = d3.select("#container")
                .append("svg")
                .attr("width", 500)
                .attr("height", 200);
    var gradient = svg.append("defs")
                      .append("linearGradient")
                        .attr("id", "gradient")
                        .attr("x1", "0%")
                        .attr("x2", "100%");
    var stops = [];
    var stop;
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "0%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    // start stop
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "0%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    // start color
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "0%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    // end color
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "100%")
        .attr("stop-color", "#009a61");
    stops.push(stop);

    // end stop
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "100%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "100%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);


    var route = svg.append("line")
                    .attr("x1", 0)
                    .attr("y1", 100)
                    .attr("x2", 500)
                    .attr("y2", 200)
                    .style("stroke", "url(#gradient)")
                    .style("stroke-width", 4)
                    .style("stroke-linecap", "round")
                    .style("fill", "none");

    var myoffset = 0;
    setInterval(function () {
        myoffset += 1;
        stops.slice(1, 3).forEach(function (stop, i) {
            stop.attr("offset", (myoffset - 20) + "%");
        });
        stops.slice(3, 5).forEach(function (stop, i) {
            stop.attr("offset", myoffset + "%");
        });
        if (myoffset >= 100) {
            myoffset = 0;
        }
    }, 100);
})();

Vuejs 和 CodeMirror 的绑定

实现 vuejs 和 codemirror 的相互绑定

Vue.directive("codemirror", {
    twoWay: true,
    bind: function () {
        // 请自行初始化CodeMirror, 可使用 $(this.el).data('codemirror')
        this.editor = editor;
        // model主动赋值标记
        this.silent = false;
        this.handler = function () {
            if (!this.silent) {
                this.set(this.editor.getValue(), true); // 加锁,防止相互赋值
            }
        }.bind(this);
        this.editor.on("change", this.handler);
    },
    update: function (value, oldValue) {
        this.silent = true;
        this.editor.setValue(this.vm.$data[this.raw]);
        this.silent = false;
    }
});

使用:

<!-- code 为 Vue data 中使用的变量名 -->
<textarea id="codepad" v-codemirror="code"></textarea>

访问可视化(续)

之前写过一片文章:访问可视化,但是局限性很大,于是利用周末的时间,改造了一番。

演示地址:http://tool.lu/visitor

打点的改造

老版本:在php里面进行打点

新版本:使用js加载空伪1px gif图片

数据分析和存储的改造

老版本:请求过来的时候,php分析,存进mysql

新版本:请求的时候,js分析,加载1px gif,nginx记录日志,python分析,存进数据库

数据的访问实时dump

老版本:监听mysql的binlog,publish到redis

新版本:分析nginx,存进数据库的同时,publish到redis

IP归属地 和 经纬度的查询

老版本:纯真数据库 + 百度地图地址反解

新版本:ip17mon + 腾讯地图行政区划latlag

预览

QQ20150209-1.png

1. js的打点日志

具体可以参考:网站统计中的数据收集原理及实现

不过我使用的方法由文章简化而来。

2. nginx的日志记录

server {
    listen 80;
    listen 443 ssl;
    server_name analytics.tool.lu;

    ssl_certificate vhosts/tool.lu.chained.crt;
    ssl_certificate_key vhosts/tool.lu.key;

    root /data/html/analytics.tool.lu/public;

    access_log off;
    error_log off;

    location / {
        index index.html;
        try_files $uri $uri/ =404;
    }

    location /__utm.gif {
        expires -1;
        if_modified_since off;
        # add_header Last-Modified "";
        empty_gif;
        access_log /data/log/nginx/analytics.tool.lu.access.log;
    }
}

3. nginx日志的分析

这里有几点要注意的:

  1. nginx日志我是使用logrotate切分的,要达到实时读取nginx日志的目的,需要保证读取的文件是最新切分出来的文件,而且在切分文件的时候要保证之前的文件已经处理完毕。

  2. 记录日志文件处理的offset,又是由于logrotate切分nginx日志的原因,如何保证记录的offset是唯一的,且能对应上日志文件,这里我使用了log文件创建的时间作为标识(在linux下是不储存文件的创建时间的,需要自己记录实现)。

  3. nginx log中时间格式的解析,基本上就是抓瞎了(不是所有版本的python strptime都支持timezone的)

  4. 如何通过腾讯地图 api 返回的数据建立索引,方便快速查找到 地址对应的经纬度

为了安全性考虑,这边就不贴 nginx log 解析的完整代码了。

ua-parser返回version的拼接

    def version(self, vs):
        v = ''
        if not vs['major']:
            return v
        v += vs['major']
        if not vs['minor']:
            return v
        v += '.' + vs['minor']
        if not vs['patch']:
            return v
        v += '.' + vs['patch']
        if not 'patch_minor' in vs or not vs['patch_minor']:
            return v
        v += '.' + vs['patch_minor']
        return v

腾讯地图行政区划数据索引的建立

        # IP17MON返回的数据带有,省市,可以优先市级别的索引,再省级别的索引
    def index(self):
        for province in self.data['result'][0]:
            self.indexed_provinces[province['name']] = province['location']
        for city in self.data['result'][1]:
            self.indexed_cities[city['name'] if 'name' in city else city['fullname']] = city['location']

python

list 的 shift

l = [1, 2]
first = l.pop(0)

parse nginx log的正则

self.pattern = re.compile(r'(?P<ipaddress>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<dateandtime>\d{2}\/[a-z]{3}\/\d{4}:\d{2}:\d{2}:\d{2}) [+-]\d{4}\] ((\"(GET|POST) )(?P<url>.+)(http\/1\.1")) (?P<statuscode>\d{3}) (?P<bytessent>\d+) (["](?P<referrer>(\-)|(.+))["]) (["](?P<useragent>.+)["])', re.I)

nginx log时间的解析

datetime.datetime.strptime(fields['dateandtime'], '%d/%b/%Y:%X')

PS: 好久不写python已有些生疏

数字点阵的动画实现

预览

未标题-1.gif

原理

数字0的js数组点阵,7x5个 span 元素拼出一个数字出来,同样的方法实现 1-9 的数字

动画效果使用 css3 的 keyframes 实现

[
    [0, 1, 1, 1, 0],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [1, 1, 0, 1, 1],
    [0, 1, 1, 1, 0]
]

QQ20150119-1.png

源码

numb.zip

chrome插件开发杂记

刷新当前 tab 页的3中方式

// 1. 通过在页面中执行js代码
chrome.tabs.getSelected(null, function(tab) {
    var code = 'window.location.reload();';
    chrome.tabs.executeScript(tab.id, {code: code});
});
// 2. 通过chrome的api更新tab的url
chrome.tabs.update(null, {url: url});
// 3. 通过chrome的api刷新
chrome.tabs.reload();

popup 和 background 之间的通讯

以修改 cookie 为例子:

popup:                            -> 修改cookie                          -> 更新UI
background: -> 监听cookie的修改事件               -> 触发监听,与 popup 通讯
// 在background中
chrome.cookies.onChanged.addListener(function (changeInfo) {
    var cookie = changeInfo.cookie;
    if (cookie.domain == 'tool.lu' && cookie.name == 'X-Backend-Server') {
        chrome.runtime.sendMessage({event: "cookie", data: cookie.value}, function (response) {
            // ...
        });
    }
});
// 在popup中
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    // do things
});

popup弹出时候,更新popup的UI

// 在popup中绑定 window.onload 事件
window.onload = init;

background中获取当前加载页面的网页头信息

chrome.webRequest.onHeadersReceived.addListener(function (o) {
    // 判断是否是主页面, 因为页面中可能包含 iframe, image, css, js 等其他资源的加载
    if (o.frameId != 0 || o.type != "main_frame") {
        return;
    }
    for (var i in o.responseHeaders) {
        var header = o.responseHeaders[i];
        console.log(header.name, header.value);
    }
}, {
    "urls":["http://tool.lu/*"]
}, ["responseHeaders"]);

background在页面加载完成之后触发事件

chrome.webNavigation.onCompleted.addListener(function(o) {
    if (o.frameId == 0) {
        // do something...
    }
}, {
    url: [
        {hostContains: 'tool.lu'}
    ]
});

访问可视化

网站的统计

由于 tool.lu 的流量还不是很大,所以我把每次的访问记录都存到了MySQL(如果流量大,这么做是作死的节奏)

主要流程图

design1.jpg

design2.jpg

使用canal做异步处理,主要是因为

  1. ip => city的映射,可能要调用第三方接口比较耗时

  2. 网站代码处不需要写2份数据

  3. 装x

其中cannal分发的数据处理是用的java,本打算sse也用java的netty来实现了,惭愧,尝试未果后就放弃了,最后用golang实现的。

这样做不会太耗性能,而且每秒钟往客户端传输一次数据,但是由于 vps 的内存有限,java 又比较吃内存,所以上线之后就直接下线了。

按位操作应用实例

需求

用户登陆后有多个列表页面中都有搜索功能,由于搜索的条件比较多,所以加了个“更多”按钮隐藏了一部分条件,只把常用的条件直接显示出来。但有些用户会经常用到更多中的条件来搜索,所以希望当点击了“更多”按钮后,我刷新页面或者下次再登陆进来后那个更多中的条件默认就是显示的。

分析

一下子就想到用cookie来做,每个用户每个页面设置个不同的cookie。
但这样会有个问题就是cookie个数太多了。。,去搜索引擎搜索了下,每个站点的cookie是有数量限制的。
不过,即便没有限制,想想那么多个cookie也是件很恐怖的事情。

解决

以前学习mysql的时候,知道可以用无符号的tinyint来存储最多8个数据的0和1的状态,就是将255转换为二进制然后约定第几位代表固定的某个数据或某个物品,1和0来标识其状态。
那这里我想也可以用类似的方法。这样的话,每个用户只用一个相应的cookie就可以搞定了。

实现

下面这个是每个页面不同的部分:

<script type="text/javascript">
    var searchMoreMark = 1;//标识:页面一为1、页面二为2、页面三为4、页面四为8、页面五为16、页面六为32、页面七为64。。依次类推。
</script>

这里是公用的部分,需要先引入jQuery及jQuery.cookie

<script type="text/javascript">
    var searchMoreCookieExpire = 36500;//cookie有效时间
    var searchMoreCookieName = "searchmore_<?=$_SESSION['LOGIN_USER_ID']?>";//根据用户来区分
    jQuery(document).ready(function() {
        //点击展开操作
        jQuery('#do_search_more').click(function(){
            jQuery('#more_condition').css('display','block');
            jQuery('#do_search_more').css('display','none');
            jQuery('#do_search_less').css('display','block');
            jQuery(window).resize();
            //设置“更多”状态
            if(jQuery.cookie(searchMoreCookieName)==null || jQuery.cookie(searchMoreCookieName)==0){
                //如果没有设置过cookie,就设置cookie为当前页面的标识
                jQuery.cookie(searchMoreCookieName, searchMoreMark, {expires:searchMoreCookieExpire, path:'/'});
            }else if((jQuery.cookie(searchMoreCookieName) & searchMoreMark) == 0){
                //如果设置过cookie且不存在当前页面的标识的话就把当前页面标识加进过
                jQuery.cookie(searchMoreCookieName, parseInt(jQuery.cookie(searchMoreCookieName)) | searchMoreMark, {expires:searchMoreCookieExpire, path:'/'});
            }
        });

        //点击收缩操作
        jQuery('#do_search_less').click(function(){
            jQuery('#more_condition').css('display','none');
            jQuery('#do_search_more').css('display','block');
            jQuery('#do_search_less').css('display','none');
            jQuery(window).resize();
            if(jQuery.cookie(searchMoreCookieName)!=null){
                //去除本页面的标识
                searchMoreMark = jQuery.cookie(searchMoreCookieName) & (~searchMoreMark);
                //如果标识为0,那么就删除掉这个cookie
                if(!searchMoreMark){
                    searchMoreCookieExpire = -1;
                }
                jQuery.cookie(searchMoreCookieName, searchMoreMark, {expires:searchMoreCookieExpire, path:'/'});
            }
        });
        //默认为上一次的状态(收缩或展开)
        if(jQuery.cookie(searchMoreCookieName)!=null && (jQuery.cookie(searchMoreCookieName) & searchMoreMark) != 0){
            jQuery('#do_search_more').click();
        }
    }
</script>

jQuery data函数的坑

这个函数

<div id="test" data-id="1e3"></div>
<script>
console.log($('#test').data('id'));
// 1e3  jQuery 1.9.1
// 1000  jQuery 1.7.2
</script>

Every attempt is made to convert the string to a JavaScript value (this includes booleans, numbers, objects, arrays, and null). A value is only converted to a number if doing so doesn't change the value's representation. For example, "1E02" and "100.000" are equivalent as numbers (numeric value 100) but converting them would alter their representation so they are left as strings. The string value "100" is converted to the number 100.

When the data attribute is an object (starts with '{') or array (starts with '[') then jQuery.parseJSON is used to parse the string; it must follow valid JSON syntax including quoted property names. If the value isn't parseable as a JavaScript value, it is left as a string.

To retrieve the value's attribute as a string without any attempt to convert it, use the attr() method.

2014-07-16 23:02 更新

这个还是要看具体的jQuery版本而定的

jQuery.fn.jquery

我这测试,1.7.2 的是会自动转换的,1.9.1 是不会自动转换

而官方的文档的更新函数中,貌似并没有提及这一点

CodeMirror绑定快捷键

自定一个命令

CodeMirror.commands.runcode = function (cm) {
    // you can do something here.
};

绑定快捷键

// 判断是否为Mac
var mac = CodeMirror.keyMap.default == CodeMirror.keyMap.macDefault;
var runKey = (mac ? "Cmd" : "Ctrl") + "-Enter";
var extraKeys = {};
extraKeys[runKey] = "runcode";

初始化CodeMirror

CodeMirror.fromTextArea(code, {
    extraKeys: extraKeys
});

svg图片蒙板

预览

2014-04-13 11\_59\_17.gif

代码

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <filter id="maskfilter">
    <feColorMatrix in="SourceAlpha"
                   type="matrix"
                   values="0 0 0 1 0
                           0 0 0 1 0
                           0 0 0 1 0
                           0 0 0 1 0" />
  </filter>
  <mask id="svgmask">
    <image xlink:href="mask.png" x="0" y="0" width="130" height="130" filter="url(#maskfilter)"></image>
  </mask>
  <g transform="translate(100, 100)">
    <image id="handler" xlink:href="default.jpg" x="0" y="0" width="260" height="130" mask="url(#svgmask)"></image>
  </g>
</svg>
// https://code.jquery.com/jquery-2.1.0.min.js
(function ($) {
    var x, y, startX, startY, isDragging;
    x = y = startX = startY = 0;
    isDragging = false;
    var handler = $('#handler');
    handler.on('mousedown', function (e) {
      x = parseInt(handler.attr('x'));
      y = parseInt(handler.attr('y'));
      startX = e.pageX;
      startY = e.pageY;
      isDragging = true;
    });
    $(document).on('mousemove', function (e) {
      if (!isDragging) return;
      handler.attr('x', x + e.pageX - startX);
      handler.attr('y', y + e.pageY - startY);
    }).on('mouseup', function (e) {
      isDragging = false;
    });
  })(jQuery);

jQuery-pjax的坑

白话解释:利用ajax进行页面的局部刷新,并可以实现浏览器前进后退 的一个库

bower install jquery-pjax -S --allow-root

使用

官方文档上是这样写的:

$(document.body).pjax("a.btn", ".container");

神马?没搞定?

QQ20140119-1.png

额!确实没搞定,于是第一个想到的就是google。无奈转载太多,而且提供的demo没有一个正常工作的(PS:鄙视转载无验证)。

根据上图中提示的代码位置,找到了locationReplace,在根据xhr请求被cancel,定位到代码223行,输出错误为timeout,找至196行,发现插件的默认值中根本就没有timeout选项,于是设置超时10s就解决这个问题了:

$(document.body).pjax("a.btn", ".container", {timeout: 10000});

PS:不知官方文档使用方法为何没有提及,不知我国人写出demo之后有无测试过

在项目中使用gulp

安装gulp

npm install gulp -g
npm install gulp gulp-coffee gulp-concat --save-dev

配置

我的js第三方库是使用bower来管理的,.bowerrc

{
    "directory": "public/js/vendor"
}

gulp这样配置,暂时只用了coffee的编译,没有使用js的合并和混淆

var gulp = require('gulp');
var coffee = require('gulp-coffee');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

gulp.task('coffee', function () {
    gulp.src(['./public/js/**/*.coffee', '!./public/js/vendor/**'])
        .pipe(coffee())
        .pipe(gulp.dest('./public/js/'));
});

gulp.task('scripts', function () {
    gulp.src(['./public/js/**/*.js', '!./public/js/vendor/**'])
        .pipe(concat('all.js'))
        .pipe(gulp.dest('./public/dist'))
        .pipe(rename('all.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest('./public/dist'));
});

gulp.task('default', function() {
    gulp.run('coffee');
    // gulp.run('scripts');
    gulp.watch(['./public/js/**/*.coffee', '!./public/js/vendor/**'], function () {
        gulp.run('coffee');
    });
    if (0) {
        gulp.watch(['./public/js/**/*.js', '!./public/js/vendor/**'], function () {
            gulp.run('scripts');
        });
    }
});

html5 drag事件

html5的drag drop相关事件

drag
dragstart
dragend
dragover
dragenter
dragleave
drop

dragend的坑!

The dragstart event is fired when the user starts dragging an element or text selection.

MDN的文档说的不错,dragstart只有在element或者选中文字拖放开始的时候触发,也就是拖放文件的时候不会触发。

The dragend event is fired when a drag operation is being ended (by releasing a mouse button or hitting the escape key).

尼玛,这没说啊,试过才知道。dragstartdragend文件拖放是不会触发的。