2012年10月30日星期二

阮一峰的网络日志

阮一峰的网络日志


Javascript模块化编程(二):AMD规范

Posted: 30 Oct 2012 01:58 AM PDT

这个系列的第一部分介绍了Javascript模块的基本写法,今天介绍如何规范地使用模块。

(接上文

七、模块的规范

先想一想,为什么模块很重要?

因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。

但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!考虑到Javascript模块现在还没有官方规范,这一点就更重要了。

目前,通行的Javascript模块规范共有两种:CommonJSAMD。我主要介绍AMD,但是要先从CommonJS讲起。

八、CommonJS

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。

这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

  var math = require('math');

然后,就可以调用模块提供的方法:

  var math = require('math');

  math.add(2,3); // 5

因为这个系列主要针对浏览器编程,不涉及node.js,所以对CommonJS就不多做介绍了。我们在这里只要知道,require()用于加载模块就行了。

九、浏览器环境

有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?

  var math = require('math');

  math.add(2, 3);

第二行Math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

十、AMD

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

  require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:

  require(['math'], function (math) {

    math.add(2, 3);

  });

math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。

目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js。本系列的第三部分,将通过介绍require.js,进一步讲解AMD的用法,以及如何将模块化编程投入实战。

(完)

文档信息

2012年10月26日星期五

阮一峰的网络日志

阮一峰的网络日志


Javascript模块化编程(一):模块的写法

Posted: 25 Oct 2012 05:32 PM PDT

随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂。

网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑。

Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。

但是,Javascript不是一种模块化编程语言,它不支持""(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版,将正式支持"类"和"模块",但还需要很长时间才能投入实用。)

Javascript社区做了很多努力,在现有的运行环境中,实现"模块"的效果。本文总结了当前"Javascript模块化编程"的最佳实践,说明如何投入实用。虽然这不是初级教程,但是只要稍稍了解Javascript的基本语法,就能看懂。

一、原始写法

模块就是实现特定功能的一组方法。

只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。

  function m1(){
    //...
  }

  function m2(){
    //...
  }

上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。

这种做法的缺点很明显:"污染"了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。

二、对象写法

为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

  var module1 = new Object({

    _count : 0,

    m1 : function (){
      //...
    },

    m2 : function (){
      //...
    }

  });

上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。

  module1.m1();

但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

  module1._count = 5;

三、立即执行函数写法

使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。

  var module1 = (function(){

    var _count = 0;

    var m1 = function(){
      //...
    };

    var m2 = function(){
      //...
    };

    return {
      m1 : m1,
      m2 : m2
    };

  })();

使用上面的写法,外部代码无法读取内部的_count变量。

  console.info(module1._count); //undefined

module1就是Javascript模块的基本写法。下面,再对这种写法进行加工。

四、放大模式

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。

  var module1 = (function (mod){

    mod.m3 = function () {
      //...
    };

    return mod;

  })(module1);

上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。

五、宽放大模式(Loose augmentation)

在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。

  var module1 = ( function (mod){

    //...

    return mod;

  })(window.module1 || {});

与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。

六、输入全局变量

独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。

为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

  var module1 = (function ($, YAHOO) {

    //...

  })(jQuery, YAHOO);

上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。这方面更多的讨论,参见Ben Cherry的著名文章《JavaScript Module Pattern: In-Depth》

这个系列的第二部分,将讨论如何在浏览器环境组织不同的模块、管理模块之间的依赖性。

(完)

文档信息

2012年10月16日星期二

阮一峰的网络日志

阮一峰的网络日志


贝叶斯推断及其互联网应用(三):拼写检查

Posted: 16 Oct 2012 12:32 AM PDT

(这个系列的第一部分介绍了贝叶斯定理,第二部分介绍了如何过滤垃圾邮件,今天是第三部分。)

使用Google的时候,如果你拼错一个单词,它会提醒你正确的拼法。

比如,你不小心输入了seperate。

Google告诉你,这个词是不存在的,正确的拼法是seperate。

这就叫做"拼写检查"(spelling corrector)。有好几种方法可以实现这个功能,Google使用的是基于贝叶斯推断的统计学方法。这种方法的特点就是快,很短的时间内处理大量文本,并且有很高的精确度(90%以上)。Google的研发总监Peter Norvig,写过一篇著名的文章,解释这种方法的原理。

下面我们就来看看,怎么利用贝叶斯推断,实现"拼写检查"。其实很简单,一小段代码就够了。

一、原理

用户输入了一个单词。这时分成两种情况:拼写正确,或者拼写不正确。我们把拼写正确的情况记做c(代表correct),拼写错误的情况记做w(代表wrong)。

所谓"拼写检查",就是在发生w的情况下,试图推断出c。从概率论的角度看,就是已知w,然后在若干个备选方案中,找出可能性最大的那个c,也就是求下面这个式子的最大值。

  P(c|w)

根据贝叶斯定理:

  P(c|w) = P(w|c) * P(c) / P(w)

对于所有备选的c来说,对应的都是同一个w,所以它们的P(w)是相同的,因此我们求的其实是

  P(w|c) * P(c)

的最大值。

P(c)的含义是,某个正确的词的出现"概率",它可以用"频率"代替。如果我们有一个足够大的文本库,那么这个文本库中每个单词的出现频率,就相当于它的发生概率。

P(w|c)的含义是,在试图拼写c的情况下,出现拼写错误w的概率。这需要统计数据的支持,但是为了简化问题,我们假设两个单词在字形上越接近,就有越可能拼错。

举例来说,相差一个字母的拼法,就比相差两个字母的拼法,发生概率更高。你想拼写单词hello,那么错误拼成hallo(相差一个字母)的可能性,就比拼成haallo高(相差两个字母)。

二、算法

最简单的算法,只需要四步就够了。

第一步,建立一个足够大的文本库。

网上有一些免费来源,比如古登堡计划Wiktionary英国国家语料库等等。

第二步,取出文本库的每一个单词,统计它们的出现频率。

第三步,根据用户输入的单词,得到其所有可能的拼写相近的形式。

所谓"拼写相近",指的是两个单词之间的"编辑距离"(edit distance)不超过2。也就是说,两个词只相差1到2个字母,只通过----删除、交换、更改和插入----这四种操作中的一种,就可以让一个词变成另一个词。

第四步,比较所有拼写相近的词在文本库的出现频率。频率最高的那个词,就是正确的拼法。

根据Peter Norvig的验证,这种算法的精确度大约为60%-70%(10个拼写错误能够检查出6个。)虽然不令人满意,但是能够接受。毕竟它足够简单,计算速度极快。(本文的最后部分,将详细讨论这种算法的缺陷在哪里。)

三、代码

我们使用Python语言,实现上一节的算法。

第一步,把网上下载的文本库保存为big.txt文件。这步不需要编程。

第二步,加载Python的正则语言模块(re)和collections模块,后面要用到。

  import re, collections

第三步,定义words()函数,用来取出文本库的每一个词。

  def words(text): return re.findall('[a-z]+', text.lower())

lower()将所有词都转成小写,避免因为大小写不同,而被算作两个词。

第四步,定义一个train()函数,用来建立一个"字典"结构。文本库的每一个词,都是这个"字典"的键;它们所对应的值,就是这个词在文本库的出现频率。

  def train(features):

    model = collections.defaultdict(lambda: 1)

    for f in features:

      model[f] += 1

    return model

collections.defaultdict(lambda: 1)的意思是,每一个词的默认出现频率为1。这是针对那些没有出现在文本库的词。如果一个词没有在文本库出现,我们并不能认定它就是一个不存在的词,因此将每个词出现的默认频率设为1。以后每出现一次,频率就增加1。

第五步,使用words()和train()函数,生成上一步的"词频字典",放入变量NWORDS。

  NWORDS = train(words(file('big.txt').read()))

第六步,定义edits1()函数,用来生成所有与输入参数word的"编辑距离"为1的词。

  alphabet = 'abcdefghijklmnopqrstuvwxyz'

  def edits1(word):

    splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]

    deletes = [a + b[1:] for a, b in splits if b]

    transposes = [a + b[1] + b[0] + b[2:] for a, b in splits if len(b)>1]

    replaces = [a + c + b[1:] for a, b in splits for c in alphabet if b]

    inserts = [a + c + b for a, b in splits for c in alphabet]

    return set(deletes + transposes + replaces + inserts)

