Monthly Archives: 十月 2012

Hello World 业界杂谈

微信二维码登录功能的实现

微信的二维码登录是个很酷的功能:客户端登录后,通过扫描web端的二维码,实现统一用在web端的登录

下面要讲的就是如何这一个功能:

实现思路:
web:访问web页面时,如果未登陆,会根据随机字符串生成二维码,显示到页面上,同时将该字符串保存起来,然后页面会通过ajax轮询方式访问web端,查看这个二维码是否可以登录,如果可以登录,则将用户名/id写入session,然后将页面跳转到登录状态
客户端:扫描页面的二维码后,通过http方式访问web端,将二维码源字符串,以及客户端登录用户的用户名/id发送过去,告诉web端这个用户可以登录

思路很简单,实现起来也不麻烦,下面会列一些核心代码,详细的,请看这个demo,代码可以到这里下载

web端:
二维码生成,使用qrcode库:

/*
 * $source  二维码源字符串
 * $temp  图片缓存目录
 * $errorCorrectionLevel, $matrixPointSize 错误等级、图片大小
 */
function getQRCodeImg($source, $temp, $errorCorrectionLevel, $matrixPointSize){
	$date = date('Ymd');
	$PNG_WEB_DIR = DIRECTORY_SEPARATOR.$temp.DIRECTORY_SEPARATOR.$date.DIRECTORY_SEPARATOR;
	$PNG_TEMP_DIR = dirname(dirname(__FILE__)).DIRECTORY_SEPARATOR.'webroot'.DIRECTORY_SEPARATOR.$temp.DIRECTORY_SEPARATOR.$date.DIRECTORY_SEPARATOR;
	include_once '../lib/qrcode/qrlib.php';
	if (!file_exists($PNG_TEMP_DIR))
		mkdir($PNG_TEMP_DIR);
 
	if (!in_array($errorCorrectionLevel, array('L','M','Q','H')))
		$errorCorrectionLevel = 'M';    
 
	$matrixPointSize = min(max((int)$matrixPointSize, 1), 10);
 
	$filename = md5($source.'|'.$errorCorrectionLevel.'|'.$matrixPointSize).'.png';
 
	QRcode::png($source, $PNG_TEMP_DIR.$filename, $errorCorrectionLevel, $matrixPointSize, 2); 
 
	return $PNG_WEB_DIR.$filename;
}

页面的ajax相当简单:

//ajax轮询时间间隔,通过服务器配置得到
var ASK_TIME = <?php echo conf('ajax.time');?>;
var i = 0;
$(function(){
	setInterval(function(){
		$.get('page_ask.php?source=<?php echo $source;?>', function(d){
			//登录成功,跳转页面
			if(d && d.login){
				window.location = '';
			}
			//测试信息的显示
			$('div.test').html(i++ + '---' + JSON.stringify(d));
		}, 'json');
	}, ASK_TIME * 1000);
});

page_ask.php的核心代码,服务端存储使用mongodb

//ajax轮询检查是否登录		登录成功返回user,否则为false 
function checkSource($source){
	$collection = getDBCollection(conf('mongo.collection'));
	$sou = $collection->findone(array('_id' => $source));
	if($sou && $sou['login'] === 1){
		return $sou['user'];
	}
	return false;
}

客户端通过访问“/client_login.php?source=xxx&user=张三”实现登录,当然,实际的系统应该是传userId

//取记录  返回  1 登录成功  10 source有误  11 此source已使用过  21 无此user(暂无此项)
function checkSource4Login($source, $user){
	$collection = getDBCollection(conf('mongo.collection'));
	$sou = $collection->findone(array('_id' => $source));
	$r = 10;
	if($sou && isset($sou['login'])){
		if($sou['login'] === 0){
			$r = 1;//校验正确
		}else{
			$r = 11;
		}
	}
	//保存登录状态
	if($r == 1){
		//保存登录状态
		$collection->update(
			array('_id' => $source), 
			array('$set' => array('login' => 1, 'user' => $user)), 
			array('safe'=>true)
		);
	}
	return $r;
}

下面是客户端的几个片段,这里使用的是ios
二维码扫描使用的库是ZBar,注意,需要添加一坨frameworks

-(IBAction)startScan:(id)sender
{
    ZBarReaderViewController *reader = [ZBarReaderViewController new];
    reader.readerDelegate = self;
    ZBarImageScanner *scanner = reader.scanner;
    [scanner setSymbology:ZBAR_I25 config:ZBAR_CFG_ENABLE to:0];
    [self presentModalViewController:reader animated:YES];
    [reader release];
}
//将上面这个方法注册给“开始扫描”按钮:
[QRBut addTarget:self action:@selector(startScan:) forControlEvents:UIControlEventTouchUpInside];
//扫描成功后,会自动调用此方法
- (void) imagePickerController: (UIImagePickerController*) picker didFinishPickingMediaWithInfo: (NSDictionary*) info
{
    UIImage *image = [info objectForKey: UIImagePickerControllerOriginalImage];    
    id<NSFastEnumeration> results = [info objectForKey: ZBarReaderControllerResults];
    ZBarSymbol *symbol = nil;    
    for(symbol in results){
        break;
    }    
    if(!symbol || !image){
        return;
    }    
    NSLog(@"symbol.data = %@", symbol.data);  
    [self sendRQcode:symbol.data];  
    [picker dismissModalViewControllerAnimated: YES];
} //imagePickerController

