一、项目简介

1.1、项目背景:
这是一个在移动终端创新应用的项目,用户在浏览器端(微信/手Q)即可完成与金秀贤的合影,希望通过这样一种趣味体验,引发用户的分享与转发的热潮。

1.2、系统要求:
ios6-ios7、android3.0-android4.3、android4.4+(非webview内)

1.3、体验地址:

二、初步技术方案确定

在项目前期首先启动了技术预演,确定初步技术方案(*非最终解决方案):

2.1、获取用户照片数据
2.1.1、首先放弃了主动获取用户摄像头的getUserMedia,因为移动终端支持率太低;
2.1.2、确定使用Input控件获取照片文件、使用FileReader读取照片数据,android3.0+、ios6.0+都可以支持。

2.2、编辑合成照片
2.2.1、放弃使用canvas编辑(即将图像数据读取到canvas内进行处理)照片,考虑到开发成本成高;
2.2.2、选用dom编辑(img标签),然后使用html2canvas,方便保存数据。

2.3、保存并上传照片
确定使用canvas的toDataURL接口,提交base64数据到服务器。

三、碰到的那些坑儿

按照既定的技术方案开始执行,开始碰到一个个问题,有些问题可以绕过,有些问题只能推倒重来。

3.1、照片方向反了(如下图所示)

