竹磬网-邵珠庆の日记 生命只有一次,你可以用它来做些更多伟大的事情–Make the world a little better and easier


65月/180

Redis内存分析方法

发布在 邵珠庆

一般会采用 bgsave 生成 dump.rdb 文件,再结合 redis-rdb-tools 和 sqlite 来进行静态分析。

BGSAVE:在后台异步(Asynchronously)保存当前数据库的数据到磁盘。

BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。

生成内存快照:redis-rdb-tools 是一个 python 的解析 rdb 文件的工具,在分析内存的时候,主要用它生成内存快照。

redis-rdb-tools 安装:

使用 PYPI 安装:

pip install rdbtools

使用 源码安装:

git clone https://github.com/sripathikrishnan/redis-rdb-tools
cd redis-rdb-tools
sudo python setup.py install

使用 redis-rdb-tools 生成内存快照:

rdb -c memory dump.rdb > memory.csv

生成 CSV 格式的内存报告。包含的列有:数据库 ID,数据类型,key,内存使用量(byte),编码。内存使用量包含 key、value 和其他值。

内存使用量是理论上的近似值,在一般情况下,略低于实际值。

[ares:~/Desktop$head memory.csv
database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
0,string,trade.coupon.id:653601465,112,string,8,8
0,string,trade.coupon.id:631354838,112,string,8,8
0,string,trade.coupon.id:632477800,112,string,8,8
0,string,trade.coupon.id:620802294,112,string,8,8
0,string,trade.coupon.id:631432959,112,string,8,8
0,string,trade.coupon.id:632933399,112,string,8,8
0,string,trade.coupon.id:632117725,112,string,8,8
0,string,trade.coupon.id:634240609,112,string,8,8
0,string,trade.coupon.id:646317603,112,string,8,8

注:若csv文件不大,可直接用相关软件打开,以size_in_bytes列排序,可以看到大致内存使用。

使用SQLite分析内存快照:

SQLite版本必须是3.16.0以上。

导入memory.csv数据库:

$sqlite3 memory.db
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> create table memory(database int,type varchar(128),key varchar(128),size_in_bytes int,encoding varchar(128),num_elements int,len_largest_element varchar(128));
sqlite> .mode csv memory
sqlite> .import memory.csv memory

数据导入后,可以随处理:

查询key总数:

sqlite> select count(*) from memory;
31143847

查询key总占用内存:

sqlite> select sum(size_in_bytes) from memory;
17391950414.0

查询内容占用最高的几个key:

sqlite> select key,size_in_bytes from memory order by size_in_bytes desc limit 10;
key,size_in_bytes
public.xx.xx:xx,7860169636
public.xx.xx:xx,3043206524
public.xx.xx:xx,1866022916
public.xx.xx:xx,420931316
public.xx.xx:idxx171118172
xx,162984940
xx,133443892
public.xx.xx:xx,80925132
public.xx.xx:xx,28340356
25月/170

抗压面试题分析

发布在 邵珠庆

问题1

  什么样的情形会让你感到沮丧?

分析:这个问题是用来发现你的致命弱点的。它会告诉面试人,什么样的紧张和压力可以让你失去希望、动力或行动能力。

错误回答:我很少处于沮丧之中,因为我非常有弹性。有些事情会让其他人感到非常沮丧,但对我往往只会有稍微的影响。

评论:求职者没有说出任何真正的致命弱点(面试中你永远不要说出自己的致命弱点),这一点是对的。但是,求职者否认存在任何使之沮丧的情况,这会让面试人感到怀疑。此外,在这种回答中,求职者还破坏了“不要打击别人”的法则,从而暴露了自己的弱点。

正确回答:我认为会让我感到沮丧的是一件事情拖得太久,虽然这并不经常发生。我认为,对于尚未解决的问题,并不是所有的成功企业都会有回旋的余地。我希望尽可能快地找到好的对策,这样我们就可以继续开展企业的业务。

评论:这种回答提供了一种真正的答案,而且它也不是软弱无力的。这样回答既合理,又不会让面试人对求职者的能力感到担心。它会使面试人确信,求职者重视质量和时间进度。

  问题2

  如果你在销售一种产品,遇上一位客户一直抱怨你的售后服务很糟糕,这时你会怎么办?

分析:从这个问题的回答可以看出求职者会如何应对一些难缠的客户。面试人期待求职者不要显得那么容易屈服。

错误回答:我记得一句谚语说:“客户永远是正确的。”我能够确保客户在离开时对我的产品感到非常满意。

评论:姜还是老的辣!面试人可能在他(她)的祖父那儿就听到过“客户永远是正确的”这句谚语。不要把面试人当小孩子来看待,记住,他(她)的智商不会比你逊色,甚至还会比你高。

正确回答:我将向客户解释,我们的企业向来以产品质量和优质服务为荣。然后我将向客户保证,我会尽一切努力来改善这种状况。接下来我会听他(她)抱怨,并查找问题的根源,做出必要的改进来满足客户。

评论:这个回答比“错误回答”里的陈词滥调要高明得多,而且也表明求职者重视服务质量。这样的回答显示出求职者没有被问题所吓倒,他(她)将采取必要的措施来解决问题。

  问题3

  当你确信自己是正确的,但是其他人却不赞同你时,你会怎样做?

分析:这个问题可以反映求职者是否能够恰当处理反对观点、是否能够承受额外压力,还可以显示求职者处理冲突的能力和自信程度。

错误回答:首先,我努力找到一种方法让他们相信我是正确的。如果这样做不奏效——实际上经常不奏效——我会思量是否有办法实现他们的目标,这样,对于我自认为正确的方式,他们就不会再干涉。

评论:除了有自大狂的嫌疑外,这种回答还存在其他问题。它意味着,如果求职者不能从反对者那里得到支持,他(她)将采取一切必要措施实现自己的方式。这种回答说明,在面对困难或者可能存在冲突的问题时,求职者就会失去道德标准。

正确回答:首先,我会确保有足够的信息来支持自己。一旦我确信自己的观点是正确的,我就会密切关注反对者的具体反对理由。我将从他们的角度看待问题,并以此说服他们。由于互相尊重,我相信我们可以最终达成协议。

评论:这种说法实现了几个目的。它表明求职者可以从解决问题的角度,用一种双赢的态度解决冲突;也表明,如果可以真正解决问题,那么求职者能够敞开胸怀接受改变;它还表明,求职者会采取一种合作的方式来解决困难问题。

  问题4

  你是否觉得有能力在自己的职位上取得成功?如果感到不妥,你将如何弥补自己的缺点?

分析:同其他涉及弱点的问题一样,你必须小心应对这个问题。如果承认自己有重大缺点,而且这些缺点将妨碍你的工作,这无疑将会使你失去机会。如果确实有一些微小的缺陷需要克服,那么可以提及一下。否则,如果不怕沉默会造成误解的话,你最好还是等到上任的时候再表白自己需要改进的地方。

错误回答:这个职位对我来说是小菜一碟,我闭着眼睛都能把它做好。我想,如果你聘用了我,你一定会对我如此迅速地完成任务而感到惊奇。这个岗位上没有什么我不能做的事情。

评论:尽管这种回答听起来很有力,但求职者显得过于狂妄了。除了会使面试人怀疑他(她)夸夸其谈的背后可能隐藏了什么东西外,面试人也会怀疑求职者是否适合这个职位。如果求职者能够做如此广泛的工作,或许这份工作的挑战性还不够。

正确回答:尽管我确信自己还有很多东西要学——在每个新工作中都是这样——但我认为,你会发现我是一个学得很快的人。我相信自己的能力和天分可以满足你们的需要。我不认为前面的路上会有什么不可克服的困难。

评论:尽管承认需要学习一些新东西,但是求职者表明自己有能力完成手头的工作。

  问题5

  完成这个句子:成功的经理应该……

分析:这个问题可以用来评价你在企业内的发展潜力。即使你正在申请的工作没有管理职责,你的回答也可以让面试人深入了解你的管理潜力,同时可以深入了解你眼中的经理会是什么样子。

错误回答:为了确实能熟练地处理事务,我认为,除了管理别人之外,一个成功的经理还应该更多地了解有关工作的信息。这是惟一能够对员工保持控制的方式——只有知道得比他们多才能对他们加以控制。一旦你失去威望,你就很难再挽回。你必须要比自己的员工领先一步。

评论:这种回答的第一个问题在于,在求职者看来,管理责任听起来像是充满敌意的,而不是合作性的。更大的问题是,求职者认为,在被管理的所有员工中,在他们所从事的一切事情上,你都应该比他们知道得多,在他看来,这不仅是可能的,而且更是可取的。任何人,只要他了解技术、信息传播以及企业流程等方面变化的频率和速度,他就会知道这种方法是不切实际的,也是注定要失败的。

正确回答:一个成功的经理应该能够及时分析形势,确定合适的战略并采取行动。然而,我认为最重要的是能够理解别人。每个人都是独一无二的。意识到这一点并且在工作中适应每个人的工作方式,这就是成功管理的全部内容。

评论:这个回答尽管只有几句话,但它清楚而自信地表达了求职者的工作方式,说明求职者有一种行之有效的管理方法。这种回答还表明,求职者理解管理的难度——任何经理听到这种说法都会很高兴。

  问题6

  你能够在压力状态下工作得很好吗?

分析:很显然,这个问题是要直接了解求职者对压力的反应。

错误回答:我在压力下会茁壮成长,实际上,事情变得越乱我就越高兴。毕竟,如今的社会是一个充满竞争的社会,在这个社会里,没有压力就不会有成功。相比之下,我更怕无聊。如果无事可做,我就会变得很懒散,但应对压力我就没问题了。

评论:这种回答会让人怀疑:“真有这样的人吗?”除了不可信之外,它还表达了一种消极论调:如果没有压力,求职者就不会得到激励。

正确回答:在从事有价值的工作时,任何人都会在工作中时不时地遇到压力。我能够应付一定量的压力,甚至在有些情况下还可以承受极大的压力。对我来说,应对压力的关键是找到一种方法控制形势,从而减轻压力的剧烈程度——通过这种方式,压力就不会影响我的生产力。我知道任何工作都有压力,如果必要的话,我会在压力下工作得很好。

评论:这种回答表明,求职者对工作压力的本质和程度都有比较现实的期望。这种回答很有说服力,但又没有对压力表现出过度热情。求职者的表述还说明,他(她)在过去曾经应对过压力,而且还制定过策略有效地处理了工作中的压力。

93月/170

jQuery源码分析

发布在 邵珠庆

前言

有时候我在想jQuery为什么可以直接$操作,可以拥有比原生js更便利的DOM操作,而且只要你想就可以直接链式操作下去

核心框架

揭开一万多行代码的jQuery核心代码:

(function(window, undefined) {
	function jQuery(selector){
		return new jQuery.fn.init(selector)
	}
	jQuery.fn = jQuery.prototype = {
		init: function () {

		}
	}
	jQuery.fn.init.prototype = jQuery.fn;
	window.jQuery = window.$ = jQuery;
})(window)
  • 闭包结构传参window
    • 闭包结构传入实参window,然后里面用形参接收
      • 减少内部每次引用window的查询时间
      • 方便压缩代码
  • 形参undefined
    • 因为ie低版本的浏览器可以给undefined赋值成功,所以为了保证undefined的纯洁给它一个形参的位置而没有实参,保证了它一定是undefined
  • jQuery传参selector
    • selector可以是一对标签,可以是id、类、后代、子代等等,可以是jQuery对象,
  • jQuery原型对象赋值
    • 方便扩展jQuery的原型方法
  • return 实例化原型方法init
    • 其实就是为了我们每次使用$不用new $();
    • 为什么jQuery要new自己的原型方法呢,因为不new自己的就要new其他的函数返回,那干嘛不自己利用自己
  • jQuery原型对象赋值给jQuery原型方法init的原型
    • 因为内部给jQuery原型每扩展一个方法init也会有该方法,是不是很酷炫,init有了那么$()出来的jQuery对象是不是也有啦
  • 给window暴露可利用成员jQuery,$
    • 给window暴露后那么全局都可以直接使用了jQuery和$了
    • 至于为什么有$,因为短啊,当然你也可以每次jQuery()来使用

御用选择器-Sizzle

  • Sizzle也是jQuery的根本,当然了你也单独使用Sizzle
  • 上面说过$(selector)的参数selector可以是id、类、后代、子代等等,可以是jQuery对象,那么咱们每次$一下就可以心想事成的得到我们想要的jQuery对象是怎么办到的呢,没错,就是因为Sizzle,Sizzle封装了获取各种dom对象的方法,并且会把他们包装成jQuery对象
  • 浏览器能力测试
    • Sizzle内部有个support对象,support对象存储着正则测试浏览器能力的结果
    • 对于有能力问题的选择器使用通用兼容方案解决(繁琐的判断代码)
  • 正则
    • 正则表达式在jQuery中使用的还是比较多的,正则的使用可以很大的提交我们对数据的处理效率
  • 判断
    • 判断是在init内部判断selector的类型,
      • 列如可能是个html标签,那么直接create一个selector标签的DOM对象包装成jQuery对象return出去
      • 列如可能是个id名、类名、标签名等等,那么直接通过Sizzle获取到DOM对象包装成jQuery对象return出去
  • 包装
    • 我已经说了很多次的包装了,没错,jQuery对象其实也是个伪数组,这也是它的设计巧妙之处,因为用数组存储数据方便我们去进行更多的数据处理,比如$("div").css("color": "red"),那么jQuery会自动帮我们隐式迭代、再给页面上所有div包含的文字颜色设置为red,简单粗暴的一行代码搞定简直是程序猿的最爱

对外扩展-extend

  • jQuery核心的结构处理完毕之后基本上就可以对外使用了,但是我们知道我们是可以基于jQuery来实现插件的,包括jQuery自己可扩展性也必须要求他要对外提供一个接口方便进行二次开发,所以有了extend方法
  • 简单的extend就是混入,列子:
    function extend(obj) {
        var k;
        for(k in obj) {
            this[k] = obj[k];
        }
    }

    Baiya.extend = extend;
    Baiya.fn.extend = extend;

对静态方法的和实例方法的扩展都要有,比如each方法,可以$.each来使用,也可以是$("div").each来使用

  • 之后jQuery一些方法都是基于extend来扩展的,当然我们自己也可以基于jQuery扩展方法

DOM操作

  • DOM操作也是jQuery的一大特点,因为它太好用了,包含了我们所能想到的所有使用场景,完善了增删查改常用的方法
  • jQuery获取和设置类的方法如html()/css()/val()等等这些传参是设置值不传参是获取值

##链式编程

  • jQuery是支持链式编程的,只要你想你就可以一行代码写完所有的功能,这是怎么做到的呢
  • 每一个改变原型链的方法都会把当前的this对象保存成他自己的属性,然后可以调用end方法找到上一级链从而方便我们可以进行链式操作

事件操作

  • jQuery的事件操作一般可以通过click类(mouseover/mouseleave等等)和on来使用,但是click类的实现是调用on的
  • on的实现是对原生的onclick类的处理,因为相同的原生的事件在同一个DOM对象上只能被绑定一次,如果再次绑定会覆盖掉上一次的,所以jQuery帮我们封装了事件的存储,把相同的事件分成一个数组存储在一个对象里面,然后对数组进行遍历,依次调用数组里存储的每个方法
  • on实现之后会把所有的事件处理字符串处理一下用on来改造一下,如下:
    Baiya.each(("onclick,onmousedown,onmouseenter,onmouseleave," +
    "onmousemove,onmouseout,onmouseover,onmouseup,onfocus," +
    "onmousewheel,onkeydown,onkeypress,onkeyup,onblur").split(","),     function (i, v) {
        var event = v.slice(2);
        Baiya.fn[event] = function (callback) {
            return this.on(event, callback);
        }
    });

属性操作

  • jQuery也提供给了我们方便的属性操作,底层就是对原生方法的包装,处理兼容性问题,如果jQuery不对IE浏览器的兼容处理的话,那么它的代码量可能会缩一半,当然锅不能全部甩给IE,比如innerText方法火狐是不支持的,但是支持textContent方法,所以jQuery会尽可能的处理这种浏览器带来的差异

样式操作

  • 基本思想如上

Ajax操作

  • Ajax可以说是前端的跨越性进步,毫不夸张的说如果没有Ajax的发展,那么今天的前端可能不叫前端,可能是美工……
  • Ajax是什么?
    • 在我的理解来看Ajax就是一个方法,这个方法遵循着http协议的规范,我们可以使用这个方法来向服务器请求少量的数据,有了数据之后我们就可以操作DOM来达到局部更新网页的目的,这是一个非常酷的事情
  • jQuery的Ajax是基于XMLHttpRequest的封装,当然了他也有兼容性问题,具体的封装见我之前的文章简单的ajax封装
  • 具体就是区别get和post请求的区别,get请求的传参是直接拼接在url结尾,而post请求需要在send()里面传递,并且post请求还要设置请求头setRequestHeader("content-type", "application/x-www-form-urlencoded")
  • 请求后对json或者text或者xml的数据进行处理就可以渲染到页面了

提到Ajax就不得不提到跨域了

  • 跨域简单的来说限制了非同源(ip/域名/端口/协议)的数据交互,当然这肯定是极好的,因为如果不限制那么你的网页别人也可以操作是不是很恐怖
  • 但是有些情况下我们需要调用别人的服务器数据,而且别人也愿意怎么办呢,程序员是很聪明的,html标签中img,script,link等一些带有src属性的标签是可以请求外部资源的,img和link得到的数据是不可用的,只有script标签请求的数据我们可以通过函数来接收,函数的参数传递可以是任何类型,所以创建一个函数,来接收,参数就是请求到的数据,而对方的数据也要用该函数来调用就可以实现跨域了
  • 简单封装jsonp实现
        // url是请求的接口
        // params是传递的参数
        // fn是回调函数
        function jsonp(url, params, fn){
			// cbName实现给url加上哈希,防止同一个地址请求出现缓存
            var cbName = `jsonp_${(Math.random() * Math.random()).toString().substr(2)}`;
            window[cbName] = function (data) {
                fn(data);
                // 获取数据后移除script标签
                window.document.body.removeChild(scriptElement);
            };

            // 组合最终请求的url地址
            var querystring = '';
            for (var key in params) {
                querystring += `${key}=${params[key]}&`;
            }
            // 告诉服务端我的回调叫什么
            querystring += `callback=${cbName}`;

            url = `${url}?${querystring}`;

            // 创建一个script标签,并将src设置为url地址
            var scriptElement = window.document.createElement('script');
            scriptElement.src = url;
            // appendChild(执行)
            window.document.body.appendChild(scriptElement);
        }

Animate

  • 很抱歉的是jQuery的动画源码我并没有阅读,但是我自己封装了一个动画函数,之后的源码阅读会补上的
  • 封装的代码
     // element设置动画的DOM对象
     // attrs设置动画的属性	object
     // fn是回调函数
     function animate(element, attrs, fn) {
        //清除定时器
        if(element.timerId) {
            clearInterval(element.timerId);
        }
        element.timerId = setInterval(function () {
            //设置开关
            var stop = true;
            //遍历attrs对象,获取所有属性
            for(var k in attrs) {
                //获取样式属性 对应的目标值
                var target = parseInt(attrs[k]);
                var current = 0;
                var step = 0;
                //判断是否是要修改透明度的属性
                if(k === "opacity") {
                    current = parseFloat( getStyle(element, k)) * 100 || 0;
                    target = target * 100;
                    step = (target - current) / 10;
                    step = step > 0 ? Math.ceil(step) : Math.floor(step);
                    current += step;
                    element.style[k] = current / 100;
                    //兼容ie
                    element.style["filter"] = "alpha(opacity="+  current +")";
                }else if(k === "zIndex") {
                    element.style[k] = target;
                } else {
                    //获取任意样式属性的值,如果转换数字失败,返回为0
                    current = parseInt(getStyle(element, k)) || 0;
                    step = (target - current) / 10;
                    console.log("current:" + current + "  step:" + step);
                    step = step > 0 ? Math.ceil(step) : Math.floor(step);
                    current += step;
                    //设置任意样式属性的值
                    element.style[k] = current + "px";
                }
                if(step !== 0) {
                    //如果有一个属性的值没有到达target  ,设置为false
                    stop = false;
                }

            }
            //如果所有属性值都到达target  停止定时器
            if(stop) {
                clearInterval(element.timerId);
                //动画执行完毕  回调函数
                if(fn) {
                    fn();
                }
            }
        },30);
    }

    //获取计算后的样式的值
    function getStyle(element, attr) {
        //能力检测
        if(window.getComputedStyle) {
            return window.getComputedStyle(element, null)[attr];
        }else{
            return element.currentStyle[attr];
        }
    }
811月/160

如何获取(GET)一杯咖啡——星巴克REST案例分析

发布在 邵珠庆

我们已习惯于在大型中间件平台(比如那些实现CORBA、Web服务协议栈和J2EE的平台)之上构建分布式系统了。在这篇文章里,我们将采取另一种做法:我们把支撑Web运行的协议和文档格式视为一种应用平台,一种可通过轻量级中间件访问的平台。我们通过一个简单的客户-服务交互的例子,展示了Web在应用集成中的作用。在这篇文章里,我们以Web为主要设计理念,提炼并分享了我们下本书《GET /connected - Web-based integration》(暂定名称)里的一些想法。

引言

我们知道,集成领域是不断变化的。Web的影响以及敏捷实践的潮流正在挑战我们的关于“良好的集成由什么构成”的观念。集成(integration)并不是一种夹在系统之间的专业活动;与此相反,现在,集成是成功方案里的不可缺少的一部分。

然而,仍有许多人误解并低估Web在企业计算中的作用。即便是那些精通Web的人士,也常常要花费很大力气才能懂得,Web不是关于支持XML over HTTP的中间件方案,也不是一种简易的RPC机制。这是相当遗憾的,因为Web不是仅能提供简单的点对点连接,它还有更大的用处;它实际上是一个健壮的集成平台。

 

在这篇文章里,我们将展示Web的一些值得关注的用途,我们将视之为一种可塑的、健壮的平台,它能够对企业系统做很“酷”的事。另外,工作流是企业软件最具代表性的特征。

为什么要工作流?

工作流(workflows)是企业计算的主要特征,它们基本上都是用中间件实现的(至少在计算方面)。工作流把一项工作(work)划分为多个离散的步骤(steps)以及触发步骤转移的事件(events)。工作流所实现的整个业务流程常常跨越若干企业信息系统,这给工作流带来很多集成问题。

星巴克:统一标准的咖啡需要统一标准的集成

Web若要成为可用于企业集成的技术,它就必须支持工作流——从而可靠地协调不同系统间的交互,以实现更大的业务能力。

 

要恰如其份地介绍工作流,就免不了讲述一大堆跟领域相关的技术细节,而这不是本文的主旨,因此,我们选择了Gregor Hohpe的星巴克工作流这个比较好理解的例子来举例说明基于Web的集成的工作原理。在这篇受到大家欢迎的博客文章里,Gregor讲述了星巴克是如何形成一个解耦合的(decoupled)盈利生产线的:

“跟大部分餐饮企业一样,星巴克也主要致力于将订单处理的吞吐量最大化。顾客订单越多,收入就越多。为此,他们采取了异步处理的办法。你在点单时,收银员取出一只咖啡杯,在上面作上记号表明你点的是什么,然后把这个杯子放到队列里去。这里的队列指的是在咖啡机前排成一列的咖啡杯。正是这个队列将收银员与咖啡师解耦开,从而,即便在咖啡师一时忙不过来的时候,收银员仍然可以为顾客点单。他们可以在繁忙时段安排多个咖啡师,就像竞争消费者模式(Competing Consumer)里那样。”

Gregor是采用EAI技术(如面向消息的中间件)来讲解星巴克案例的,而我们将采用Web资源(支持统一接口的可寻址实体)来讲解同一案例。实际上,我们将展示Web技术何以能够具有跟传统EAI工具一样的可靠性,以及何以不仅仅是请求/响应协议之上的XML消息传递!

首先,我们很抱歉擅自设想了星巴克的工作流程,因为我们的目的并不是精确无误地描述星巴克,而是用基于Web的服务来讲解工作流。好的,既然讲清楚了这一点,那么我们现在开始吧。

简明陈述

因为我们在讲工作流,所以我们有必要理解构成工作流的状态(states)以及将工作流从一个状态转移到另一个状态的事件(events)。我们的例子里有两个工作流,我们把它们用状态机(state machines)表达出来了。这两个工作流是并行执行的。一个反映了顾客与星巴克服务之间的交互(如图1),另一个刻画了由咖啡师执行的一系列动作(如图2)。

在顾客工作流里,顾客为了得到某种口味的咖啡而与星巴克服务进行交互。我们假定该工作流里包含以下动作:顾客点单,付款,然后等待饮品。在点单与付款之间,顾客通常可以修改菜单,比方说请求改用半脱脂牛奶。 /p>

图1 顾客状态机

尽管顾客看不见咖啡师,但咖啡师也有自己的状态机;这个状态机是服务实现私有的。如图2所示,咖啡师在周而复始地等待下一个订单,制作饮品,然后收取费用。当一个订单被加入到咖啡师的队列中时,一次循环实例就开始了。当咖啡师完成订单并把饮品交付给顾客时,工作流就结束了。

图2 咖啡师的状态机

尽管这些看似跟基于Web的集成毫不相干,但这两个状态机里的每一个状态迁移,都代表着与Web资源的一次交互。每一次迁移,就是通过URI对资源实施HTTP操作,从而导致状态的改变。

GET和HEAD属于特例,因为它们不引起状态迁移。它们的作用是用于查看资源的当前状态。

我们节奏稍快了点。理解状态机和Web,不是那么容易一口吃个胖子的。所以,让我们在Web的背景下,来从头回顾一下整个场景,逐步慢慢深入。

顾客视角

我们将从一张简单的故事卡片开始,它启动整个流程:

这个故事里涉及一些有用的角色与实体。首先,里面有“顾客(Customer)”角色。显然,它是(隐含的)星巴克服务(Starbucks Service)的消费者。其次,里面有两个重要的实体(“咖啡”和“订单”),以及一个重要的交互(“点单”)——我们的工作流正是由它启动的。

要把订单提交给星巴克,我们只要把订单的表示(representation)POST给下面这个众所周知的星巴克点单URI即可: http://starbucks.example.org/order

图3 点一杯咖啡

图3显示了向星巴克点单的交互过程。星巴克采用自己的XML格式来表达有关实体;需要关注的是,这个格式允许客户往里嵌入信息,以便进行点单——稍后我们会看到。实际提交的数据如图4所示。

在面向人类的Web(human Web)上,消费者和服务使用HTML作为表示格式(representation format)。HTML有自己特定的语义,所有浏览器都理解并接受这些语义,比如:代表“一个链接到其他文档或本文档内部某个书签的锚(anchor)”。消费者应用——浏览器——只是呈现HTML,状态机(也就是你!)用GETPOST跟随链接。对于基于Web的集成也一样,只不过服务和消费者不仅要就交互协议达成一致,还要就表示的格式与语义统一意见。

图4 POST饮品订单

星巴克服务创建一个订单资源,然后把这个新资源的位置放在HTTP报头Location里返回给消费者。为方便起见,服务还要把这个新创建的订单资源的表示(representation)也放在响应里。发给消费者的响应如下所示。

图5 创建好了订单,等待付款

201 Created状态表明星巴克已经成功接受了订单。Location报头给出了新创建订单的URI。响应主体里的表示(representation)包含了所点饮品及其价格。另外,这个表示里还包含另一个资源的URI——星巴克希望我们与这个URI交互,以完成顾客工作流;我们稍后将用到它。

注意,该URI是放在标签、而不是标签里的。这里的在顾客工作流里是具有特定含义的,其语义是事先定义好的。

我们已经知道201 Created状态代码表示“成功创建资源”的意思。对于这个例子以及一般的基于Web的集成,我们还需要其他一些有用的代码:
200 OK——它的意思是:一切正常;继续执行。
201 Created——我们刚刚创建了一个资源,一切正常。
202 Accepted——服务已经接受了我们的请求,并请我们对Location响应报头里的URI进行轮询(poll)。这在异步处理中相当有用。
303 See Other——我们需要跟另一个资源交互,应该不会出错。
400 Bad Request——我们的请求格式有问题,应重新格式化后再提交。
404 Not Found——服务因为偷懒(或者保密)没有告知请求失败的真实原因,但不管什么原因,我们都得应付它。
409 Conflict——服务器拒绝了我们更新资源状态的请求。我们需要获取资源的当前状态(要么检查响应实体主体,要么做一次GET操作),然后再作打算。
412 Precondition Failed——请求未被处理,因为Etag、If-Match或类似的“哨兵(guard)”报头的值不满足条件。我们需要考虑下一步怎么走。
417 Expectation Failed——幸亏核查一下,服务器不将接受你的请求,所以别真正发送那个请求。
500 Internal Server Error——最偷懒的响应。服务器出错了,而且什么原因都没说。祝你不要碰见它。

更新订单

星巴克很不错的一点就是,你可以按无数种不同的方式来定制自己的饮品。其实,考虑到某些高端客户极高的要求,也许让他们按化学公式来点单更好。但我们别那么贪心——至少开始的时候。我们来看另一张故事卡片:

回顾图4,显然我们在那里犯了一个错误:真正爱喝咖啡的人是不喜欢往浓咖啡里放太多热牛奶的。我们要改正那个问题。幸运地是,Web(或更确切地说,HTTP)以及我们的服务均为这样的改变提供了支持。

首先,我们要确认我们仍然可以修改订单。有时咖啡师动作很快,在我们想修改订单之前,他们就已经把咖啡做好了——于是,我们只有慢慢享用这杯热咖啡风味的牛奶了。不过,有时咖啡师会比较慢,这样我们就可以在订单得到咖啡师处理之前修改它了。为了知道我们是否还能修改订单,我们通过HTTP动词OPTIONS来向订单资源查询它接受哪些操作(如图6)。

请求 响应
OPTIONS /order/1234 HTTP 1.1 Host: starbucks.example.org 200 OK Allow: GET, PUT

图6 看看有哪些选择(OPTIONS)

从图6我们可以知道,订单资源既是可读的(支持GET)、也是可更新的(支持PUT)。作为好网民,我们可以拿我们的新表示来做一次试验性的PUT操作,在真正PUT之前先用Expect报头来试一试(如图7)。

请求 响应
PUT /order/1234 HTTP 1.1 Host: starbucks.example.com Expect: 100-Continue 100 Continue

图7 看好再做(Look before you leap)

若我们不能修改订单了,那么对图7所示请求的响应将是417 Expectation Failed。不过,假定我们现在得到的响应是100 Continue,也就是说,我们可以用PUT来更新订单资源(如图8)。用PUT方法来提交更新后的资源表示(representation),实际上就相当于修改现有资源。在这个例子中,PUT请求里的新描述包含一个元素,其中包含我们的更新,即外加一杯浓咖啡。

尽管部分更新(partial updates)属于REST社区里比较难懂的理念争论之一,但这里我们采取一种实用的做法,我们假定:增加一杯浓咖啡的请求,是在现有资源状态的上下文中被处理的。因此,我们没必要在网络上传送整个资源表示,我们只要传送变化的部分即可。

图8 更新资源状态

如果我们能够成功提交(PUT)更新,那么我们会从服务器得到响应代码200,如图9所示。

图9 成功更新资源状态

检查OPTIONS和采用Expect报头并不能令我们避免碰到“后续的修改请求失败”的情况。因此,我们并不强制使用它。作为好网民,我们会以某种方式来应付405409响应。

OPTIONS和Expect报头的使用应当被视为可选步骤。

尽管我们明智地使用ExpectOPTIONS,但有时PUT仍将失败;毕竟咖啡师也在一刻不停地工作——有时他们动作很敏捷!

若我们落后于咖啡师,我们在试图用PUT操作把更新提交给资源时会被告知。图10显示的就是一个常见的更新失败的响应。409 Conflict状态代码表明,若接受更新,将导致资源处于不一致的状态,所以没有进行更新。响应主体里显示出了我们试图PUT的表示(representation)与服务端资源状态之间的差异。按咖啡制作的话说,加得太晚了——咖啡师已经把热牛奶倒进去了。

图10 慢了一步

我们已经讲述了使用ExpectOPTIONS来尽量防止竞争条件。除此以外,我们还可以给我们的PUT请求加上If-Unmodified-SinceIf-Match报头,以表达我们对服务的期望条件。If-Unmodified-Since采用时间戳,而If-Match采用原始订单的ETag1 。若订单状态自从被我们创建以来还没有改变过——也就是说,咖啡师还没有开始制作我们的咖啡——那么更新可以处理。若订单状态已经发生改变,那么我们会得到412 Precondition Failed响应。虽然我们因为慢了咖啡师一步而只能享用牛奶咖啡,但至少我们没有把资源转移到不一致的状态。

用Web进行一致的状态更新可以采取很多种模式。HTTP PUT是幂等的(idempotent),这样我们在进行状态更新时就用不着处理一些复杂事务了,不过仍有一些选择需要我们决定。下面是正确进行状态更新的一些方法:

1. 通过发送OPTIONS请求,查询服务是否接受PUT操作。这一步是可选的。它可以告知客户端,此刻服务器允许对该资源做哪些操作,不过这无法保证服务器将永远支持那些操作。

2. 使用If-Unmodified-SinceIf-Match报头,以避免服务器执行不必要的PUT操作。假如PUT后来失败了,那么你会得到412 Precondition Failed。此方法要求:要么资源是缓慢更新的,要么支持ETag;对于前者就用If-Unmodified-Since,对于后者就用If-Match

3. 立即用PUT操作提交更新,并应付可能出现的409 Conflict响应。就算我们使用了(1)和(2),我们可能仍得应付这些响应,因为我们的“哨兵”和检查本质上都是乐观的。

关于检测和处理不一致的更新,W3C有一个非规范性文档,该文档推荐采用ETag。ETags也是我们推荐采用的方法。

在完成那些更新咖啡订单的艰苦工作之后,按理说我们应当得到额外那杯浓咖啡了。所以我们现在假定已设法得到了额外那杯浓咖啡。当然,我们要付过款后星巴克才会把咖啡递给我们(其实他们也已经暗示过了!),所以我们还需要一张故事卡片:

还记得最初那个针对原始订单的响应吗?其中有个元素。星巴克在订单资源的表示里面嵌入了有关另一个资源的信息。我们前面看过那个标签,但当时因为顾于修改订单就没有具体讲。现在我们应该进一步探讨它了:

关于next元素,有几点是值得指出的。首先,它处于一个不同的名称空间之下,因为状态迁移并不是只有星巴克需要。在这里,我们决定把这种用于状态迁移的URI放在一个公共的名称空间里,以便于重用(或甚至最终的标准化)。

其次,rel属性里嵌入了一则语义信息(你乐意的话,也可以称之为一种私有的微格式)。能够理解http://starbucks.example.org/payment这串文字的消费者,可以使用由uri属性标识的资源转移到工作流里的下一状态(付款)。

元素里的uri指向的是一个付款资源。根据type属性,我们已经知道预期的资源表示(representation)是XML格式的。我们可以向这个付款资源发送OPTIONS请求,看看它支持哪些HTTP操作。

微格式(microformat)是一种在现有文档里嵌入结构化、语义丰富的数据的方式。微格式在人类可读的Web上相当常见,它们用于往网页里增加结构化信息(如日程表)的 表示(representations)。不过,它们同样也可以方便地被用于集成。微格式术语是在微格式社区里达成一致的,不过我们也可以自由创建自己的 私有微格式,用于特定领域的语义标记。

尽管它们看上去没多大用,但如图10里那样的简单链接正是REST社区所呼吁的“将超媒体作为应用状态的引擎(hypermedia as the engine of application state)”的关键。更简单地说,URI代表了状态机里的状态迁移。正如我们在文章开始时所看到的,客户端是通过跟随链接的方式来操作应用程序的状态 机的。

如果你一时不能理解,不要感到奇怪。这一模型的最不可思议之处在于:状态机和工作流不是像WS-BPEL或WS-CDL那样事先描述好的,而是在你 经历各个状态的过程中逐步得到描述的。不过,一旦你的想明白了,你就会发现,跟随链接(following links)这种方式使得我们可以在应用的各种状态下向前推进。每次状态迁移时,当前资源的表示里都包含了指向可能的下一状态的链接以及它们所代表的状 态。另外,由于这些代表下一状态的资源是Web资源,所以我们知道如何使用它们。

在顾客工作流里,我们下一步要做的是为咖啡付款。我们可以由订单里的元素得知总金额,但在我们向星巴克付款之前,我们想向付款资源查询一下我们应当如何与之交互(如图11)。

消费者需要事先掌握多少关于一个服务的知识呢?我们已经说过了,服务和消费者在交互之前需要就它们将会交换的表示(representations)的语 义达成一致。可以将这些表示格式(representation formats)看成一组可能的状态和迁移。在消费者与服务交互时,服务选择可用的状态和迁移,并构造下一个表示。步向目标的过程是 动态发现的,而把这一过程中的各个部分串起来的方式是事先达成一致的。

在设计与开发过程中,消费者会就表示和迁移的语义与服务器达成一致。但谁也不能保证服务在其演化过程中会不会采用一种客户端预期之外的表示和迁移 (不过客户端还是知道如何处理它的)——那是Web松耦合的本质特性。尽管如此,在这些情况下就资源格式和表示达成一致超出了本文的范围。

我们下一步要做的是为咖啡付款。我们可以由订单表示的元素得知总金额,所以我们要做的就是付款给星巴克,然后咖啡师把饮品交给我们。首先,我们向付款资源查询我们应当如何与之交互(如图11)。

请求 响应
OPTIONS/payment/order/1234 HTTP 1.1 Host: starbucks.example.com Allow: GET, PUT

图11 获知如何付款

服务器返回的响应告诉我们,我们既可以读取付款(通过GET)、也可以更新它(通过PUT)。既然知道了金额,那么接下来,我们就把款项PUT给那个由付款链接标识的资源。当然,付款金额属于秘密信息,所以我们将通过认证2来保护该资源。

请求
PUT /payment/order/1234 HTTP 1.1
Host: starbucks.example.com
Content-Type: application/xml
Content-Length: ...
Authorization: Digest username="Jane Doe"
realm="starbucks.example.org“ 
nonce="..."
uri="payment/order/1234"
qop=auth
nc=00000001
cnonce="..."
reponse="..."
opaque="..."

   123456789
   07/07
   John Citizen
   4.00
响应
201 Created
Location: https://starbucks.example.com/payment/order/1234
Content-Type: application/xml
Content-Length: ...

   123456789
   07/07
   John Citizen
   4.00

图12 付款

为成功完成付款,我们只需按图12进行交互即可。一旦经认证的PUT返回一个201 Created响应,我们就可以庆祝付款成功、并拿到我们的饮品了。

不过事情也有出错的时候。当资金处于危险状态时,我们希望要么没出错、要么可以挽救错误3。付款时可能出现很多种容易想象的出错情况:

  • 由于服务器宕机或其他原因,我们无法连接上服务器了;
  • 在交互过程中,与服务器的连接被切断了;
  • 服务器返回一个4xx5xx范围的错误状态。

幸运地是,Web可以帮助我们应付以上这些情况。对前两种情况(假定连接问题是瞬间的),我们可以反复做PUT请求,直至我们收到成功响应为止。如果前次PUT操作已经得到了成功处理,那么我们将收到一个200响应(本质上是一个来自服务器的空操作确认);如果本次PUT操作成功完成了付款,那么我们将收到一个201响应。在第三种情况中,如果服务器返回的响应代码是500503504,那么也可以做同样处理。

4xx范围的状态代码比较难处理,不过它们仍然指出了下一步怎么办。例如,400响应表明我们通过PUT请求提交的内容无法被服务器所理解,我们需要纠正后重新发送PUT请求。403响应则相反,它表明服务器能够理解我们的请求,但不知道如何履行(fulfil)它,而且服务器希望我们不要重试。对于这些情况,我们得在响应的有效负载(payload)里寻找其他的状态迁移(链接),换其他推进状态的路线。

在这个例子中,我们已经多次使用状态代码来指引客户端步向下一个交互了。状态代码是具有丰富语义的确认信息。让服务返回有意义状态代码,并且令客户 端懂得如何处理状态代码,这样一来,我们便给HTTP简单的请求响应机制增加了一层协调协议,从而提高了分布式系统的健壮性和可靠性。

一旦我们为自己的饮品买了单,我们这个工作流就算完成了,有关顾客的故事也就到此结束了。不过整个故事还没有完。现在我们进入到服务里面,看看星巴克的内部实现。

咖啡师视角

作为顾客,我们乐于把自己放在咖啡世界的中央,不过我们并不是咖啡服务的唯一消费者。从与咖啡师的“时间竞赛”中我们已经得知,咖啡服务还为包括咖啡师在内的其他一些相关方面提供服务。按照我们循序渐进的介绍方式,现在该推出另一张故事卡片了。

用Web的格式与协议来描述饮品列表是件很容易的事。用Atom提要(feeds)来表达列表之类的东西是相当不错的选择,它几乎可描述任何列表(比如未完成的咖啡订单),所以这里我们可以也采用它。咖啡师可以通过向该Atom提要的URI发送GET请求来访问它,对于未完成的订单,URI是http://starbucks.example.org/orders(如图13)。

图13 待制作饮品的Atom提要

星巴克是家相当繁忙的店,位于/orders的Atom提要更新相当频繁,所以咖啡师要不断轮询这个提要才能保证掌握最新信息。轮询通常被认为可伸缩性很差;但是,Web支持可伸缩性极强的轮询机制——我们稍后会看到。另外,由于星巴克每分钟要制作很多咖啡,所以承受住负荷是个重要问题。

这里我们有两个相抵触的需求。一方面,我们希望咖啡师通过经常轮询订单提要,以不断掌握最新信息;另一方面,我们又不希望给服务增添负担、或者徒然增加网 络流量。为防止我们的服务因过载而崩溃,我们将在我们服务之外,用一个逆向代理(reverse proxy)来缓存并提供被频繁访问的资源表示(如图14所示)。

图14 通过缓存提升可伸缩性

对于大多数资源(尤其是那些会被很多人访问的资源,如返回饮品列表的Atom提要),在宿主服务之外缓存它们是合理的。这样可以降低服务器负 载,提升可伸缩性。我们在架构里增设了Web缓存(逆向代理),再加上有缓存元数据,这样客户端获取资源时就不会给原服务器增添很大负担了。

缓存的有利一面是,它屏蔽掉了服务器的间隙性故障,并通过提高资源可用率来帮助灾难恢复。也就是说,即便星巴克服务出现了故障,咖啡师仍然可以继续工 作,因为订单信息是被代理缓存起来的。而且,假如咖啡师漏了某个订单的话(错误),恢复也很容易进行,因为订单具有很高的可用率。

是的,缓存可以把旧订单多保留一段时间,但对于像星巴克这样吞吐量很高的商户而言,这是不太理想的。为了把太旧的订单从缓存中清除,星巴克服务用

Expires

报头来声明一个响应可以被缓存多久。任何介于消费者与服务之间的缓存都应当服从这一指示,拒绝提供过期订单4,而是把请求转发到星巴克服务上,以获取最新的订单信息。

图13所示的响应对Atom提要的Expires报头进行了相应的设置,令饮品列表在10秒钟后过期。由于这 种缓存行为,服务器每分钟最多只要响应6次请求,其余请求将由缓存机制代劳。即便对于性能比较糟糕的服务,每分钟6个请求也属于容易处理的工作量了。在最 愉快的情况下(对星巴克服务来说),咖啡师的轮询请求是由本地缓存响应的,这样就不会给增加网络活动或服务器负荷了。

在我们的例子中,我们只设置了一个缓存来帮助提升主咖啡列表的可伸缩性。然而,在真实的基于Web的场景中,我们可以从多层缓存中受益。要在大规模环境中提升可伸缩性,利用现有Web缓存的优点是至关重要的。

Web以延迟换取了高度的可伸缩性。假如你的问题对延迟很敏感的话(比如外汇交易),那么就不太适合采用基于Web的方案了。但是,假如你可以接受“秒”数量级上的延迟,那么Web也许是个不错的平台。

既然我们已经成功解决了可伸缩性问题,那么我们继续来实现更多的功能。当咖啡师开始为你制作咖啡时,应当修改订单状态,以达到禁止更新的目的。从顾客的角度来看,这相当于我们无法再对我们的订单执行PUT操作了(如图6、7、8、9、10所示)。

幸运地是,我们可以利用一个已经定义好的协议——Atom发布协议(Atom Publishing Protocol,简称APP或AtomPub)——来实现这一目标。AtomPub是一个以Web中心(基于URI)的协议,用于管理Atom提要里的 条目(entries)。我们来仔细看看Atom提要(/orders)里代表咖啡的条目。

图15 咖啡订单对应的Atom条目

在图15所示的XML里,有几点值得注意。首先,它将我们的订单与Atom提要里的其他订单区分开了。其次,其中包含订单本身,即咖啡师制作咖啡所需的全部信息——包括我们要求增加一杯浓咖啡的重要信息!该订单对应的entry元素里有个link元素,它声明了本条目(entry)的编辑URI(edit)。这个编辑URI指向的是一个可以通过HTTP编辑的订单资源。(这里,可编辑资源的地址刚好跟订单资源本身的地址一样,不过这不是必须的。)

如果咖啡师要锁定订单资源、禁止它被修改,就可以通过该编辑URI来改变订单资源的状态。具体地讲,咖啡师可以用PUT请求把经修改的资源状态提交给这个编辑URI(如图16所示)。

图16 通过AtomPub设置订单状态

服务器一旦处理了如图16所示的PUT请求,它就会拒绝对位于/orders/1234的订单资源做除GET以外的操作。

现在订单处于稳定状态了,咖啡师可以毫无顾虑地继续制作咖啡了。当然,咖啡师只有知道我们已经付过款才会把咖啡给我们,所以咖啡师还要查询我们是否已经完 成付款。在真实的星巴克里,情况会略有不同:一般来说,我们是点单后立即付款的;然后,其他顾客站在周围,以免你拿走别人点的饮品。但在我们计算机化的版 本里,增加这一检查并不麻烦,所以我们来看倒数第二张故事卡片:

咖啡师只要向付款资源(该资源的URI在订单表示里给出了)发送GET请求,即可查询付款状态。

这里,顾客和咖啡师是通过订单表示里给出的链接得知付款资源的URI的。但有时,通过URI模版来访问资源也很方便。

URI模版(URI template)是一种描述知名URI的格式。它允许消费者通过修改URI里的部分字符来访问不同的资源。

Amazon的S3存储服务就是基于URI模版的。用户可以对由以下模版生成的URIs进行HTTP操作,从而对已保存的制品进行操作:http://s3.amazonaws.com/{bucket_name}/{key_name}

为方便咖啡师(或其他经授权的星巴克系统)不用遍历所有订单即可访问各个付款资源,我们可以在我们的模型里设计一个类似的URL模版方案:http://starbucks.example.org/payment/order/{order_id}

URI模版就像与消费者订立的契约,服务提供者须在服务演化过程中注意维持它们的稳定。由于这一潜在的耦合,有些Web集成工作者会有意避免采用URI模版。我们的建议是,仅当可推断的URIs(inferable URIs)很有帮助而且不会改变时才使用。

对于我们的例子,另一种办法是在/payments处暴露一个提要,用它提供包含指向各个付款资源的(不可推断的)链接。该提要只有经授权的系统才能读取。

最终,URI模版是不是一个相对超媒体来说安全而有效的捷径,要由服务设计者来决定。我们的建议是:要保守地使用URI模版!

当然,不是人人都可以查看付款信息的。我们不想让咖啡社区里会动歪脑筋的人查看他人的信用卡详细信息,因此,跟其他敏感的Web系统一样,我们利用请求认证来保护敏感资源。

如有未认证的用户或系统试图获取一个具体的付款信息,那么服务器会质询(challenge)它、要求它提供证书。(如图17)

请求 响应
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org 401 Unauthorized WWW-Authenticate: Digest realm="starbucks.example.org", qop="auth", nonce="ab656...", opaque="b6a9..."

图17 对付款资源的非授权访问受到质询

401状态(及其认证元数据)告诉我们,我们应当在请求里附上正确的证书、然后重新发送请求。重新用正确的证书发送请求(图18)后,我们得到了付款信息,并将之与代表订单总金额的资源http://starbucks.example.org/total/order/1234进行比较。

请求 响应
GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org Authorization: Digest username="barista joe" realm="starbucks.example.org“ nonce="..." uri="payment/order/1234" qop=auth nc=00000001 cnonce="..." reponse="..." opaque="..." 200 OK
Content-Type: application/xml
Content-Length: ...    123456789
   07/07
   John Citizen
   4.00

图18 授权访问付款资源

一旦咖啡师制作好、交出咖啡并完成收款,他们就要在待处理饮品列表中删除相应的订单。如同前面一样,我们采用一个故事来讲解这个回合:

因为订单提要里的各个条目(entry)都标识着一个可编辑资源,而且有自己的URI,所以我们可以对各个订单资源做HTTP操作。如图19所示,咖啡师只要对相关条目(entry)所引用的资源做DELETE操作即可将它从列表中删除。

请求 响应
DELETE /order/1234 HTTP 1.1 Host: starbucks.example.org 200 OK

图19 删除已完成的订单

在条目被删除(DELETE)之后,再对订单提要做GET操作的话,返回的表示里将不再包含已删除(DELETE)的资源。假定我们的缓存工作正常、且我们已经设置了合理的缓存过期元数据的话,那么当你试图获取(GET)那个订单条目时将直接得到404 Not Found响应。

也许你已经注意到了,Atom发布协议可以满足我们对星巴克这个问题的大部分需求。如果我们想直接把位于/orders的Atom提要暴露给顾客的话,顾客就可以用Atom发布协议来向该提要发布饮品订单、甚至修改订单了。

演化:Web上的现实情况

因为我们的咖啡店是基于自描述的状态机(state machines)构建起来的,所以我们可以方便地根据业务需要改造我们的工作流。例如,星巴克也许会提供一种免费的网上促销活动:

    • 7月——我们的星巴克店开业,并提供标准的工作流以及我们前面提到的状态迁移和表示(representation)。消费者知道用这些格式与表示跟我们的服务进行交互。
    • 8月——星巴克新推出了一种免费网上促销的表示(representation)。我们的咖啡工作流将进行更新,以包含指向该网上促销资源的链接。由于URI的特性,链接可以是指向第三方的——这跟指向星巴克内部的资源一样简单。

      因为表示里仍然包含原来的迁移点,所以现有消费者仍然可以实现它们的目标,只不过它们可能无法享受促销而已,因为这部分还没有写进它们的代码里去。
  • 9月——消费者应用和服务都进行了有关升级,以便能够理解并使用免费的网上促销。

成功进行演化的关键在于,服务的消费者们要能够预料到改变。在每一步,服务不是直接跟资源绑定(例如通过URI模版),而是提供指向具名资源(named resources)的URIs,以便消费者与之交互。这些具名资源,有些是消费者不认识的、将被忽略的,有些是消费者已知的、想采用的状态迁移点。不管 采用哪种方式,这种方案使得服务可以优雅地演化,同时还能维持与消费者兼容。

你将使用的是一个相当热门的技术

交付咖啡是我们工作流的最后一步。我们已经点了单、修改了订单(也可能无法修改)、付过款并最终拿到了我们的咖啡。在柜台另一侧,星巴克也已经同样完成了收款和订单处理。

我们可以用Web来描述所有必需的交互。我们可以利用现有的Web模型处理一些简单的不愉快的事(例如无法修改处理中或已处理完毕的订单),而不必 自己发明新的异常或错误处理机制——我们所需的一切都是HTTP现成提供的。而且,即便发生了那些不愉快的事,客户端仍然可以向它们的目标迈进。

HTTP提供的特性起初看来是无关紧要的。但这个协议现在已经取得广泛的一致、并得到广泛的部署了,而且所有的软件与硬件都能一定程度上理解它。当 我们看到其他分布式计算技术(如WS-*)处于割据状态的格局时,我们意识到了HTTP享有的巨大成功,以及它在系统间集成方面的潜力。

甚至在非功能性方面,Web也是有益的。在我们碰到临时故障时,HTTP操作(GETPUTDELETE)的幂等性质令我们可以进行安全的重试;内在的缓存机制既屏蔽了故障,又有助于灾难恢复(通过增强的可用率);HTTPS和HTTP认证有助于基本的安全需求。

尽管我们的问题域是人为制造的,但我们所强调的技术同样可以应用于分布式计算环境。我们不会伪称Web很简单(除非你是天才),Web可以解决一切问题 (除非你是超级乐观的人,或受到REST信仰的感染),但事实上,在局部、企业级和Internet级进行系统集成,Web是个健壮的框架。

致谢

本文作者要向英国卡迪夫大学(Cardiff University)的Andrew Harrison表示感谢,是他启发了我们就Web上的“对话描述”进行讨论。

About the Authors

Jim Webber博士是ThoughtWorks公司的专业服务主管,他的工作是为全球客户进行可靠的分布式系统架构设计。此 前,Jim担任英国E-Science计划高级研究员,从事将Web服务实践及可靠面向服务计算的架构模式应用于网格计算的战略设计工作,他在Web及 Web服务架构与 开发方面具有广泛的经验。Jim还担任过惠普公司和Arjuna公司的架构师,他是业界首个Web服务事务方案的首席开发者。Jim是一位活跃的演说家, 他经常受邀出席 国际会议并发言。他还是一位活跃的作家,除了《Developing Enterprise Web Services - An Architect's Guide》这本书外,目前他正在撰写一本关于基于Web的集成的新书。Jim获得英国纽卡斯尔大学(University of Newcastle)的计算机科学学士学位和并行计算博士学位。他的博客地址是:http://jim.webber.name

Savas Parastatidis是一位软件思想家,他的思考领域涉及系统和软件。他研究技术在eResearch里的运用,他尤其对云计算、知识表示与管理、社会网络感兴趣。他目前任职于微软研究院合作研究部。Savas喜欢在http://savas.parastatidis.name上写博客。

Ian Robinson帮助客户们创建可持续的面向服务的能力,令业务与IT从开始到实际运营始终保持齐合。他为微软公司写过关于采用微软技术实现面向服务系统的指南,还发表过文章讲述消费者驱动的服务契约及其在软件开发生命周期中的作用——该文章可以在《ThoughtWorks文集(The ThoughtWorks Anthology)》(Pragmatic Programmers,2008)及InfoQ中文站上找到。他经常在会议上做有关REST式企业开发及面向服务交付的测试驱动基础的讲演。


1 ETag(Entity Tag的简写)是资源状态的唯一标识符。一个资源的ETag通常是根据该资源的数据得到的MD5校验和或SHA1哈希值。

2 我们将从稍后的星巴克例子中了解认证的工作原理。

3 当然,如果安全性遭到威胁,我们只要防止事情不要错得更厉害就行了!但得到咖啡并不是一项攸关安全的任务,尽管每天早晨我的同事们可能会这么认为!

4 HTTP 1.1提供了一些有用的请求指令,比如max-agemax-stalemax-fresh,它们允许客户端指出愿意接受缓存里多旧的数据。

查看英文原文:How to GET a Cup of Coffee

271月/150

Code Review 同行代码审查实战分析

发布在 邵珠庆

代码审查(Code Review)是软件开发中常用的手段,和QA测试相比,它更容易发现较难发现的问题,还可以帮助团队成员提高编程技能,统一编程风格等。本文作者从实际出发,详细分析了开发者在代码审查过程中会遇到的问题及解决方法。

以下为译文:

数百万年前,人类祖先人猿学会直立行走——解放双手——最终进化到人;而代码审查在开发过程中有着异曲同工之妙——区别出野蛮开发和先进开发。

然而,在实际工作中,以下声音总不绝于耳:

  1. “代码审查在项目中简直就是浪费时间!”
  2. “我根本没有时间去做复查。”
  3. “由于我那个谨慎的拍档还没做好复查,进度只能延后了。”
  4. “你确定,我的同事要求我修改代码吗?请向他们解释一下,任何轻微地改动都会让我的代码失去原有的优雅。”

那么问题来了,我们为什么需要做代码复查?

作为专业的软件开发人员,持续提高代码质量是工作生涯不断追求的目标之一。无论我们有多么优秀,都离不开团队;而代码复查是个人与团队的润滑剂:

  • 当局者迷,旁观者清。代码复查如同为我们安装了后视镜;
  • 使我们的代码多了至少一个知音人;
  • 能帮助新员工在这个过程中学习和领悟到前辈的代码精髓;
  • 有助开展知识共享,众“智”成城。

要做就要做到最好

如果想要达成上述种种美好结果,是离不开时间和工作的科学安排的。仅仅做好代码缩进、变量命名规范等基本工作,还不能算得上完美。而如果曾经尝试过结对编程,或许你会发现其所花的时间往往都比代码复查要多。

我的建议是用开长总时长的四分之一时间来进行代码复查。举例来说,如果开发总用时为两天,那么应该花上大约四个小时来进行复查。

当然,如果能把事情做对,可不必拘泥于时间的多少。进一步说,我们必须能看明白将要复查的代码。这不仅代表要掌握基本的语言语法,更关键的是要掌握整个代码的架构,所用组件或库等细节。如果不能做到对每一行代码所做的事情都了然于胸,这样的复查工作是没有多少价值的。所以要做好这点是不能一味讲求速度的,必须花一番功夫来从头到尾对代码进行梳理分析。

此外还有两件事是务必要做到的:

  1. 复查工作中包含所有必须的测试工作;
  2. 做好设计文档的编写工作。

避免拖延症

今天的工作今天完成是最完美的工作状态,否则一旦拖延症出现,再多再好的复查都只会成为开发过程中的绊脚石。好的复查需要紧密而持久的努力,不是搞搞突击就能做好的。

因此,开发者应当尽力做到日清日结。把复查作为每天工作的开端是个不错的主意;理清旧的思路将有助于开展新的编码任务。也或许有的人喜欢在午休或下班前进行,无论在什么时候进行,以下几点是应该避免的:

  • 让积压工作越积越多;
  • 由于复查没有做好而导致进度延后;
  • 由于代码更新频率快就放弃做复查;
  • 往往在最后一刻才去做复查。

编写出可复查的代码

不应该把所有复查工作都推给复查员。如果我的同事花了一周时间添加了看起来比较乱的代码,这对复查工作无疑是重大打击,也很难让人摸清其思路和结构。

所以我们在编程时,要有意识地把代码划分为可操作单元。我们使用的方法是scrum,它为我们的开发工作做了很明晰的指导,同时使得整个开发过程有迹可循,便于进行追溯和回顾。

其次,在与复查员进行讨论前要搞好关系。这样将有助于双方对彼此有所了解,从而减少讨论时矛盾发生的机率。

再者,项目结构应当在设计文档中描述得清楚具体。这对于项目新成员的成长是大有裨益的,同时能帮助复查员提高工作效率。

最后也是最重要的一点是在自我复查过程中做好注释。换言之要先自行对代码过一遍,把需要做出说明的地方标示出来并解释清楚。有研究表明,开发者在对自己的进行复查和注释时,经常会找出不少瑕疵。

大型代码重构

有时候如果需要进行代码重构,这势必会影响到很多组件,特别是较大型的应用。在这种情况下,最好的解决方案是逐步推进重构工作。先对要做的变更进行划分,然后根据修改意图进行分段式重构。当这部分变更完成并做好复查后,再执行第二部分的重构,重复该步骤直至完成全部工作。这或许增加了重构用时,但会带来更高质量的代码同时可以减轻复查员的工作量。

如果实际情况真的不允许进行逐步重构,可以试试结对编程。

解决矛盾

在一个技术团队中,各人有各自的观点,如何达成共识是成败的关键。作为开发者,应该保持开明的心态并虚心接受不同的意见。避免固步自封,避免对自我复查工作的不屑一顾。如果有人提议把我们一些重复的代码做成一个可复用函数,这并不代表我们之前的工作是毫无价值的。

而作为复查员,要懂得人情世故。在给出修改意见前,先考虑清楚这真的会更好抑或仅仅是风格上的不同看法。提议说法可以是:“如果尝试另一种方法,或许会更好”或“有同事建议这样做”,而要避免的是:“就连我家宠物都能写出比这好的算法!”

如果真的一时僵持不下,争议双方不妨请教第三个开发人员,从他的角度来再次审度各自的观点,直到形成共识,三人行,必有我师焉。

67月/130

天猫淘宝公开课:卖家数据分析全攻略

发布在 邵珠庆

几乎每个淘宝运营一提到数据营销,基本上都能说上来这些软件:量子恒道、淘宝指数、数据魔方。但是谈及具体应用方面,到底要关注哪些指标,有了这些指标后要怎么优化,自己的店铺正处于什么阶段,有什么问题,接下来要怎么改变……很多卖家就犯难了。

古有云,知己知彼,百战不殆。数据分析其实也就是在做这两件事。知己就是要对自己的店铺状况了如指掌,而知彼则要对大环境和直接竞争对手有清晰的认识。

【知己篇】店内数据分析

店内运营如果简单来看,就是流量和转化这两件事。所以分析也主要从这两个指标出发。因为流量结构和精度直接影响转化,转化好坏再反过来影响流量,所以在分析时,先做流量盘查,再做转化分析。

一、 流量

1、 搜索流量

工具:搜索诊断助手、直通车

a基础条件:不违规,可在“卖家工作台”-“搜索诊断助手”-“宝贝诊断里”检查。

b相关性:类目属性相关性、标题关键字相关性。

C人气分:是否橱窗推荐、是否加入消保、DSR评分、支付宝使用率、旺旺效应速度、拍货与发货的时差。

D图片:很多卖家在优化主搜流量时,经常会忽略图片的优化,然而图片点击率的差距,直接影响了最后的搜索流量。买家不是直接搜索进来的,而是被图片吸引进来的,优化图片就显得非常重要。建议可以用直通车来测试图片(方法下文会介绍到)。

E价格与销量:销量相当的产品,价格高的有更多展示的机会;价格相同的产品,销量高的有更多展示机会。而检查该项指标主要检查自己与直接竞争对手的差距,尤其是7天销量的差距,以做调整。

F标题优化:在销量相对低的时候多使用长尾词,销量高的时候多使用泛词、中心词,并反复测试,得出搜索流量×搜索转化率的最大值。

2、 付费流量

工具:各付费工具的数据报表、量子。

a直通车:诊断直通车主要看点击率和转化率这两个指标。

点击率直接影响淘宝直通车的收入,在行业利润如此透明的年代,每家的出价其实都不可能差很多,而点击率越高,直通车本身的收入就越高,就会提供更好的位置给你。

查看工具:行业解析报表。

优化办法:挖掘USP。

转化率则是淘宝看重的用户体验,直通车转化率要做到约等于或略低于该宝贝整体转化率才算比较健康。

查看工具:直通车转化报表、量子-销售分析-宝贝销售排行;

优化办法:在销量较低的时候重点优化长尾词,销量高时优化泛词和定向。

b钻石展位:诊断钻展其实也和直通车原理基本一致。也是优化图片,然后选择精准的店铺来定向。

查看工具:钻展广告位对应类目数据、钻展定向报表-手工统计各项回报率;

优化办法:总结同行优秀素材的构成因素和失败素材的特点、把收集店铺ID的维度做细。

C淘客:淘客诊断只要看自己与竞争对手的销量和佣金有何差距即可。

二、 内功

1、 转化率

工具:量子、数据魔方

a、 内页:首先看销量,其次看评价质量,再来看单品转化率、页面停留时间和询单率。如果连基础销量都没有,评价很差,转化率是不可能好的。两个先决条件解决了,再看单品转化率、页面停留时间和询单率是否不低于行业均值(或店内卖的好的宝贝)。若低于,则一一优化USP卖点、逻辑顺序(是否都做到围绕USP)、展现内容多样化(数据、图表、细节图、权威认证报告、大量实证、视频等)、展现方式(字体、字号、背景色、配色)。

b、 访问深度:由于80%的顾客入店都是从内页进来,所以主要优化内页可导流的位置,分别为店招、宝贝页关联、宝贝页侧边栏、店尾进行优化。再优化首页。

查看工具:量子-销售分析-销售总览-平均访问深度、量子-流量分析-宝贝被访排行、量子-流量分析-首页被访数据(停留时间、点击率、跳失率)、量子-店铺装修。

优化办法:将店内20%的产品用导航、促销、关联等的方式做集中展示。

c、 支付率:是否做到了80%以上。

查看工具:量子-销售分析-销售总览。

优化办法:利用短电旺给顾客一个必须现在下单的理由。

d、 营销活动:定期举办营销活动可提升转化率。

e、 客服询单转化率:是否至少做到了行业均值。

查看工具:如赤兔等第三方工具。

优化方法:顾客的每一个问题都建立标准答案。

2、 客单价

工具:量子-销售分析-销售总览。

优化办法:包邮条件、满减满赠、爆款关联、客服推荐、SKU扩充、促销产品等。

3、 DSR

工具:淘宝DSR评分计算器。

优化办法:a、淘宝原有服务的升级(7天无理由升级为30天、3天发货升级为24小时发货等);b、淘宝未有服务的创新(围绕客户与商家接触点的创新,如SNS、游戏)。

4、 CRM

CRM主要查看老客户占比、老客户转化率、二次购买率、客户分组短彩邮的ROI。

工具:卖家工作台-会员关系管理、数云、客道等第三方软件。

优化的办法:建立老客户分组,根据分组创建老客户的不同特权。越高级的客户拥有越高级的特权。

【知彼篇】竞争数据分析

竞争数据主要包括整个市场数据和直接竞争对手数据。市场数据的获取能够判定市场容量、市场竞争的激烈程度,避免新产品在还未上市时就输了。直接竞争对手的数据能则能取长补短。

一、市场数据

1、市场容量

用数据魔方-行业分析-整体情况可以看到整个行业、子类目的市场容量。

2、 搜索指数

用淘宝指数查看搜索指数和成交指数

3、 细分数据

工具:淘宝指数和数据魔方

a、 买家购买分析

用数据魔方-行业分析看到买家的购买分析,可查看购买的单价和客单价及购买的频次。

b、 买家信息分析

用数据魔方-行业分析看到买家的信息分析,可查看买家的来访高峰时段、购买高峰时段、买家地域、性别分布和年龄。

c、 属性热销

用数据魔方-属性分析-属性热销排行,查看买家需求的产品属性。

4、 趋势

工具:数据魔方

a、 市场趋势

用数据魔方-行业分析-整体情况拉出最近3年、1年、半年和一季度的走势。

b、 行业关键词热搜飙升榜(数据魔方-淘词-行业热词榜-行业关键词热搜飙升榜)

5、 竞争度

用数据魔方-行业分析-卖家分析,查看卖家的数量,值越大,竞争越激励。再用核心关键词在淘宝上搜索,按销量排序,看前3名,第3和第40名的销售笔数的差额,值越低,竞争越激烈。

6、 空白市场

需要细心的留意,并对整个行业的动态有清晰的掌握。

二、 竞争对手数据

竞争对手的数据建议是每周都收集,然后和自己店内数据进行对比。

1、 销售额

用数据魔方-品牌分析-品牌详情来查询

2、 转化率

用数据魔方-品牌分析-品牌详情来查询

3、 客单价

用数据魔方-品牌分析-品牌详情来查询

4、 营销活动

定期收集竞争对手的营销活动,包括活动商品品类、商品价格、活动政策、互动平台和媒介投放等。

5、 爆款

定期收集竞争对手的主要爆款,包括产品名、价格、转化率、销量、单品活动、单品卖点、关联、流量来源、直通车卡位、直通车图片、钻展图片、老客户优惠等。查看工具:数据魔方-品牌分析-品牌详情-热销宝贝排行、手工收集。

6、特色

对竞争对手的赠品、包裹、优惠券、客户互动等相关信息进行收集。

数据分析说到底就是和自己的过去比,和竞争对手的现在比,和我们期望的未来比。找出现在店铺存在的问题,再思考可以用什么方法来改善它,这样的数据分析才有意义。

272月/130

CI框架源码完全分析之核心文件Codeigniter.php

发布在 邵珠庆

$assign_to_config['subclass_prefix']));
}