edit1()函数中的几个变量的含义如下:

  (1)splits:将word依次按照每一位分割成前后两半。比如,'abc'会被分割成 [('', 'abc'), ('a', 'bc'), ('ab', 'c'), ('abc', '')] 。

  (2)beletes:依次删除word的每一位后、所形成的所有新词。比如,'abc'对应的deletes就是 ['bc', 'ac', 'ab'] 。

  (3)transposes:依次交换word的邻近两位,所形成的所有新词。比如,'abc'对应的transposes就是 ['bac', 'acb'] 。

  (4)replaces:将word的每一位依次替换成其他25个字母,所形成的所有新词。比如,'abc'对应的replaces就是 ['abc', 'bbc', 'cbc', ... , 'abx', ' aby', 'abz' ] ,一共包含78个词(26 × 3)。

  (5)inserts:在word的邻近两位之间依次插入一个字母,所形成的所有新词。比如,'abc' 对应的inserts就是['aabc', 'babc', 'cabc', ..., 'abcx', 'abcy', 'abcz'],一共包含104个词(26 × 4)。

最后,edit1()返回deletes、transposes、replaces、inserts的合集,这就是与word"编辑距离"等于1的所有词。对于一个n位的词,会返回54n+25个词。

第七步,定义edit2()函数,用来生成所有与word的"编辑距离"为2的词语。

  def edits2(word):

    return set(e2 for e1 in edits1(word) for e2 in edits1(e1))

