Gravatar头像玩独立博客的人都非常熟悉。如果在Gravatar的服务器上放置了你自己的头像,那么在任何支持Gravatar的网站留言时,只要提供你与这个头像关联的email地址,就能够显示出你的Gravatar头像来。

但是,其访问速度却非常鸡肋,即便用上cdn方案,也差强人意,很久以前有人提供过本地缓存的方案,但是实用性并不好,无法解决留言时加载慢的问题。于是就有了下面的方案。

特点:

1,甄别QQ邮箱和其他邮箱,优先显示QQ头像,毕竟QQ头像家在还是非常快的。
2,利用比较快的cdn及时显示当前评论者的头像。
3,利用popen异步实现缓存头像到本地。
4,为什么要异步?因为:如果不异步那么访客首次评论是仍要忍受龟速。

代码:

/*
 * [函数11] local_avatar 
 * 
 * 功能:实现gravatar头像本地缓存
 * 逻辑:本地 > QQ > Gravatar && 默认头像用本地随机(并缓存)
 */
function local_avatar($mail){
    $from_avatar = '';
    // default_avatar 默认头像为本地的一些预置的头像图片,随机输出一张(并且缓存后不会改变)
    $default_avatar = Typecho_Widget::widget('Widget_Options')->themeUrl. '/assets/img/avatar/'.rand(1,6).'.png';
    // 邮箱地址转hash
    $mail_hash = md5(strtolower($mail));
    // avatar_time 设置本地缓存过期时间*天数
    $avatar_time = 1209600*14;
    // avatar_doc 本地头像的绝对路径
    $avatar_doc = __TYPECHO_ROOT_DIR__ . '/usr/uploads/avatar/'.$mail_hash.'.jpg';
    // local_url 本地头像的网络地址
    $local_url = Typecho_Widget::widget('Widget_Options')->siteUrl.'usr/uploads/avatar/'.$mail_hash.'.jpg';

    //匹配邮箱是否为QQ邮箱
    preg_match_all('/((\d)*)@qq.com/', $mail, $is_qq_mail);//正则匹配QQ邮箱

    //判断本地是否有该头像可用(存在 且 未过期 且 大小正常)的缓存,有就直接调用
    if (is_file($avatar_doc) && (time() - filemtime($avatar_doc)) < $avatar_time && filesize($avatar_doc)>900){
        $url = $local_url;
    //如果没有可用的本地缓存
    }else{
        // 如果评论者用的不是QQ邮箱
        if (empty($is_qq_mail['1']['0'])){
            // url 有两个用处,1,评论是输出 2,不知执行时传参(作为本地缓存的来源)3,loli源算是比较快的~
            $url = 'https://gravatar.loli.net/avatar/' . $mail_hash . 's=80&r=X&d=';
            // from_avatar 时用来传给异步执行文件的一个参数,判断头像来源
            $from_avatar = 'gravatar';
        // 如果用的是QQ邮箱
        }else{
            // 评论时直接输出QQ头像
            $url = 'https://q2.qlogo.cn/headimg_dl?dst_uin='.$is_qq_mail['1']['0'].'&spec=100';
            // 头像来源为 'qq'
            $from_avatar = 'qq';
        }
    //执行异步缓存
    pclose(popen("php usr/themes/QuarkGarden/ic/asyn_avatar.php '$url' '$avatar_doc' '$mail_hash' 'from_avatar' '$default_avatar'>/dev/null 2>&1 &", 'r')); 
    }
    // 返回头像
    return  $url;
} 

异步文件asyn_avatar内容:

 <?php
// 一共传递过来5个参数,重新赋值方便调用
$url           = $argv[1];
$url_to         = $argv[2];
$mail_hash      = $argv[3];
$from_avatar    = $argv[4];
$default_avatar = $argv[5];
// 判断头像来源是否gravatar
if($from_avatar != 'gravatar'){
    // 如果不是
    $url = $url;
// 如果是gravatar
}else{
    // 判断是否有gravatar
    $avatar_gravatar = 'http://gravatar.loli.net/avatar/'.$mail_hash.'?d=404';
    $avatar_headers = @get_headers($avatar_gravatar);
    if (!preg_match("|200|", $avatar_headers)) {
        // 如果没有gravatar那就用本地的头像(毕竟官方默认是在不怎么好看)
        $url = $default_avatar;
    }
}
// 用copy 完成本地缓存
copy($url, $url_to);

以上就是全部实现代码了。

调用:

<img class="avatar" src="<?php echo local_avatar($comments->mail);?>" />

拓展:

如果想要再智能一些,可以吧$default_avatar换成动态生成的和评论者名字相关的图片,可以利用网上的占位图API来轻松实现。
只需要在函数里再添加一个参数来传递评论者名称即可。

思考更新

上面条理已经比较清晰了,但是还有不足:
1,对于“胡编乱造的”qq邮箱
2,代码本身有些复杂,异步文件里参数太多了

另一个思考

如果把get_headers后的判断(图片是否可用)放在主函数里面执行,可能会导致页面卡顿(当然对于整体缓存方案来说自由“当时”一条评论的头像需要判断);如果放到异步进程中去,有没有办法解决“默认”头像和“假”QQ头像这两种情况的”首次替换“(即评论者当时就能考到随机头像)能力。
因此这是一个固有矛盾,不可解决。

这里以”有效显示为先“重新整理代码如下:

/*
 * [函数11-0] is_val_img
 * 判断一个链接是否为可用图片链接
 */