/*
*php 程序运行默认是30s,这里用set_time_limt延长了,关于set_time_Limit() http://www.phpddt.com/php/set_time_limit.html
* 扩展阅读,关于safe_mode:http://www.phpddt.com/php/643.html ,你会完全明白的
*/
if (function_exists("set_time_limit") == TRUE AND @ini_get("safe_mode") == 0)
{
@set_time_limit(300);
}

/*
* 加载Benchmark,它很简单,就是计算任意两点之间程序的运行时间
*/
$BM =& load_class('Benchmark', 'core');
$BM->mark('total_execution_time_start');
$BM->mark('loading_time:_base_classes_start');

//加载钩子,后期会分析到,这玩意特好,扩展它能改变CI的运行流程
$EXT =& load_class('Hooks', 'core');

//这里就是一个钩子啦,其实就是该钩子程序在这里执行
$EXT->_call_hook('pre_system');

//加载配置文件,这里面都是一些加载或获取配置信息的函数
$CFG =& load_class('Config', 'core');

// 如果在index.php中也有配置$assign_to_config,则也把它加入到$CFG
if (isset($assign_to_config))
{
$CFG->_assign_to_config($assign_to_config);
}

//加载utf8组件、URI组件、Router组件
$UNI =& load_class('Utf8', 'core');
$URI =& load_class('URI', 'core');
$RTR =& load_class('Router', 'core');
$RTR->_set_routing();