但是这样的话,会返回一个 (54n+25) * (54n+25) 的数组,实在是太大了。因此,我们将edit2()改为known_edits2()函数,将返回的词限定为在文本库中出现过的词。

  def known_edits2(word):

    return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)

第八步,定义correct()函数,用来从所有备选的词中,选出用户最可能想要拼写的词。

  def known(words): return set(w for w in words if w in NWORDS)

  def correct(word):

    candidates = known([word]) or known(edits1(word)) or known_edits2(word) or [word]

    return max(candidates, key=NWORDS.get)

我们采用的规则为:

  (1)如果word是文本库现有的词,说明该词拼写正确,直接返回这个词;

  (2)如果word不是现有的词,则返回"编辑距离"为1的词之中,在文本库出现频率最高的那个词;

  (3)如果"编辑距离"为1的词,都不是文本库现有的词,则返回"编辑距离"为2的词中,出现频率最高的那个词;

  (4)如果上述三条规则,都无法得到结果,则直接返回word。

至此,代码全部完成,合起来一共21行。

  import re, collections

  def words(text): return re.findall('[a-z]+', text.lower())

  def train(features):

    model = collections.defaultdict(lambda: 1)

    for f in features:

      model[f] += 1

    return model

  NWORDS = train(words(file('big.txt').read()))

  alphabet = 'abcdefghijklmnopqrstuvwxyz'

  def edits1(word):

    splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]

    deletes = [a + b[1:] for a, b in splits if b]

    transposes = [a + b[1] + b[0] + b[2:] for a, b in splits if len(b)>1]

    replaces = [a + c + b[1:] for a, b in splits for c in alphabet if b]

    inserts = [a + c + b for a, b in splits for c in alphabet]

    return set(deletes + transposes + replaces + inserts)

  def known_edits2(word):

    return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)

  def known(words): return set(w for w in words if w in NWORDS)

  def correct(word):

    candidates = known([word]) or known(edits1(word)) or known_edits2(word) or [word]

    return max(candidates, key=NWORDS.get)

使用方法如下:

  >>> correct('speling')

  'spelling'

  >>> correct('korrecter')

  'corrector'

四、缺陷

我们使用的这种算法,有一些缺陷,如果投入生产环境,必须在这些方面加入改进。

(1)文本库必须有很高的精确性,不能包含拼写错误的词。

如果用户输入一个错误的拼法,文本库恰好包含了这种拼法,它就会被当成正确的拼法。

(2)对于不包含在文本库中的新词,没有提出解决办法。

如果用户输入一个新词,这个词不在文本库之中,就会被当作错误的拼写进行纠正。

(3)程序返回的是"编辑距离"为1的词,但某些情况下,正确的词的"编辑距离"为2。

比如,用户输入reciet,会被纠正为recite(编辑距离为1),但用户真正想要输入的词是receipt(编辑距离为2)。也就是说,"编辑距离"越短越正确的规则,并非所有情况下都成立。

(4)有些常见拼写错误的"编辑距离"大于2。