function is_val_img_easy($is_url_val,$url){
    $default_avatar = 'http://link.zizdog.com/avatar/'.rand(1,13).'.jpg';
    $url_headers = implode(get_headers($is_url_val));
    $is_200 = preg_match("|200|", $url_headers);
    $is_img = preg_match("|image|", $url_headers);
    if ($is_200 && $is_img) {
        $url = $url;
    } else {
        $url = $default_avatar;
    }
    return $url;
}

local_avatar主体函数

/*
 * [函数11] local_avatar 
 * 
 * 功能:实现gravatar头像本地缓存
 * 逻辑:本地 > QQ > Gravatar && 默认头像用本地随机(并缓存)
 */
function local_avatar($mail){
    $from_avatar = '';

    // 邮箱地址转hash
    $mail_hash = md5(strtolower(trim($mail)));
    // avatar_time 设置本地缓存过期时间*天数
    $avatar_time = 1209600*14;
    // avatar_doc 本地头像的绝对路径
    $avatar_doc = __TYPECHO_ROOT_DIR__ . '/usr/uploads/avatar/'.$mail_hash.'.jpg';
    // local_url 本地头像的网络地址
    $local_url = Typecho_Widget::widget('Widget_Options')->siteUrl.'usr/uploads/avatar/'.$mail_hash.'.jpg';

    //匹配邮箱是否为QQ邮箱
    preg_match_all('/((\d)*)@qq.com/', $mail, $is_qq_mail);//正则匹配QQ邮箱,如果是,则配位结果为,$is_qq_mail[0]='xx@qq.com';$is_qq_mail[1]='xx';$is_qq_mail[0]='2'(is_qq_mail的长度);$is_qq_mail[1][0]为qq好

    //判断本地是否有该头像可用(存在 且 未过期 且 大小正常)的缓存,有就直接调用
    if (is_file($avatar_doc) && (time() - filemtime($avatar_doc)) < $avatar_time && filesize($avatar_doc)>900){
        $url = $local_url;
    //如果没有可用的本地缓存
    }else{
        // 如果评论者用的不是QQ邮箱
        if ($is_qq_mail[1][0]){
            // 评论时直接输出QQ头像
            $url = 'https://q2.qlogo.cn/headimg_dl?dst_uin='.$is_qq_mail[1][0].'&spec=100';//必须加&spec=100且不可更改
            $is_url_val_easy = $url;
        // 如果用的是QQ邮箱
        }else{
            $url = Helper::options()->gravatar . $mail_hash . 's=80&r=G&d=identicon';
            $is_url_val = 'http://cn.gravatar.com/avatar/'.$mail_hash.'?d=404';
        }
        //include 'ic/is_img.php';
        $url = is_val_img_easy($is_url_val,$url);
    //执行异步缓存
    pclose(popen("php usr/themes/QuarkGarden/ic/asyn_avatar.php '$url' '$avatar_doc'>/dev/null 2>&1 &", 'r')); 
    }

    // 返回头像
    return  $url;
} 

异步文件asyn_avatar内容:

<?php
$url = $argv[1];
$avatar_doc = $argv[2];
copy($url, $avatar_doc);

最后的思考

其实比较权衡的方案应该是吧is_val_img这部分放到异步文件里面去,这样评论者的速度体验上是有优势的,毕竟,对于故意填写一个错误QQ好的评论者,也没多大必要及时显示一个有效头像。只要后续缓存成功,对于其他访问者来说体验是很完美的。

值得注意的是:
如果只判断一个链接是否200的时候curl 是要由于 get_headers的,因此可以用下面的方案替代[函数11-0]

/*
 * [函数11-0] is_val_img
 * 判断一个链接是否为可用图片链接
 * 优点:比get_headers快(理论上是的),缺点:代码增多
 */

function is_val_img($is_url_val,$url){
    // default_avatar 默认头像为本地的一些预置的头像图片,随机输出一张(并且缓存后不会改变)
    //$default_avatar = Typecho_Widget::widget('Widget_Options')->themeUrl. '/assets/img/avatar/'.rand(1,13).'.jpg';
    $default_avatar = 'http://link.zizdog.com/avatar/'.rand(1,13).'.jpg';
    // 创建一个cURL资源
    $ch = curl_init();

    // 设置URL和相应的选项
    curl_setopt($ch, CURLOPT_URL, $is_url_val); //设置URL
    curl_setopt($ch, CURLOPT_HEADER,  1); // 将头文件的信息作为数据流输出
    //curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); //是否跟着爬取重定向的页面,这一项不需要开启,因为我们判断的是链接本身,不是跳转后内容
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); //将curl_exec()获取的值以文本流(字符串)的形式返回,而不是直接输出。
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); //设置超时时间
    curl_setopt($ch, CURLOPT_NOBODY,true); //设置只获取header不获取body

    // curl_exec($ch);抓取URL并把它传递给浏览器
    $content = curl_exec($ch);
    
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);  //curl的httpcode
    $http_type = curl_getinfo($ch,CURLINFO_CONTENT_TYPE); //看看能不能直接或获取type

    // 关闭cURL资源,并且释放系统资源
    curl_close($ch);

    $img_type = explode("/",$http_type); //将'Content-Type: image/jpeg'中的值用/分隔开
    if ($http_code == 200 && strtolower($img_type[0]) == 'image') {
        $url = $url;
    } else {
        $url = $default_avatar;
    }
    return $url;
}

又一次更新

上面有一处问题,异步文件路径。

$cmd = "php ".__THEME_DIR__."ic/asyn_avatar.php  '$url' '$avatar_doc'>/dev/null 2>&1 ";
pclose(popen($cmd.'&', 'r'));

完结!