//如果在index.php中定义了$routing,那么就会覆盖上面路由
if (isset($routing))
{
$RTR->_set_overrides($routing);
}

//加载output输出组件,不然你怎么用$this->Load->view()啊
$OUT =& load_class('Output', 'core');

//又见钩子,这里你可以自己写钩子程序替代Output类的缓存输出
if ($EXT->_call_hook('cache_override') === FALSE)
{
if ($OUT->_display_cache($CFG, $URI) == TRUE)
{
exit;
}
}

//安全组件啦,防xss攻击啊,csrf攻击啊
//关于xss攻击:http://www.phpddt.com/php/php-prevent-xss.html
//关于csrf:攻击:http://www.phpddt.com/reprint/csrf.html
$SEC =& load_class('Security', 'core');

//加载输入组件,就是你常用的$this->input->post();等
$IN =& load_class('Input', 'core');

//加载语言组件啦
$LANG =& load_class('Lang', 'core');

//引入CI的控制器父类
require BASEPATH.'core/Controller.php';

function &get_instance()
{
return CI_Controller::get_instance();
}

//当然你扩展了CI_Controller控制器的话,也要引入啦
if (file_exists(APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php'))
{
require APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php';
}

//加载你自己应用中的控制器Controller,如果没有当然error啦
if ( ! file_exists(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().'.php'))
{
show_error('Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.');
}
include(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().'.php');

// 好的基础的类都加载完毕了,咱可以mark一下
$BM->mark('loading_time:_base_classes_end');

//路由获取了控制器名和方法名,比如说默认welcome/index
$class = $RTR->fetch_class();
$method = $RTR->fetch_method();
//这里CI规定一般非公共的方法以_开头,下面是判断,如果URI不可访问就show_404()
if ( ! class_exists($class)
OR strncmp($method, '_', 1) == 0
OR in_array(strtolower($method), array_map('strtolower', get_class_methods('CI_Controller')))
)
{
if ( ! empty($RTR->routes['404_override']))
{
$x = explode('/', $RTR->routes['404_override']);
$class = $x[0];
$method = (isset($x[1]) ? $x[1] : 'index');
if ( ! class_exists($class))
{
if ( ! file_exists(APPPATH.'controllers/'.$class.'.php'))
{
show_404("{$class}/{$method}");
}

include_once(APPPATH.'controllers/'.$class.'.php');
}
}
else
{
show_404("{$class}/{$method}");
}
}

//又是钩子,该钩子发生在控制器实例化之前的
$EXT->_call_hook('pre_controller');

//又mark一个点
$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_start');
//终于实例化控制器了
$CI = new $class();

//钩子,不想多说了
$EXT->_call_hook('post_controller_constructor');

/*
* ------------------------------------------------------
* Call the requested method
* ------------------------------------------------------
*/
// Is there a "remap" function? If so, we call it instead
if (method_exists($CI, '_remap'))
{
$CI->_remap($method, array_slice($URI->rsegments, 2));
}
else
{
// is_callable() returns TRUE on some versions of PHP 5 for private and protected
// methods, so we'll use this workaround for consistent behavior
if ( ! in_array(strtolower($method), array_map('strtolower', get_class_methods($CI))))
{
// Check and see if we are using a 404 override and use it.
if ( ! empty($RTR->routes['404_override']))
{
$x = explode('/', $RTR->routes['404_override']);
$class = $x[0];
$method = (isset($x[1]) ? $x[1] : 'index');
if ( ! class_exists($class))
{
if ( ! file_exists(APPPATH.'controllers/'.$class.'.php'))
{
show_404("{$class}/{$method}");
}

include_once(APPPATH.'controllers/'.$class.'.php');
unset($CI);
$CI = new $class();
}
}
else
{
show_404("{$class}/{$method}");
}
}

// 终于调用方法了,$this->load->view()把内容放到缓存区
call_user_func_array(array(&$CI, $method), array_slice($URI->rsegments, 2));
}
$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_end');
$EXT->_call_hook('post_controller');

//这里就是把缓存区的内容输出了
if ($EXT->_call_hook('display_override') === FALSE)
{

$OUT->_display();
}

$EXT->_call_hook('post_system');
//关闭数据库的链接
if (class_exists('CI_DB') AND isset($CI->db))
{
$CI->db->close();
}

 
48月/120

Google Analytics(分析) 跨域分析帮助

发布在 邵珠庆

 

    1.  
    2.  
    3. 跟踪多个域

    4. 当您设置 Google Analytics(分析)来跟踪不同的域,或是将 Google Analytics(分析)限制为跟踪您网站的某个部分时,就是在对 Google Analytics(分析)如何为您的网络媒体资源收集访问者数据进行调整。为了理解这一点,让我们先看看网站或网络媒体资源的默认 Google Analytics(分析)设置。

      默认情况下,Google Analytics(分析)会以组的形式记录对给定网址所进行的访问。例如,如果您设置跟踪您的博客 (myexampleblog.example.com),那么对您网站中的所有网页和子目录的访问将作为一个单元来进行收集和记录。如此一来,当某个用户从您网站上的一个网页转至同一网站上的另一网页时,Google Analytics(分析)报告会显示如下关系:

      • 网页之间的浏览路径
      • 总的网站停留时间,也就是页面停留时间的累计
      • 具体会话数和唯一身份会话数(访问次数)
      • 唯一身份访问者人数

      此外,Google Analytics(分析)将那些对独立网址进行的访问作为单独的、不相关的访问处理(引荐链接除外)。如果您不希望某个网站的访问者数据在 Google Analytics(分析)报告中显示为单独的不相关网站的数据,那么您会希望 Google Analytics(分析)按照这种方式工作。

      用来收集两个独立网站的数据的两个配置文件

      假定您拥有一间在线商店,并且在另一个域中设置了第三方购物车,如:

      1. www.example-petstore.com
      2. www.example-commerce-host.com/example-petstore/

      如果没有跨域自定义,那么当一位访问者先访问您的在线商店,随后又进入您的第三方购物车时,将被计为两位不同的访问者,他们在不同的时间段内进行了两次不同的访问,显然这种设置并不适用于此模式。

      然而,您可以使用跨域跟踪,确保 Google Analytics(分析)将这两个网站的流量记录到同一份报告中。在网站分析中,这通常称为“网站关联”。有了此功能,当一位访问者先访问您的在线商店,随后又进入您的购物车时,将被计为一位用户,而不是两位用户,而且他们于在线商店开始的会话将延续到购物车。

       

      用来收集两个独立网站的数据的一个配置文件

      1. 为何要使用跨域跟踪

        适合使用跨域跟踪的情况有很多种:

        • 您的购物车软件保留在第三方网站或其他域中。
        • 您在多个子域中都有网页,但您需要涵盖所有访问者数据的统一报告。
        • 您的某些网站内容显示于托管在其他域的 iframe 中。
        • 您想要单独跟踪网站内的特定区域,比如单独跟踪一个子目录,而不跟踪网站其余部分。

        在这些情况下,您需要对 Google Analytics(分析)如何收集数据做出调整,以便将多个独立域的流量收集到一个报告中。

        当您设置跨域跟踪时,您可以收集与两个网站相关的所有有用数据,还能够了解到以下信息(或更多):

        • 哪些关键字与您购物车内的物品相关
          如果没有跨域跟踪,Google Analytics(分析)对您购物车的报告只会显示关键字(直接)。
        • 访问者使用了哪些搜索引擎和常规搜索字词
          如果没有跨域跟踪,Google Analytics(分析)对您购物车的报告只会将您的在线商店网站显示为主要的引荐网站
        • 包括购买物品所用时间在内的访问的持续时间
        • 哪些目标网页对销售或目标完成情况贡献最大
  1. 可用配置

    1. 可用配置

      自定义域跟踪可用于多种情况:

      • 子域 - 跟踪访问过 dogs.example.com 和 www.example.com 这两个网站的所有访问者,两者的数据将显示在同一个报告配置文件中
      • 子目录 - 只跟踪访问了某个子目录(如 www.example.com/dogs)的访问者,其数据将单独显示在一份报告中,就像它是一个单独的网站一样
      • 顶级域 - 跟踪对您拥有的两个域(如 www.example.com 和 www.example2.com)进行访问的所有访问者,两者的数据将显示在同一个报告配置文件中
      • IFrame 的内容 - 跟踪另一个域的 iFrame 中的内容的访问者和浏览量数据。

       

      有关为这些类型的配置设置跟踪的说明,请参阅 Google 代码上的 跨域跟踪中的详细文档。

    2. 跟踪方法列表

      由于跨域跟踪需要在两个独立的域之间关联访问者数据,因此通常必须对 Google Analytics(分析)跟踪进行一些自定义。原因即在于 Google Analytics(分析)与大多数网站分析解决方案一样,使用 Cookie 来保存关于网站访问者的信息,例如:页面停留时间、引荐信息以及访问者是新访者还是回访者。

      为了使两个关联的网站共享访问者信息,必须将一个域的 Google Analytics(分析)Cookie 中的数据传递给另一个域,以便后者更新自己的 Google Analytics(分析)Cookie 集。*

      可以执行的自定义如下所示(并非每种情况都需要执行全部自定义):

      • _setDomainName()
      • _setAllowLinker()
      • _link()
      • _linkByPost()
      • _getLinkerUrl()
      • _setAllowHash()

       

      有关所有这些方法的详细描述,请参阅 Google 代码中的跨域跟踪文档以及关于跨域跟踪的跟踪代码参考

      * 默认情况下,某个域的 Cookie 集不能由其他域访问。有关如何标识 Cookie 的更详细说明,请参阅 Google 代码上关于 Cookie 的文档。

设置跨域跟踪

  1. 多个域

    通过 Google Analytics(分析)设置跨域跟踪时,需要对跟踪代码进行一些调整。您需要掌握 HTML 和一些基本的 JavaScript 知识才能顺利完成这一操作。如果您不熟悉 HTML,请与网站管理员联系,让他们帮您设置跨域跟踪。

    本文说明了如何自定义跟踪设置,以将完全独立的多个顶级域作为单个实体进行跟踪。在进行说明时,用到了两个顶级(虚拟)域:www.myownpersonaldomain.com 和 www.examplepetstore.com,并假设将它们作为单个网站进行跟踪。

    如果您使用 Google Analytics(分析)管理界面中的内置辅助工具跨多个顶级域进行跟踪,则跟踪代码应该已针对跟踪多个域进行了自定义。以下说明完整描述了跨域跟踪的设置。

    1. 设置跟踪。
      在各个域的每个网页的跟踪代码中添加以下行(以粗体显示):
      异步跟踪代码(默认)

      <script type="text/javascript">
      
        var _gaq = _gaq || [];
        _gaq.push(['_setAccount', 'UA-XXXXX-X']);
        _gaq.push(['_setDomainName', 'none']);
        _gaq.push(['_setAllowLinker', true]);
        _gaq.push(['_trackPageview']);
      
        (function() {
          var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
          ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
          var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
        })();
      
      </script>

      传统语法

       
    2. 设置交叉链接。
      在横跨各个域的链接中调用 _link() 方法。如果您的当前链接类似以下形式:

      <a href="https://www.secondsite.com/?login=parameters">立即登录</a> 

      请将其改为:

      异步跟踪代码(默认)

      <a href="https://www.secondsite.com/?login=parameters" onclick="_gaq.push(['_link', 'https://www.secondsite.com/?login=parameters']); return false;">Login Now</a>

      传统语法

       

       

    3. 设置表单。
      如果您使用表单在域之间发送信息,则需要使用 _linkByPost() 方法。

      异步跟踪代码(默认)

      <form name="f" method="post" onsubmit="_gaq.push(['_linkByPost', this]);">

      传统语法

       

      请注意:上述函数同样适用于使用 GET 方法的表单。不过请注意,由于表单数据与 Google Analytics(分析)跟踪都会生成很长的查询字符串,因此如果您的网络服务器设有网址长度限制(例如 255 个字节),对 GET 表单使用此方法可能会发生问题。 
       

    4. 单独显示域名。
      默认情况下,报告中的数据仅包括路径和网页名称,而不包括域名。例如,网页列表在“内容”报告中显示为/about/contactUs.html,不会显示域名。如果您跨两个域进行跟踪,那么您会很难分辨网页是属于哪一个域;尤其当每个网站上的目录结构与网页名称很相似时,更是如此。

      如果您希望在报告中查看域名,可使用以下设置为配置文件创建高级过滤器

    过滤器类型自定义过滤器 > 高级
    字段 A主机名提取 A:(.*)
    字段 B请求 URI
    提取(.*) 
    输出至请求 URI 
    构造器$A1$B1

     

    不要忘了主配置文件:保存此过滤器后,配置文件只会接收与过滤器表达式相匹配的数据。这意味着,如果您在设置此过滤器时出现任何差错,您的配置文件可能根本接收不到数据。基于这一原因,我们建议您先设置一个不使用任何过滤器的主配置文件,这样,即使过滤器出现错误,您还有基准数据可供使用。建议您创建配置文件副本来跟踪同一个网站,然后在副本中创建您的高级过滤器。为了适应新的网址结构,有可能还需要对过滤器和目标进行修改。了解有关创建配置文件副本的详情。
    如果您仍在使用传统 (ga.js) 跟踪方法。如果您的网页包含对 _trackPageview()_link()_trackTrans()或 _linkByPost() 的调用,则您的 Google Analytics(分析)跟踪代码必须放在 HTML 代码中所有这些调用之前。此时,跟踪代码可以放在起始 <body> 标记和 JavaScript 调用之间的任意位置。要简化电子商务和跨域跟踪,建议您迁移至最新版本的跟踪代码,以获得最佳结果。

  2.  
38月/121

Google Analytics跨域跟踪原理分析与设置-[跟踪代码设置]

发布在 邵珠庆

 

自定义域跟踪可用于多种情况:

  • 子域 - 跟踪访问过 dogs.example.com 和 www.example.com 这两个网站的所有访问者,两者的数据将显示在同一个报告配置文件中
  • 子目录 - 只跟踪访问了某个子目录(如 www.example.com/dogs)的访问者,其数据将单独显示在一份报告中,就像它是一个单独的网站一样
  • 顶级域 - 跟踪对您拥有的两个域(如 www.example.com 和 www.example2.com)进行访问的所有访问者,两者的数据将显示在同一个报告配置文件中
  • IFrame 的内容 - 跟踪另一个域的 iFrame 中的内容的访问者和浏览量数据。

部署实现要点,请严格按照实验步骤实现:

前两点:注意push方法的顺序;_setDomainName设置

  _gaq.push(['_setAccount', 'UA-XXX-1']);

  _gaq.push(['_setDomainName', '.XXX.cn']);//(请设置在各自跨域的根域下)

  _gaq.push(['_setAllowLinker', true]);

  _gaq.push(['_setAllowHash', false]);

  _gaq.push(['_trackPageview']);//(请放在后面,否则会重新刷新Cookie)

后两点:传递链接href与_link要是同一个URL;要写return false

<a href="http://abc.shaozhuqing.com/abc.html" onclick="_gaq.push(['_link', ' http://abc.shaozhuqing.com/abc.html']); return false;">_link(get) View My OTA fengshun</a>

辅助工具插件:

Firebug查看Cookie的变化和_utm.gif参数的专递

Omnibug 监控服务器发送请求情况

默认情况下,Google Analytics为每个独立域名(包括主域名下的子域名)创建单独的数据报告(包括访问者信息、流量来源等),两个域名网站间的任何链接将被归为引荐流量。因此,如果要实现同时跨域跟踪主域名与子域名、单独跟踪网站的某个子目录、同时跨域跟踪两个主域名、跟踪iFrame嵌入的域名内容等,就需要对Google Analytics基础跟踪代码进行自定义,以实现跨域跟踪。本文将介绍Google Analytics跨域跟踪的设置,以及和大家一起探讨跨域跟踪的原理。

Google Analytics跨域跟踪代码设置:跨域跟踪主域名及其子域名、跨域跟踪两个主域名、 跨域跟踪主域名与另一主域名的子目录 、 跨iFrame跟踪 、 跟踪单个子目录 、 跨主域名跟踪其两个或多个子目录

跨域跟踪原理说明

默认情况,Google Analytics通过读取网页托管的 document.domain 属性,获取网站的域名(主域名、子域名),然后为其设置/读取 Cookie,并最终生成该域名的数据。

Google Analytics跨域跟踪Cookie主域名与域哈希值

图一:Google Analytics跨域跟踪Cookie主域名与域哈希值

因此,可以通过控制各域名间(主域名与主域名之间、主域名与子域名之间)的Cookie共享,将各域名的访问数据记录到同一个域名Cookie上,实现主域名之间、主域名与子域名之间的访客数据共享,即跨域跟踪。

跨域跟踪设置成功后,通过查看浏览器访问Cookie中Google Analytics跟踪参数_utma,你会发现各个域名下的Cookie有相同的部分,例如跨域跟踪主域名与其二级域名设置后,Cookie中_utma都属于共同一个域名,例如.exmaple.com,且其域哈希值为1。

同样,可以通过限制Cookie共享,实现Google Analytics跟踪单个子目录。

Google Anlaytics跨域跟踪主域名及其子域名

图二:Google Anlaytics跨域跟踪主域名及其子域名

跨域跟踪主域名及其子域名

通常,规模较大的网站会考虑为每个大分类、项目、购物车等在主域名下创建一个独立子域名,例如:

* www.example-.com

* news.example.com

* cart.example.com

对于此类情况,你需要对主域名与子域名的所有跟踪代码使用以下的自定义设置。

<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-1']);
_gaq.push(['_setDomainName', '.example.com']);
_gaq.push(['_setAllowHash', false]);
_gaq.push(['_trackPageview']);

 

(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>

说明:_setAllowHash(bool),此方法主要用于设置“允许域哈希”,当设置为true(默认值)时Google Analytics会为每个域名设置一个哈希值,并以此来辨别访客Cookie的完整性,此时访客Cookie将拒绝接收另外域名的访问数据;设置为false将停用域哈希功能(此时域哈希值为1),可进行Cookie数据共享。

Google Analytics跨域跟踪两个不同的主域名

图三:Google Analytics跨域跟踪两个不同的主域名

_setDomainName(DomainName),为Cookie设置域名,跨域跟踪时设置为:前导向“.”主域名,设置前导向“.”主要便于更深层级子域名的跨域跟踪,例如跨域跟踪二级子域名下的三级子域名a.news.example.com。

通过将各域名所有跟踪代码,设置为以上自定义跟踪,即可实现跨域跟踪主域名及其多级子域名。

跨域跟踪两个主域名

若要同时跟踪两个不同的主域名(例如网店www.store.com与购物车www.carts.com),可以做一下跟踪代码自定义。

主域名www.store.com中设置的跟踪代码

<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-1']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_setDomainName', '.store.com']);
_gaq.push(['_setAllowHash', false]);

(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>

同时,需要给www.store.com中跳转到www.carts.com的链接以及数据表单添加跟踪代码,以传递Cookie共享数据:

<a href="www.carts.com"
onclick="_gaq.push(['_link', 'www.carts.com']); return false;">View My Carts</a>
<form name="f" method="post" onsubmit="_gaq.push(['_linkByPost', this]);">

主域名www.carts.com的跟踪代码为:

<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-1']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_setDomainName', '.carts.com']);
_gaq.push(['_setAllowHash', false]);