问题描述:
手持设备不同方向所拍摄的照片方向不同,照片的方向都会不同,但相册中会自动纠正,这一问题在ios和android中都存在。
问题解决:
3.1.1、将图片数据转换成二进制数据,方便获取图片的exif信息;(这里我引入了 Binary Ajax
3.1.2、获取图片的exif信息;(这里我使用了 Javascript EXIF Reader
3.1.3、通过图片exif信息,获取图片拍摄时所持设备方向orientation。
关键代码:

// 读取图片数据
var fr = new FileReader();
fr.readAsDataURL(file); fr.onload = function(fe){
var result = this.result;
var img = new Image();
var exif;
img.onload = function() {
var orientation = exif ? exif.Orientation : 1;
// 判断拍照设备持有方向调整照片角度
switch(orientation) {
case 3:
imgRotation = 180;
break;
case 6:
imgRotation = 90;
break;
case 8:
imgRotation = 270;
break;
}
}; // 转换二进制数据
var base64 = result.replace(/^.*?,/,'');
var binary = atob(base64);
var binaryData = new BinaryFile(binary); // 获取exif信息
exif = EXIF.readFromBinaryFile(binaryData); img.src = result;
};

3.2、html2canvas问题重重
问题背景:
为什么要用html2canvas呢,因为我们需要将用户合成照片后的base64数据提交服务器,所以我们需要通过转换为canvas获取照片数据。
问题详情:
3.2.1、图片使用css3 transform旋转了图片方向,但最终html2canvas渲染结果却未保存旋转信息;
3.2.2、html2canvas的渲染起点为网页右上角,而且不能更改设置;
3.2.3、ios大图被压扁了。
问题解决:
但最终因为碰到太多无法绕过的问题,不得不放弃html2canvas的方案,全部转为canvas开发。

3.3、ios大图被压扁了
问题详情:
当照片超过2M时,ios会出现压扁的情况(如下图所示)

问题解决:
获取图片实际比例,重置图片的比例。(stack overflow讨论帖
需要注意的是,ratio的获取是通过检测像素alpha,需要过滤png图片,这在stack overflow的讨论上没有人提出。
关键代码:

var getRatio = function(img) {
if(/png$/i.test(img.src)) {
return 1;
}
var iw = img.naturalWidth, ih = img.naturalHeight;
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = ih;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
var data = ctx.getImageData(0, 0, 1, ih).data;
var sy = 0;
var ey = ih;
var py = ih;
while (py > sy) {
var alpha = data[(py - 1) * 4 + 3];
if (alpha === 0) {
ey = py;
} else {
sy = py;
}
py = (ey + sy) >> 1;
}
var ratio = (py / ih);
return (ratio===0)?1:ratio;
}

3.4、照片太模糊啦,我想提高精度!
问题描述:

如上图所示,为了减少本地内存消耗,项目最初采用尺寸是320×270。在项目上线后,在确保内存占用不过高的情况下,开始尝试开发高清方案,测试地址如下:

在主流设备上测试,性能并无太大问题,但当网络切换为3g时,测试图片合并上传时间8-12s,是原来时间的3倍左右,于是测试了一下3g网络的上传速度:

 
下载速度
上传速度
联通3g
220kb/s
80kb/s
电信3g
180kb/s
60kb/s
移动3g
100kb/s
13kb/s
移动2g
15kb/s
12kb/s

平常会留意用户的下载速度,但对上传速度没太在意,640×540图片的base64数据大小为120kb左右,加上延时,3g环境下平均上传时间是5s左右。于是,上传速度成为了高清方案的瓶颈。

解决方案:
3.4.1、在微信和手Q环境中检测用户环境如果为wifi,则启用高清方案,但由于在这个网站推广的渠道很多,环境复杂,并不能完全解决问题,所以放弃了该解决方式;
3.4.2、在上传前对base64数据进行文本压缩,目前正在尝试lz77压缩,未上线。

3.5、canvas toDataURL bug
问题描述:
已测试,至少在手机QQ浏览器中,canvas对象使用toDataURL方法获取不到任何数据。
问题解决:
使用JPEGEncoder将图片像素数据转换为base64数据。
关键代码:

_public.toDataURL = function(callback){
var self = this;
// 去除编辑状态的元素
self.unSelect(); // 已测手机QQ浏览器canvas.toDataURL有问题,使用jeegEncoder
window.setTimeout(function(){
var encoder = new JPEGEncoder();
var data = encoder.encode(self.canvas.getContext('2d').getImageData(0,0,self.stage.width,self.stage.height), 90);
callback.call(self, data);
}, 1000/self.config.fps)
}

3.6、当getElementOffset遇上transform
问题代码:

Quark.getElementOffset = function(elem)
{
var left = elem.offsetLeft, top = elem.offsetTop;
while((elem = elem.offsetParent) && elem != document.body && elem != document)
{
left += elem.offsetLeft;
top += elem.offsetTop;
}
return {left:left, top:top};
};

问题描述:
当目标元素的上级元素中有使用transform:translate(x,y)时,用如上的方法都会导致offset计算错误,这一bug在常用canvas框架EaseJSQuarkJS,DOM类库Zepto中都存在。我项目中使用的是QuarkJS,碰到具体问题是舞台事件坐标不正确,由于是框架中的bug,足足花了半天时间才追查到。
问题解决:
offsetLeft或offsetTop需要减去translate的差值。

四、项目总结

4.1、最终技术方案
4.1.1、获取用户照片数据
使用Input控件获取照片文件、使用FileReader读取照片数据,android3.0+、ios6.0+都可以支持。
4.1.2、编辑合成照片
4.1.2.1、使用canvas编辑图片,使用canvas框架为QuarkJS;
4.1.2.2、使用binaryajax和exif获取照片信息,用于解决ios bug和照片方向调整;
4.1.3、保存并上传照片
4.1.3.1、使用JPEGEncoder转换为base64数据;
4.1.3.2、使用lz77进行数据压缩

4.2、心得
这个项目进行得并不顺利,经历过1次推翻整体方案重写、1次框架bug纠错、多次系统和浏览器的bug修复,由于线上并没有此类相对成熟的应用,找不到可参考案例,吐槽之余,也总结出一些心得:
4.2.1、对于创新类的应用,前期技术预演很关键,不能只是探索可行性;
4.2.2、选择一个成熟的框架很关键,QuarkJS虽然本身架构不错并且很轻量,但使用它的过程中还是碰到过不少bug或不完善之处,并且文档不详细;
4.2.3、需要善于利用现有技术。这个项目中使用了不少第三方框架来解决特定问题,如果没有这些,项目周期将会相当长。
4.2.4、H5从图像到音频到视频,还有太多领域值得探索,有很大可挖掘的价值,想想都有点小兴奋呢!

4.3、图片编辑类整体代码

/**
* @author Brucewan
* @version 1.0
* @date 2014-07-11
* @description 图片编辑器
* @extends tg.Base
* @name tg.ImageEditor
* @requires zepto.js
* @requires base.js
* @class
*/
tg.add('tg.ImageEditor:tg.Base', function() { /**
* public 作用域
* @alias tg.ImageEditor#
* @ignore
*/
var _public = this; var _private = {}; /**
* public static作用域
* @alias tg.ImageEditor.
* @ignore
*/
var _static = this.constructor; _public.constructor = function(config) {
this.config = Zepto.extend(true, {}, _static.config, config); // 参数接收
this.init();
} // 插件默认配置
_static.config = {
width: 320,
height: 320,
fps: 60
}; /***
* 初始化
* @description 参数处理
*/
_public.init = function(){
var self = this;
var config = self.config; // 自定义事件绑定
self.effect && self.on(self.effect);
config.event && self.on(config.event); if(self.trigger('beforeinit') === false) {
return;
} // 创建canvas
var canvas = Quark.createDOM('canvas', {
width: config.width,
height: config.height,
style: {backgroundColor:"#fff"}
});
canvas = $(canvas).appendTo(config.container)[0]; var context = new Quark.CanvasContext({canvas:canvas});
self.stage = new Quark.Stage({width:config.width, height:config.height, context:context});
self.canvas = canvas;
self.context = context; // register stage events
var em = this.em = new Quark.EventManager();
em.registerStage(self.stage, ['touchstart', 'touchmove', 'touchend'], true, true);
self.stage.stageX = config.stageX !== window.undefined ? config.stageX : self.stage.stageX;
self.stage.stageY = config.stageY !== window.undefined ? config.stageY : self.stage.stageY; var timer = new Quark.Timer(1000/config.fps);
timer.addListener(self.stage);
timer.addListener(Quark.Tween);
timer.start(); var bg = new Q.Graphics({width:config.width, height:config.height});
bg.beginFill("#fff").drawRect(0, 0, config.width, config.height).endFill().cache();
self.stage.addChild(bg) _private.attach.call(self);
}; _private.attach = function(){
var self = this;
var config = self.config; config.trigger.on('change', function(e){
self.trigger('beforechange'); // 只上传一个文件
var file = this.files[0]; // 限制上传图片文件
if(file.type && !/image\/\w+/.test(file.type)){
alert('请选择图片文件!');
return false;
} var fr = new FileReader();
fr.readAsDataURL(file); fr.onload = function(fe){
var result = this.result;
var img = new Image();
var exif;
img.onload = function() {
self.addImage({img: img, exif: exif});
self.trigger('change');
};
// 转换二进制数据
var base64 = result.replace(/^.*?,/,'');
var binary = atob(base64);
var binaryData = new BinaryFile(binary); // get EXIF data
exif = EXIF.readFromBinaryFile(binaryData); img.src = result; }; }); self.stage.addEventListener('touchstart', function(e){
if(self.imgs) {
for(var i = 0; i < self.imgs.length; i++) {
self.imgs[i].disable();
}
}
if(e.eventTarget && e.eventTarget.parent.enEditable) {
e.eventTarget.parent.enEditable();
self.activeTarget = e.eventTarget.parent;
}
});
self.stage.addEventListener('touchmove', function(e){
var touches = e.rawEvent.touches || e.rawEvent.changedTouches;
if(e.eventTarget && (e.eventTarget.parent == self.activeTarget) && touches[1]) {
var dis = Math.sqrt(Math.pow(touches[1].pageX - touches[0].pageX, 2) + Math.pow(touches[1].pageY - touches[0].pageY, 2) );
if(self.activeTarget.mcScale.touchDis) {
var scale = dis / self.activeTarget.mcScale.touchDis -1;
if( self.activeTarget.getCurrentWidth() < 100 && scale < 0) {
scale = 0;
} self.activeTarget.scaleX += scale;
self.activeTarget.scaleY += scale;
}
self.activeTarget.mcScale.touchDis = dis;
}
});
self.stage.addEventListener('touchend', function(){
if(self.activeTarget && self.activeTarget.mcScale) {
delete self.activeTarget.mcScale.touchDis;
}
}); }; _public.addImage = function(info){
var self = this;
var config = self.config;
var img = info.img;
var exif = info.exif;
var imgContainer;
var mcScale;
var mcClose;
var imgWidth = img.width;
var imgHeight = img.height;
var imgRotation = 0;
var imgRegX = 0;
var imgRegY = 0;
var imgX = 0;
var imgY = 0;
var posX = info.pos ? info.pos[0] : 0;
var posY = info.pos ? info.pos[1] : 0;
var imgScale = 1;
var orientation = exif ? exif.Orientation : 1;
var getRatio = function(img) {
if(/png$/i.test(img.src)) {
return 1;
}
var iw = img.naturalWidth, ih = img.naturalHeight;
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = ih;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
var data = ctx.getImageData(0, 0, 1, ih).data;
var sy = 0;
var ey = ih;
var py = ih;
while (py > sy) {
var alpha = data[(py - 1) * 4 + 3];
if (alpha === 0) {
ey = py;
} else {
sy = py;
}
py = (ey + sy) >> 1;
}
var ratio = (py / ih);
return (ratio===0)?1:ratio;
}
var ratio = getRatio(img); // window.setTimeout(function(){
// alert(imgContainer.width);
// alert(img);
// }, 5000) if(typeof img == 'string') {
var url = img;
img = new Image();
img.src = url;
} // 判断拍照设备持有方向调整照片角度
switch(orientation) {
case 3:
imgRotation = 180;
imgRegX = imgWidth;
imgRegY = imgHeight * ratio;
// imgRegY -= imgWidth * (1-ratio);
break;
case 6: imgRotation = 90;
imgWidth = img.height;
imgHeight = img.width;
imgRegY = imgWidth * ratio ;
// imgRegY -= imgWidth * (1-ratio);
break;
case 8:
imgRotation = 270;
imgWidth = img.height;
imgHeight = img.width;
imgRegX = imgHeight * ratio; if(/iphone|ipod|ipad/i.test(navigator.userAgent)) {
alert('苹果系统下暂不支持你以这么萌!萌!达!姿势拍照!');
return;
} break; }
imgWidth *= ratio;
imgHeight *= ratio; if(imgWidth > self.stage.width) {
imgScale = self.stage.width / imgWidth;
} imgWidth = imgWidth * imgScale;
imgHeight = imgHeight * imgScale; imgContainer = new Quark.DisplayObjectContainer({width: imgWidth, height: imgHeight});
imgContainer.x = posX;
imgContainer.y = posY; img = new Quark.Bitmap({image:img, regX:imgRegX, regY:imgRegY});
img.rotation = imgRotation;
img.x = imgX;
img.y = 0;
img.scaleX = imgScale * ratio;
img.scaleY = imgScale; if(config.iconScale && !info.disScale) {
var iconScaleImg = new Image();
iconScaleImg.onload = function(){
var rect = config.iconScale.rect;
mcScale = new Quark.MovieClip({image:iconScaleImg});
mcScale.addFrame([{rect: rect}]);
mcScale.x = imgWidth - rect[2];
mcScale.y = 0;
mcScale.alpha = 0.5;
mcScale.visible = false;
mcScale.addEventListener('touchstart', function(e){
mcScale.scaleable = true;
mcScale.startX = e.eventX;
mcScale.startY = e.eventY;
mcScale.alpha = 0.8;
var curW = imgContainer.getCurrentWidth();
var scaleMove = function(e){
if(mcScale.scaleable) {
// 缩放
var disX = e.eventX - mcScale.startX;
var scaleX = (curW+disX)/imgContainer.width; if( imgContainer.getCurrentWidth() < 100 && imgContainer.scaleX > scaleX) {
return;
} imgContainer.scaleX = scaleX;
imgContainer.scaleY = scaleX; // 旋转
var disOriX = e.eventX - imgContainer.x;
var disOriY = e.eventY- imgContainer.y;
var rotate = Math.atan2(disOriY,disOriX) * 360 / (2 * Math.PI);
imgContainer.rotation = parseInt(rotate/1)*1;
}
};
var scaleEnd = function(e) {
mcScale.scaleable = false;
mcScale.alpha = 0.5;
self.stage.removeEventListener('touchmove', scaleMove);
self.stage.removeEventListener('touchend', scaleEnd);
}
self.stage.addEventListener('touchmove', scaleMove);
self.stage.addEventListener('touchend', scaleEnd);
});
imgContainer.mcScale = mcScale;
imgContainer.addChild(mcScale);
};
iconScaleImg.src = config.iconScale.url;
} var border = new Q.Graphics({width:imgWidth+10, height:imgHeight+10, x:-5, y:-5});
border.lineStyle(5, "#aaa").beginFill("#fff").drawRect(5, 5, imgWidth, imgHeight).endFill().cache();
border.alpha = 0.5;
border.visible = false;
imgContainer.addChild(border); if(config.iconClose) {
var iconCloseImg = new Image();
iconCloseImg.onload = function(){
var rect = config.iconClose.rect;
mcClose = new Quark.MovieClip({image:iconCloseImg});
mcClose.addFrame([{rect: rect}]);
mcClose.x = 0;
mcClose.y = 0;
mcClose.alpha = 0.5;
mcClose.visible = false;
mcClose.addEventListener('touchstart', function(e){
mcClose.alpha = 0.8;
});
mcClose.addEventListener('touchend', function(e){
self.stage.removeChild(imgContainer);
});
self.stage.addEventListener('touchend', function(e){
mcClose.alpha = 0.5;
});
imgContainer.addChild(mcClose);
};
iconCloseImg.src = config.iconClose.url;
} if(!info.disMove && !info.disable) {
img.addEventListener('touchstart', function(e){
var fnMove;
var fnEnd;
// 拖动
img.curW = imgContainer.getCurrentWidth();
img.curH = imgContainer.getCurrentHeight();
img.moveabled = true;
img.startX = e.eventX;
img.startY = e.eventY; fnMove = function(e){
// 是否双指按下
var isScale = e.rawEvent && e.rawEvent.touches[1]; if(img.moveabled && !isScale) {
var disX = e.eventX - img.startX;
var disY = e.eventY - img.startY;
var setX = imgContainer.x + disX;
var setY = imgContainer.y + disY; var diffX = 0, diffY = 0; if(setX < -img.curW/2 + 5 && disX < 0) {
setX = -img.curW/2;
}
if(setY < -img.curH/2 + 5 && disY < 0) {
setY = -img.curH/2;
}
if(setX > -img.curW/2 + self.stage.width - 5 && disX > 0) {
setX = self.stage.width - img.curW/2;
}
if(setY > self.stage.height - 5 && disY > 0) {
setY = self.stage.height;
} imgContainer.x = setX;
imgContainer.y = setY;
img.startX = e.eventX;
img.startY = e.eventY;
}
}; fnEnd = function(){
img.moveabled = false;
self.stage.addEventListener('touchmove');
self.stage.addEventListener('touchend');
}
self.stage.addEventListener('touchmove', fnMove);
self.stage.addEventListener('touchend', fnEnd); });
} imgContainer.enEditable = function(){
if(info.disable) {
return;
}
border.visible = true;
if(mcScale) {
mcScale.visible = true;
}
if(mcClose) {
mcClose.visible = true;
}
}
imgContainer.disable = function(){
border.visible = false;
if(mcScale) {
mcScale.visible = false;
}
if(mcClose) {
mcClose.visible = false;
}
} img.update = function(){
if(imgContainer && imgContainer.scaleX) {
if(mcScale && mcScale.scaleX) {
mcScale.scaleX = 1/imgContainer.scaleX;
mcScale.scaleY = 1/imgContainer.scaleY;
mcScale.x = border.getCurrentWidth() - 10 - mcScale.getCurrentWidth();
}
if(mcClose && mcClose.scaleX) {
mcClose.scaleX = 1/imgContainer.scaleX;
mcClose.scaleY = 1/imgContainer.scaleY;
mcClose.x = 0;
}
} } // imgContainer.rotation = 10; imgContainer.addChild(img); self.stage.update = function(){
// console.log(0)
// img.rotation ++;
} imgContainer.update = function(){
// this.rotation ++;
} self.stage.addChild(imgContainer); if(self.imgs) {
self.imgs.push(imgContainer);
} else {
self.imgs = [imgContainer];
} // self.imgContainer.addEventListener('touchend', function(){
// alert('sss')
// }); return imgContainer; }; _public.clear = function(){
if(this.imgs) {
for(var i = 0; i < this.imgs.length; i++) {
this.stage.removeChild(this.imgs[i]);
}
}
}; _public.unSelect = function(){
var imgs = this.imgs;
if(imgs) {
for(var i = 0; i < imgs.length; i++) {
imgs[i].disable();
}
}
}; _public.toDataURL = function(callback){
var self = this;
// 去除编辑状态的元素
self.unSelect(); // 已测手机QQ浏览器canvas.toDataURL有问题,使用jeegEncoder
window.setTimeout(function(){
var encoder = new JPEGEncoder();
var data = encoder.encode(self.canvas.getContext('2d').getImageData(0,0,self.stage.width,self.stage.height), 90);
callback.call(self, data);
}, 1000/self.config.fps)
} });

H5拍照应用开发经历的那些坑儿的更多相关文章

  1. H5 移动端开发中 ios/安卓坑 和经验总结

    1. ios new时间对象,需要用逗号隔开传日期的方式, 不支持 new Date('2019-03-01 08:00:00') 格式: 支持以下两种方式: 2. ios个别版本对fixed的属性的 ...

  2. 如何快速上手一款新的嵌入式CPU芯片(记录CC2540开发经历)

    新换了工作,需要熟悉新公司的产品开发项目,更新博客就懈怠了,不过环境的不同,也让我对嵌入式开发有了更深刻的理解.在原公司我主要负责在STM32F207芯片平台上, 利用UCOS+LWIP进行嵌入式服务 ...

  3. Egret白鹭H5小游戏开发入门(二)

    前言: 昨天的文章中简单的介绍了Egret白鹭引擎从安装到基本的使用配置等问题,今天着重介绍H5小游戏开发的起步阶段,如Wing面板的使用,素材的处理,类的说明,开始布局等等. 整体概况: 根据上一篇 ...

  4. 高拍仪拍照SDK开发(良田影像S300L|S500L)

    高拍仪拍照SDK开发下载地址:点击下载 本SDK适用于:良田影像S300L|S500L 高拍仪如图: SDN开发包安装之后找到安装目录,如图: 大家找到各自需要的版本即可,需要注意的是如果需要上传图片 ...

  5. Egret白鹭H5小游戏开发入门(三)

    前言: 在上一篇文章中着重介绍了H5小游戏开发的起步阶段,如Wing面板的使用,素材的处理,类的说明等等,那么今天主要是涉及到场景的创建,loading的修改等等的代码编写. 对于这一节,我在讲解的过 ...

  6. Android开发需要注意的坑

    Android开发需要注意的坑一览​对于一些Android开发过程中坑爹.细小,但又重要的错误的总结​Android开发在路上:少去踩坑,多走捷径其他参考: ​google官方版本发布图 ​umeng ...

  7. 用做网页开发经历了三个阶段(附长篇讨论) good

    用做网页开发经历了三个阶段:第一阶:傻干阶段使用Intraweb,傻瓜型,无需知道javascript,html,css,会pascal就可以了. 第二阶:困惑阶段使用Intraweb,有很多限制,比 ...

  8. 移动端H5拍照代码实现及外网部署

    最近的工作中,遇到了一个需求:对于无APP登陆权限的人员,提供拍照上传功能,以便生成更完善的出工记录.经研究讨论,决定实现的机制为:由合法的人员登陆APP认领相关工作任务,并生成当天当工作的唯一二维码 ...

  9. H5微信页面开发 IOS系统 input输入框失去焦点,软键盘关闭后,被撑起的页面无法回退到原来正常的位置,导致弹框里的按钮响应区域错位

    H5微信页面开发,软键盘弹起后,若原输入框被遮挡,页面整体将会上移,然而当输入框失焦,软键盘收起后,页面未恢复,导致弹框里的按钮响应区域错位. 解决方案:给输入框(或select选择框)添加失去焦点的 ...

随机推荐

  1. C语言 · 矩阵乘法 · 算法训练

    问题描述 输入两个矩阵,分别是m*s,s*n大小.输出两个矩阵相乘的结果. 输入格式 第一行,空格隔开的三个正整数m,s,n(均不超过200). 接下来m行,每行s个空格隔开的整数,表示矩阵A(i,j ...

  2. solr_架构案例【京东站内搜索】(附程序源代码)

    注意事项:首先要保证部署solr服务的Tomcat容器和检索solr服务中数据的Tomcat容器,它们的端口号不能发生冲突,否则web程序是不可能运行起来的. 一:solr服务的端口号.我这里的sol ...

  3. Pivot 和 Unpivot

    在TSQL中,使用Pivot和Unpivot运算符将一个关系表转换成另外一个关系表,两个命令实现的操作是“相反”的,但是,pivot之后,不能通过unpivot将数据还原.这两个运算符的操作数比较复杂 ...

  4. centos7+mono4+jexus5.6.2安装过程中的遇到的问题

    过程参考: http://www.linuxdot.net/ http://www.jexus.org/ http://www.mono-project.com/docs/getting-starte ...

  5. [Egret]优雅的写http

    首先,自从使用链式调用的写法后,就一发不可收拾的喜爱上了这种优雅的方式.不管是写架构还是写模块,我都会不自觉的使用这种最优雅的方式.链式写法既减少了代码量,又非常优雅的. 在使用 egret 的htt ...

  6. Java进击C#——前言

    本章简言 记得三年前笔者来到现在的公司的时候,公司人口不出十个人.那个时候笔者刚从日本回来,想在福州.厦门.青岛找一个合适自己发展的机会.最后我的一个福州的朋友打电话希望我能过去帮他,跟他一起创业.这 ...

  7. 推荐一个ASP.NET网站内容管理系统源码

    许多人都有各自的兴趣,如打球.踢毽子.看书.看电视.玩游戏等等....我近来迷上了猜灯谜,于是业余做了一个在线猜灯谜的网站:何问起谜语. 先出个谜语让你猜猜:不可缺一点(打一字).可以在线猜:http ...

  8. isEmpty和isNull()区别

     isEmpty和isNull()区别一个NULL字符串一定是一个空串,一个空串未必是一个NULL字符串例如:QString().isNull():   //结果为trueQString().isEm ...

  9. Lind.DDD.LindAspects方法拦截的介绍

    回到目录 什么是LindAspects 之前写了关于Aspects的文章<Lind.DDD.Aspects通过Plugins实现方法的动态拦截~Lind里的AOP>,今天主要在设计思想上进 ...

  10. 简单酷炫的canvas动画

    作为一个新人怀着激动而紧张的心情写了第一篇帖子还请大家多多支持,小弟在次拜谢. 驯鹿拉圣诞老人动画效果图如下 html如下: <div style="width:400px;heigh ...