//取得二维码后,通过http方式发送,然后解析返回的json数据

- (void) sendRQcode:(NSString*)code
{
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    NSDate* dat = [NSDate dateWithTimeIntervalSinceNow:0];
    NSTimeInterval a=[dat timeIntervalSince1970];
    NSString *timeString = [NSString stringWithFormat:@"%f", a];
    NSHTTPURLResponse* urlResponse = nil;
    NSError *error = [[NSError alloc] init];
    NSLog(@"-------loginClick------user:%@", timeString);
    NSString *url = [[[[[@"http://xxxxxx/client_login.php?user=" stringByAppendingString:user]                     
            stringByAppendingString:@"&source="] stringByAppendingString:code]
            stringByAppendingString:@"&time="] stringByAppendingString:timeString];
    [request setURL:[NSURL URLWithString:url]];
    [request setHTTPMethod:@"GET"];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];  
    //发送请求  
    NSData *returnData = [NSURLConnection sendSynchronousRequest:request 
            returningResponse:&urlResponse error:&error];
    NSLog(@"-------sendRQcode--status:------%d", [urlResponse statusCode]);
    NSString *login = @"0";
    if(returnData != NULL && [urlResponse statusCode] == 200) {
        //解析json
        NSDictionary *data = [returnData objectFromJSONData];
        NSLog(@"-------sendRQcode----returnData:------%@", data);
        login = [data objectForKey:@"login"];
    }
    NSLog(@"-------sendRQcode----login:------%@", login);
    msg = @"";
    if([login isEqual: @"1"]){
        msg = @"登录成功!";
    }else if([login isEqual:@"0"]){
        msg = @"网络连接错误,请检查网络设置后重试!";
    }else{
        msg = [[@"未知错误(login:" stringByAppendingString:login] stringByAppendingString:@"),请检查网络设置后重试!"];
    }
    NSLog(@"-------sendRQcode----msg:------%@", msg);
    [self alertWithMessage:msg];
}//sendRQcode

———–
代码就展示这么多,具体的,请访问github:https://github.com/jklgithub/qrcode_login
本文为本人原创,转载请注明地址:http://www.jiangkunlun.com/2012/10/%E4%BA%8C%E7%BB%B4%E7%A0%81%E7%99%BB%E5%BD%95_qrcode_zbar/
演示地址:dev.jiangkunlun.com (mongo不太稳定,测试不通的话可以给我留言)

Hello World

html5 localStorage使用备忘

localStorage和sessionStorage是html5提出的web存储方案,难能可贵的是,除了chrome/safari/firefox/opera,老大难的IE,从IE8便开始支持它们!所以,今天好好研究了这两个东西:

1. localStorage地持久的,sessionStorage是只对当前页面,也就是“session”的,关闭浏览器、关闭窗口就会失效,甚至在同一个浏览器中再开一个窗口,都不能相互访问。除此之外,二者的表现是一样的

2. 存储容量的大小:html5标准是5M,但具体的,也依照各个浏览器的实际情况(这一点要多加注意)

3. api

window.localStorage.name = 'rainman';           // 赋值
window.localStorage.setItem('name','cnblogs');  // 赋值
window.localStorage.getItem('name');            // 取值
window.localStorage.removeItem('name');         // 移除值
delete window.localStorage.name                 // 移除值
window.localStorage.clear();                    // 删除所有localStorage

4. 在将value付给localStorage时候,会自然的调一下value.toString(),如果value是一个对象,而没有做变换,保存的就是字符串“[object Object]”~~汗吧。所以这里需要用到JSON.stringify()和JSON.parse(),万幸的是支持localStorage,都支持这两个方法(包括IE8)

BTW:其他web存储方式:
Database Storage是html5的“Web SQL Database”API,可以想操作其他关系型数据库一样,使用sql读写~~可惜的是,只有Chrome支持它
————

Hello World

html5研究—-自动化的manifest

manifest是html5提供的离线web应用解决方案,它的作用有两个:
1. 连网情况下,访问使用manifest的页面时(之前曾经访问过),会先加载一个manifest文件,如果这个manifest文件没有改变,页面相关的资源便都来自浏览器的离线缓存,不会再有额为的网络请求,从而大大提高页面相应时间
2. 断网时,在浏览器地址栏输入页面url,仍然能够正常显示页面,以及正常使用不依赖ajax的功能

之前曾想在手机webview的开发中使用这种功能,但权衡再三,也没有使用,其中一个原因,便是manifest配置太繁琐,很不利于开发和维护,比如,一个正常的manifest文件是这样的:

test.manifest

CACHE MANIFEST
 
# VERSION 0.3
 