这样的错误,程序无法发现。下面就是一些例子,每一行前面那个词是正确的拼法,后面那个则是常见的错误拼法。

purple perpul
curtains courtens
minutes muinets
successful sucssuful
inefficient ineffiect
availability avaiblity
dissension desention
unnecessarily unessasarily
necessary nessasary
unnecessary unessessay
night nite
assessing accesing
necessitates nessisitates

(5)用户输入的词的拼写正确,但是其实想输入的是另一个词。

比如,用户输入是where,这个词拼写正确,程序不会纠正。但是,用户真正想输入的其实是were,不小心多打了一个h。

(6)程序返回的是出现频率最高的词,但用户真正想输入的是另一个词。

比如,用户输入ther,程序会返回the,因为它的出现频率最高。但是,用户真正想输入的其实是their,少打了一个i。也就是说,出现频率最高的词,不一定就是用户想输入的词。

(7)某些词有不同的拼法,程序无法辨别。

比如,英国英语和美国英语的拼法不一致。英国用户输入'humur',应该被纠正为'humour';美国用户输入'humur',应该被纠正为'humor'。但是,我们的程序会统一纠正为'humor'。

(完)

文档信息

2012年10月8日星期一

阮一峰的网络日志

阮一峰的网络日志


Google日历简易版 2.0

Posted: 07 Oct 2012 08:58 AM PDT

长假期间,我写了一个小程序,现在正式发布。

大家用不用Google日历

它可以用来规划日程、记录事项、甚至写日记,既安全(数据保存在Google的机房)又方便(各种平台都能访问),甚至还很贴心地提供手机同步免费短信提醒

相信很多人与我一样,非常需要这个产品。但是,又不喜欢它的界面:拥挤丑陋,辨识困难,操作麻烦。于是,2008年,我写了一个"Google日历简易版"。

今年四月份,Google启用新版本API,我的那个程序彻底无法使用了。考虑到还有需求,利用这几天,我索性就重写了一遍。

现在就让我,正式推出"Google日历简易版 2.0"

  * 操作简便,只需鼠标一点,就可以看到近期事件;

  * 界面清爽,放大了字体,易于阅读;

  * 快速安全,直接与Google交互,全程https加密通信。

欢迎大家试用,看看有没有bug。网址是:

  http://calendar.ruanyifeng.com

两点使用说明:

  1)支持各大浏览器的最新版本,IE6、7、8、9除外(因为它们不支持ajax跨域)。

  2)这个程序对Javascipt的要求比较高,移动终端方面,我的Android平板可以使用,但是Android手机不行。有ios设备的朋友,帮忙看看,ipad/iphone能不能用。

======================================

(关于发布软件的内容到此为止,接下来是插播时间,我实在忍不住,想谈谈Google。)

这个程序全靠Google的API,但是Google是怎么开放API的?用户是不知道,开发者看了,心都凉了。

今年四月生效的API第三版,比第二版少了很多功能。其中有两个,影响尤其巨大。

  1. 只提供所有事件(按日期)升序排列,不提供(按日期)降序排列。

  2. 不提供某个时间段内的事件总数。

少了这两个基本功能,还怎么玩呀?!你写了一个日历程序,可是连用户的最新事件都取不到......(我现在的解决方法是,一个时间段内限定取回30个事件。如果超出这个数量,只有用户自行缩短时间段了。)

此外,Google还规定,日历API每天请求上限是10000次。你没有看错,真的只有四个零。我数了好几遍,都不敢相信自己的眼睛。

这就是说,你的用户总数,每天最多只能有几百人,Google不允许你发展更多的用户。(相比之下,Google的短网址API,每天请求上限是100万次!)所以,基于这个API的任何程序,大概只能是写写玩玩,不可能考虑运营与发展。

我认为,Google这样地封闭平台,无非就是为了防止外部开发者与其竞争,尽量把用户留在自家网站上。这种鼠目寸光、画地为牢的行为,哪来还有半点理想主义的色彩?

Google,枉费我还为你呐喊过!

========================================

不管怎么说,这个"Google日历简易版",我还是会维护下去的(毕竟眼前找不到更好的在线日历)。

下一次大版本的更新,我打算实现下面两个功能:

  1. 颜色标签,不同事件采用不同的背景色;

  2. 所有事件都用LocalStorage储存在本地(要不是想到得太晚,这一次我就应该实现这个功能)。