(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>

同时,传递链接以及数据表单Cookie共享数据:

<a href="www.store.com"
onclick="_gaq.push(['_link', 'www.store.com']); return false;">View My Store</a>
<form name="f" method="post" onsubmit="_gaq.push(['_linkByPost', this]);">

说明:_setAllowLinker(bool),true启用跨域跟踪,并允许通过链接、表单在地址参数中传输Cookie数据,默认为flase;_link()、_linkByPost()分别用于跨域跟踪中链接、表单的Cookie数据传输。

跨域跟踪主域名与另一主域名的子目录

如果想跨域跟踪跟踪一个网站(www.example.com)以及在一另个域名目录下的内容(例如博客、购物车等,www.other.com/myblog),那么可以采用以下跟踪代码(请自行修改UA-XXXXX-1、.example.com):

主域名www.example.com中设置的跟踪代码

<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-1']);
_gaq.push(['_setAllowLinker', true]);
_gaq.push(['_setDomainName', '.example.com']);
_gaq.push(['_setAllowHash', false]);

 

(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>

同时,需要给www.example.com中跳转到www.other.com/myblog的链接以及数据表单添加跟踪代码,以传递Cookie共享数据:

<a href="www.other.com/myblog"
onclick="_gaq.push(['_link', 'www.other.com/myblog']); return false;">View My Blog</a>
<form name="f" method="post" onsubmit="_gaq.push(['_linkByPost', this]);">

另一主域名的子目录跟踪代码为:

<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-1']);
_gaq.push(['_setDomainName', 'none']);
_gaq.push(['_setAllowLinker', true]);

 

(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>

同样需要给跳转到www.example.com的链接与数据表单,添加跟踪代码:

<a href="www.example.com"
onclick="_gaq.push(['_link', 'www.example.com']); return false;">View My Site</a>
<form name="f" method="post" onsubmit="_gaq.push(['_linkByPost', this]);">
Google Analytics跨iFrame跟踪

图四:Google Analytics跨iFrame跟踪

由于二者没有共同的主域名,因此需要将www.other.com/myblog中的_setDomainName设置为none,并通过给链接、表单添加代码传送Cookie共享数据。

跨iFrame跟踪

若想同时跟踪网站(例如www.example.com)以及通过iFrame嵌入的另一个网站(例如www. iframecontent.com)间的访客信息、广告些列信息等,需要已设置跨域跟踪两个域名的跟踪代码后,使用在www.example.com中(iframe嵌入的当前页)使用_getLinkerUrl() 方法传输共享Cookie数据。

_gaq.push(function() {
var pageTracker = _gat._getTrackerByName();
var iframe = document.getElementById('myIFrame');
iframe.src = pageTracker._getLinkerUrl('http://www. iframecontent.com/');
});

说明:_getLinkerUrl(targetUrl, useHash) ,用于跨域跟踪主域名与iFrame(或者在新窗口中打开的外部网站链接)时的网址参数传递Cookie共享数据。

跟踪单个子目录

如果你的网站字子目录没有采用子域名形式,内容又很多或者是单个独立项目(例如www.example.com/myblog/),想单独跟踪,那么可以在该子目录下采取以下跟踪代码:

<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXX-1']);
_gaq.push(['_setCookiePath', '/myblog/']);
_gaq.push(['_trackPageview']);

 

(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>

说明:_setCookiePath(newCookiePath),为您的网站设置新的 Cookie 路径。默认Cookie路径为主域名的根级路径(/),通过它可以设置Google Analytics所有跟踪的默认路径,从而限制跟踪,实现跟踪单个子目录。

此时,Google Analytics会将/myblog/下的页面当做一个独立的域名站点对待,有独立的访客信息与广告系列信息。
 

112月/114

5W2H分析法

发布在 邵珠庆

5W2H分析法

5W2H分析法又叫七何分析法,是二战中美国陆军兵器修理部首创,简单、方便,易于理解、使用,富有启发意义,广泛用于企业管理和技术活动,对于决策和执行性的活动措施也非常有帮助,也有助于弥补考虑问题的疏漏。

5W2H分析法简介

5W2H法是第二世界大战中美国陆军兵器修理部首创。简单、方便,易于理解、使用,富有启发意义,广泛用于企业管理和技术活动,对于决策和执行性的活动措施也非常有帮助,也有助于弥补考虑问题的疏漏。

1 WHY——为什么?为什么要这么做?理由何在?原因是什么?

2 WHAT——是什么?目的是什么?做什么工作?

3 WHERE——何处?在哪里做?从哪里入手?

4 WHEN——何时?什么时间完成?什么时机最适宜?

5 WHO——谁?由谁来承担?谁来完成?谁负责?

6 HOW——怎么做?如何提高效率?如何实施?方法怎样?

7 HOW MUCH——多少?做到什么程度?数量如何?质量水平如何?费用产出如何?

发明者用五个以W开头的英语单词和两个以H开头的英语单词进行设问,发现解决问题的线索,寻找发明思路,进行设计构思,从而搞出新的发明项目,这就叫做5W2H法。

提出疑问于发现问题和解决问题是极其重要的。创造力高的人,都具有善于提问题的能力,众所周知,提出一个好的问题,就意味着问题解决了 一半。提问题的技巧高,可以发挥人的想象力。相反,有些问题提出来,反而挫伤我们的想象力。发明者在设计新产品时,常常提出:为什么(Why);做什么 (What);何人做(Who);何时(When);何地(Where);如何(How);多少(How much)。这就构成了5W2H法的总框架。如果提问题中常有假如……”如果……”是否……”这样的虚构,就是一种设问,设问需要更高的想象 力。

在发明设计中,对问题不敏感,看不出毛病是与平时不善于提问有密切关系的。对一个问题追根刨底,有可能发现新的知识和新的疑问。所以从根本上说,学会发明首 先要学会提问,善于提问。阻碍提问的因素,一是怕提问多,被别人看成什么也不懂的傻瓜,二是随着年龄和知识的增长,提问欲望渐渐淡薄。如果提问得不到答复 和鼓励,反而遭人讥讽,结果在人的潜意识中就形成了这种看法:好提问、好挑毛病的人是扰乱别人的讨厌鬼,最好紧闭嘴唇,不看、不闻、不问,但是这恰恰阻碍 了人的创造性的发挥。

5W2H法的应用程序

1、检查原产品的合理性

1)为什么(Why)?

为什么采用这个技术参数?为什么不能有响声?为什么停用?为什么变成红色:为什么要做成这个形状?为什么采用机器代替人力?为什么产品的制造要经过这么多环节?为什么非做不可?

2)做什么(What)?

条件是什么?哪一部分工作要做?目的是什么?重点是什么?与什么有关系?功能是什么?规范是什么?工作对象是什么?

3)谁(Who)?

谁来办最方便?谁会生产?谁可以办?谁是顾客?谁被忽略了?谁是决策人?谁会受益?

4)何时(When)?

何时要完成?何时安装?何时销售?何时是最佳营业时间?何时工作人员容易疲劳?何时产量最高?何时完成最为时宜?需要几天才算合理?

5)何地(Where)?

何地最适宜某物生长?何处生产最经济?从何处买?还有什么地方可以作销售点?安装在什么地方最合适?何地有资源?

6)怎样(How to)?

怎样做省力?怎样做最快?怎样做效率最高?怎样改进?怎样得到?怎样避免失败?怎样求发展?怎样增加销路?怎样达到效率?怎样才能使产品更加美观大方?怎样使产品用起来方便?

7)多少(How much)?

功能指标达到多少?销售多少?成本多少?输出功率多少?效率多高?尺寸多少?重量多少?

2、找出主要优缺点

如果现行的做法或产品经过七个问题的审核已无懈可击,便可认为这一做法或产品可取。如果七个问题中有一个答复不能令人满意,则表示这方面有改进余地。如果哪方面的答复有独创的优点,则可以扩大产品这方面的效用。

3、决定设计新产品

克服原产品的缺点,扩大原产品独特优点的效用。

标签: , 4 评论
   下一页