# 直接缓存的文件
CACHE:
/index.html
/img/x1.png
/img/x2.png
/img/x3.png
/js/xx.js
/css/xx.css
 
# 需要在线访问的文件
NETWORK:
 
# 替代方案
FALLBACK:

也就是说,每一个要缓存的文件都要配置到“CACHE”下面,每次修改这些文件后,都要修改VERSION~~

但是,离线缓存这个功能又是如此的诱人,所以就想着用其他方式绕开繁琐的配置

首先,test.manifest文件的扩展名必须是manifest静态文件,不能动态声称,于是便想使用php来生成manifest文件,对于apache服务器,再网站根目录下添加.htaccess文件,将/xxx.manifest请求,转给/manifest/xxx.php:

.htaccess

RewriteEngine on
RewriteRule ^([a-zA-Z0-9_\-]{1,})\.manifest$ manifest/$1.php

然后,我们要寻找一个可以自动更新的VERSION,我这里使用的时文件更新时间,对,寻找所有缓存文件中最晚修改过的那个文件的更新时间,然后将可能需要缓存的文件列表打印出来

index.html

<!DOCTYPE HTML>
<html manifest="/test.manifest">
    <head>
        <meta charset="UTF-8">
        <title>manifest测试</title>
        <link rel='stylesheet' href='/css/test.css' />
        <link rel='stylesheet' href='/css/test2.css' />
        <script type='text/javascript' src='/js/test.js'></script>
    </head>
<body>
    <img src="/img/test-pass-icon.png"></img>
    <hr>
    <img src="/img/btn_s.png"></img>
    <hr>
    <img src="/img/charts_images/dragIcon.gif"></img>
    <hr>
    <img src="/img/xx.png"></img>
    <hr>
</body>
</html>

/test.manifest将会转到/manifest/test.php:

/manifest/test.php

<?php 
    require_once 'manifestUtil.php';
    //配置信息
    $webroot = '../';
    $urlroot = '';
    //需要缓存的目录和文件(目录内的所有文件都将被输出)
    $sources = array(
        'img',
        'js/test.js',
        'css',
    );
 
    $updateTime = 0;
    $files = listFiles($sources, $updateTime, $webroot, $urlroot);
?>
CACHE MANIFEST 
# version <?php echo $updateTime;?>
 
CACHE:
/index.html
<?php echoFiles($files);?>
 
NETWORK:
*
 
FALLBACK:

manifestUtil.php

<?php 
//更具配置信息,读取文件列表
function listFiles($sources, &$updateTime, $webroot = '../', $url = ''){
    $r = array();
    foreach($sources as $s){
        $r = array_merge($r, getFiles($webroot.'/'.$s, $updateTime, $url.'/'.$s));
    }
    return $r;
}
//读取文件列表
//$types,需要缓存的文件类型
function getFiles($fileName, &$updateTime = 0, $url = '', $levels = 100, 
        $types = array('jpg', 'png', 'gif', 'jpeg', 'css', 'js')){
    if(empty($fileName) || !$levels){
        return false;
    }
    $files = array();
    if(is_file($fileName)){
        $updateTime = getMax(filectime($fileName), $updateTime);
        $files[] = $url;
    }else if($dir = @opendir($fileName)){
        while(($file = readdir($dir)) !== false){
            if(in_array($file, array('.', '..')))
                continue;
            if(is_dir($fileName.'/'.$file)){
                $files2 = getFiles($fileName.'/'.$file, $updateTime, $url.'/'.$file, $levels - 1);
                if($files2){
                    $files = array_merge($files, $files2);
                }
            }else{
                $updateTime = getMax(filectime($fileName.'/'.$file), $updateTime);
                $type = end(explode(".", $file));
                if(in_array($type, $types)){
                    $files[] = $url.'/'.$file;
                }
            }
        }
    }
    @closedir($dir);
//    echo date("Y-m-d H:i:s",$updateTime).'<hr>';
    return $files;
}
//打印缓存文件列表
function echoFiles($files){
        for($i = 0; $i < count($files); $i++){
            echo $files[$i].'
';//后面加一个换行
        }
}
//取较大的更新时间
function getMax($a, $b){
    return $a < $b ? $b : $a;
}

over~~~就是这些,下面的可能出现的问题:
这个只是初级方案,对于动态网页还有问题,比如,你请求不是静态的html,而是动态的php,而这个php里有require了其他的php,如果这些php发生了修改,上面的方案时不知道的,生成的manifest不会发生变化,这里就需要对方案做一个升级:除$sources之外,再加一个,比如$aboutSources,来配置其他相关文件,拿$aboutSources再调一次listFiles,综合$sources与$aboutSources的updateTime生成VERSION值
另外一个问题是,遍历相关文件,需要多次磁盘IO,如果文件太多,也会消耗较多的时间~~~凡事有利必有弊,如果对此不满,可以手动配置一个VERSION,每次修改后,更新VERSION值
—————
本文为本人原创,转载请注明出处
github:https://github.com/jklgithub/auto_manifest.git
参考:青梅煮酒-HTML5 离线存储实战之manifest