顺便提一下,这一次我是用Bootstrap框架开发的,感觉它方便好用,效果也不错。但是下一次,大概不会用它了,因为觉得不够灵活,很多地方都被它限制住了。Foundation框架对我有可能是一个更好的选择。

(完)

文档信息

2012年10月4日星期四

阮一峰的网络日志

阮一峰的网络日志


网站的无密码登录

Posted: 03 Oct 2012 06:00 AM PDT

大部分网站,都要求用户登录。

常见的做法,是让用户注册一个账户。

这种做法并不让人满意。

对于用户来说,每个网站必须记住一个密码,非常麻烦;对于开发者来说,必须承担保护密码的责任,一旦密码泄漏,对网站的业务和信誉都是巨大打击

所以,很早以前,人们就开始设想"无密码登录"(password-less login)。这对用户和网站,都将是极大的减负。

本文先回顾"无密码登录"的几种常见做法,然后探讨一种最简单的实现。

一、OpenID

OpenID是最早提出的一种无密码登录。

它的设想是这样的:互联网上每一个网址(URL),都指向一个独一无二的网页,这说明网址具有唯一性。因此,可以用网址来标识用户。

所以,使用OpenID的网站,不要求用户输入"用户名",而要求用户输入一个代表其身份的网址。然后,向该网址进行求证,如果得到证实,就允许用户登录,从而实现"无密码登录"。

OpenID有两个很大的缺点:一是需要服务器端支持,二是使用网址表示身份,违背直觉,普通用户难以理解。因此,始终无法得到推广。

二、第三方账户

OpenID的实质,是让第三方网站认证用户身份。那么很显然,这等同于用户在第三方网站登录。

因此,可以直接告诉用户,使用第三方帐号登录(前提是对方支持OpenID)。

这样做的优点是比较直观,用户容易接受;缺点是自身的业务,从此多多少少要依赖第三方网站。比如,现在很多网站使用Facebook帐号登录,一旦Facebook出现故障,这些网站都会受到影响。

三、Persona

去年,Mozilla提出了Persona方案,号称是无密码登录的终极解决方案

它与OpenID异曲同工。后者用网址标识用户,它用Email标识用户。用户键入Email地址以后,网站向Email服务器请求认证。

虽然这种方案还处在推广期,效果有待观察。但是,我目前不太看好它。一则,它的技术要求和流程,比OpenID更复杂,无法用一句话讲清楚;二则,它要求服务器端支持,很难想象世界上大部分Email服务器都会部署Persona代码。

四、OAuth

OAuth协议其实与"第三方帐户"是一回事。

"第三方账户"是第三方网站提供用户身份认证,属于"认证"服务(authentication);OAuth则是更进一步,第三方网站允许你直接操作它的用户数据,属于"授权"服务(authorization)。

因为涉及到用户数据的改变,所以OAuth认证比Openid认证要求更严格。通常,只有针对某个第三方网站的外部服务,才需要用到OAuth;如果只是单纯地区分用户身份,其实没必要用它。

五、Email一次性登录

上面四种登录方法,是目前主流的"无密码登录"。下面,我想介绍一种最简单的实现,它是美国程序员Ben Brown在今年7月份提出来的。

他的做法很简单。用户登录的时候,只显示一个Email地址输入框。

用户输入Email地址以后,网站就向该地址发出一封邮件,里面包含了一个登录链接。用户点击这个链接,就证明他/她确实是这个邮箱的主人,身份有效,从而实现登录。

登录链接只在一段时间内有效,但是可以通过cookie,让用户长时间处在登录状态。如果cookie失效,则重新向用户邮箱发出另一个登录链接即可。

由于整个认证过程,都通过电子邮件完成,彻底实现"无密码登录",而且操作流程很自然,易于理解。更重要的是,它使用现有的Email协议,不需要服务器端部署新的代码,具有最好的兼容性。

主要缺点是,它需要用户额外查看一次邮箱,稍显麻烦;它也不适合那种用户无法打开Email的场合,比如在朋友家中上网。因此,使用它的网站,还必须部署备用的登录方式。

总的来说,我觉得这是一个简单易行的好方法,以后做网站的时候,打算尝试一下。

想听听大家的意见,你觉得这种方法可行吗?

(完)

文档信息