2015年2月20日星期五

阮一峰的网络日志

阮一峰的网络日志


Make 命令教程

Posted: 20 Feb 2015 02:03 AM PST

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。

Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用Make构建。

本文介绍Make命令的用法,从简单的讲起,不需要任何基础,只要会使用命令行,就能看懂。我的参考资料主要是Isaac Schlueter的《Makefile文件教程》《GNU Make手册》

(题图:摄于博兹贾阿达岛,土耳其,2013年7月)

一、Make的概念

Make这个词,英语的意思是"制作"。Make命令直接用了这个意思,就是要做出某个文件。比如,要做出文件a.txt,就可以执行下面的命令。

 $ make a.txt 

但是,如果你真的输入这条命令,它并不会起作用。因为Make命令本身并不知道,如何做出a.txt,需要有人告诉它,如何调用其他命令完成这个目标。

比如,假设文件 a.txt 依赖于 b.txt 和 c.txt ,是后面两个文件连接(cat命令)的产物。那么,make 需要知道下面的规则。

 a.txt: b.txt c.txt     cat b.txt c.txt > a.txt 

也就是说,make a.txt 这条命令的背后,实际上分成两步:第一步,确认 b.txt 和 c.txt 必须已经存在,第二步使用 cat 命令 将这个两个文件合并,输出为新文件。

像这样的规则,都写在一个叫做Makefile的文件中,Make命令依赖这个文件进行构建。Makefile文件也可以写为makefile, 或者用命令行参数指定为其他文件名。

 $ make -f rules.txt # 或者 $ make --file=rules.txt 

上面代码指定make命令依据rules.txt文件中的规则,进行构建。

总之,make只是一个根据指定的Shell命令进行构建的工具。它的规则很简单,你规定要构建哪个文件、它依赖哪些源文件,当那些文件有变动时,如何重新构建它。

二、Makefile文件的格式

构建规则都写在Makefile文件里面,要学会如何Make命令,就必须学会如何编写Makefile文件。

2.1 概述

Makefile文件由一系列规则(rules)构成。每条规则的形式如下。

 <target> : <prerequisites>  [tab]  <commands> 

上面第一行冒号前面的部分,叫做"目标"(target),冒号后面的部分叫做"前置条件"(prerequisites);第二行必须由一个tab键起首,后面跟着"命令"(commands)。

"目标"是必需的,不可省略;"前置条件"和"命令"都是可选的,但是两者之中必须至少存在一个。

每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。下面就详细讲解,每条规则的这三个组成部分。

2.2 目标(target)

一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象,比如上文的 a.txt 。目标可以是一个文件名,也可以是多个文件名,之间用空格分隔。

除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target)。

 clean:       rm *.o 

上面代码的目标是clean,它不是文件名,而是一个操作的名字,属于"伪目标 ",作用是删除对象文件。

 $ make  clean 

但是,如果当前目录中,正好有一个文件叫做clean,那么这个命令不会执行。因为Make发现clean文件已经存在,就认为没有必要重新构建了,就不会执行指定的rm命令。

为了避免这种情况,可以明确声明clean是"伪目标",写法如下。

 .PHONY: clean clean:         rm *.o temp 

声明clean是"伪目标"之后,make就不会去检查是否存在一个叫做clean的文件,而是每次运行都执行对应的命令。像.PHONY这样的内置目标名还有不少,可以查看手册

如果Make命令运行时没有指定目标,默认会执行Makefile文件的第一个目标。

 $ make 

上面代码执行Makefile文件的第一个目标。

2.3 前置条件(prerequisites)

前置条件通常是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新(前置文件的last-modification时间戳比目标的时间戳新),"目标"就需要重新构建。

 result.txt: source.txt     cp source.txt result.txt 

上面代码中,构建 result.txt 的前置条件是 source.txt 。如果当前目录中,source.txt 已经存在,那么make result.txt可以正常运行,否则必须再写一条规则,来生成 source.txt 。

 source.txt:     echo "this is the source" > source.txt 

上面代码中,source.txt后面没有前置条件,就意味着它跟其他文件都无关,只要这个文件还不存在,每次调用make source.txt,它都会生成。

 $ make result.txt $ make result.txt 

上面命令连续执行两次make result.txt。第一次执行会先新建 source.txt,然后再新建 result.txt。第二次执行,Make发现 source.txt 没有变动(时间戳晚于 result.txt),就不会执行任何操作,result.txt 也不会重新生成。

如果需要生成多个文件,往往采用下面的写法。

 source: file1 file2 file3 

上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令。

 $ make source 

执行make source命令后,就会一次性生成 file1,file2,file3 三个文件。这比下面的写法要方便很多。

 $ make file1 $ make file2 $ make file3 

2.4 命令(commands)

命令(commands)表示如何更新目标文件,由一行或多行的Shell命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。

每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX声明。

 .RECIPEPREFIX = > all: > echo Hello, world 

上面代码用.RECIPEPREFIX指定,大于号(>)替代tab键。所以,每一行命令的起首变成了大于号,而不是tab键。

需要注意的是,每行命令在一个单独的shell中执行。这些Shell之间没有继承关系。

 var-lost:     export foo=bar     echo "foo=[$$foo]" 

上面代码执行后(make var-lost),取不到foo的值。因为两行命令在两个不同的进程执行。一个解决办法是将两行命令写在一行,中间用分号分隔。

 var-kept:     export foo=bar; echo "foo=[$$foo]" 

另一个解决办法是在换行符前加反斜杠转义。

 var-kept:     export foo=bar; \     echo "foo=[$$foo]" 

最后一个方法是加上.ONESHELL:命令。

 .ONESHELL: var-kept:     export foo=bar;      echo "foo=[$$foo]" 

三、Makefile文件的语法

3.1 注释

井号(#)在Makefile中表示注释。

 # 这是注释 result.txt: source.txt     # 这是注释     cp source.txt result.txt # 这也是注释 

3.2 回声(echoing)

正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing)。

 test:     # 这是测试 

执行上面的规则,会得到下面的结果。

 $ make test # 这是测试 

在命令的前面加上@,就可以关闭回声。

 test:     @# 这是测试 

现在再执行make test,就不会有任何输出。

由于在构建过程中,需要了解当前在执行哪条命令,所以通常只在注释和纯显示的echo命令前面加上@。

 test:     @# 这是测试     @echo TODO 

3.3 通配符

通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 [...] 。比如, *.o 表示所有后缀名为o的文件。

 clean:         rm -f *.o 

3.4 模式匹配

Make命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是%。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。

 %.o: %.c 

等同于下面的写法。

 f1.o: f1.c f2.o: f2.c 

使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。

3.5 变量和赋值符

Makefile 允许使用等号自定义变量。

 txt = Hello World test:     @echo $(txt) 

上面代码中,变量 txt 等于 Hello World。调用时,变量需要放在 $( ) 之中。

调用Shell变量,需要在美元符号前,再加一个美元符号,这是因为Make命令会对美元符号转义。

 test:     @echo $$HOME 

有时,变量的值可能指向另一个变量。

 v1 = $(v2) 

上面代码中,变量 v1 的值是另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。

为了解决类似问题,Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow

 VARIABLE = value # 在执行时扩展,允许递归扩展。  VARIABLE := value # 在定义时扩展。  VARIABLE ?= value # 只有在该变量为空时才设置值。  VARIABLE += value # 将值追加到变量的尾端。  

3.6 内置变量(Implicit Variables)

Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册

 output:     $(CC) -o output input.c 

3.7 自动变量(Automatic Variables)

Make命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。

(1)$@

$@指代当前目标,就是Make命令当前构建的那个目标。比如,make foo的 $@ 就指代foo。

 a.txt b.txt:      touch $@ 

等同于下面的写法。

 a.txt:     touch a.txt b.txt:     touch b.txt 

(2)$<

$< 指代第一个前置条件。比如,规则为 t: p1 p2,那么$< 就指代p1。

 a.txt: b.txt c.txt     cp $< $@  

等同于下面的写法。

 a.txt: b.txt c.txt     cp b.txt a.txt  

(3)$?

$? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。

(4)$^

$^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2 。

(5)$*

$* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1。

(6)$(@D) 和 $(@F)

$(@D) 和 $(@F) 分别指向 $@ 的目录名和文件名。比如,$@是 src/input.c,那么$(@D) 的值为 src ,$(@F) 的值为 input.c。

(7)$(<D) 和 $(<F)

$(<D) 和 $(<F) 分别指向 $< 的目录名和文件名。

所有的自动变量清单,请看手册。下面是自动变量的一个例子。

 dest/%.txt: src/%.txt     @[ -d dest ] || mkdir dest     cp $< $@ 

上面代码将 src 目录下的 txt 文件,拷贝到 dest 目录下。首先判断 dest 目录是否存在,如果不存在就新建,然后,$< 指代前置文件(src/%.txt), $@ 指代目标文件(dest/%.txt)。

3.8 判断和循环

Makefile使用 Bash 语法,完成判断和循环。

 ifeq ($(CC),gcc)   libs=$(libs_for_gcc) else   libs=$(normal_libs) endif 

上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。

 LIST = one two three all:     for i in $(LIST); do \         echo $$i; \     done  # 等同于  all:     for i in one two three; do \         echo $i; \     done  

上面代码的运行结果。

 one two three 

3.9 函数

Makefile 还可以使用函数,格式如下。

 $(function arguments) # 或者 ${function arguments} 

Makefile提供了许多内置函数,可供调用。下面是几个常用的内置函数。

(1)shell 函数

shell 函数用来执行 shell 命令

 srcfiles := $(shell echo src/{00..99}.txt) 

(2)wildcard 函数

wildcard 函数用来在 Makefile 中,替换 Bash 的通配符。

 srcfiles := $(wildcard src/*.txt) 

(3)替换函数

替换函数的写法是:变量名 + 冒号 + 替换规则。

 min: $(OUTPUT:.js=.min.js) 

上面代码的意思是,将变量OUTPUT中的 .js 全部替换成 .min.js 。

四、Makefile 的实例

(1)执行多个目标

 .PHONY: cleanall cleanobj cleandiff  cleanall : cleanobj cleandiff         rm program  cleanobj :         rm *.o  cleandiff :         rm *.diff 

上面代码可以调用不同目标,删除不同后缀名的文件,也可以调用一个目标(cleanall),删除所有指定类型的文件。

(2)编译C语言项目

 edit : main.o kbd.o command.o display.o      cc -o edit main.o kbd.o command.o display.o  main.o : main.c defs.h     cc -c main.c kbd.o : kbd.c defs.h command.h     cc -c kbd.c command.o : command.c defs.h command.h     cc -c command.c display.o : display.c defs.h     cc -c display.c  clean :      rm edit main.o kbd.o command.o display.o  .PHONY: edit clean 

今天,Make命令的介绍就到这里。下一篇文章我会介绍,如何用 Make 来构建 Node.js 项目。

(完)

文档信息

2015年2月10日星期二

阮一峰的网络日志

阮一峰的网络日志


JavaScript 有多灵活?

Posted: 09 Feb 2015 10:53 PM PST

JavaScript 是一种灵活的语言,表达力极强,我来举一个例子,保证让很多人大吃一惊。

本文受到了 Kyle Simpson 的文章《Iterating ES6 Numbers》的启发。

首先,在 Number.prototype 对象上,部署一个 add 方法。

  Number.prototype.add = function (x) {    return this + x;  };  

上面代码为 Number 的实例定义了一个 add 方法。(如果你对这种写法不熟悉,建议先阅读我写的《JavaScript 面向对象编程》。)

由于 Number 的实例就是数值,在数值上调用某个方法,数值会自动转为实例对象,所以就得到了下面的结果。

  8['add'](2)  // 10  

上面代码中,调用方法之所以写成8['add'],而不是8.add,是因为数值后面的点,会被解释为小数点,而不是点运算符。

将数值放在圆括号中,就可以使用点运算符调用方法了。

  (8).add(2)  // 10  

其实,还有另一种写法。

  8..add(2)  // 10  

上面代码的第一个点解释为小数点,第二个点解释为点运算符。为了语义清晰起见,下面我统一采用圆括号的写法。

由于add方法返回的还是数值,所以可以链式运算。

  Number.prototype.subtract = function (x) {    return this - x;  };    (8).add(2).subtract(4)  // 6  

上面代码在Number对象的实例上部署了subtract方法,它可以与add方法链式调用。

如果使用方括号调用属性,写法会很古怪。

  8["add"](2)["subtract"](4)  // 6  

我们还可以部署更复杂的方法。

  Number.prototype.iterate = function () {    var result = [];    for (var i = 0; i <= this; i++) {      result.push(i);    }    return result;  };    (8).iterate()  // [0, 1, 2, 3, 4, 5, 6, 7, 8]  

上面代码在 Number 对象的原型上部署了 iterate 方法,可以将一个数值自动扩展为一个数组。

总之,现在我们可以在数值上直接调用方法了,但是后面一对圆括号看着有点碍眼,有没有可能去掉圆括号呢?也就是说,能不能将下面的表达式

  (8).double().square()  

写成另一种样子?

  (8).double.suqare  

这是可以做到的。

ES5规定,每个对象的属性都有一个取值方法get,用来自定义该属性的读取操作。

  Number.prototype = Object.defineProperty(    Number.prototype, "double", {      get: function (){return (this + this)}     }  );    Number.prototype =  Object.defineProperty(    Number.prototype, "square", {      get: function (){return (this * this)}     }  );  

上面代码在 Number.prototype 上定义了两个属性 double 和 square ,以及它们的取值方法 get 。

因此,在任一数值上,读取这两个属性,就可以写成下面的样子。

  (8).double.square  // 256  

也可以改用方括号运算符。

  8["double"]["square"]  // 256  

(完)

文档信息

2015年2月8日星期日

阮一峰的网络日志

阮一峰的网络日志


强类型 JavaScript 的解决方案

Posted: 07 Feb 2015 09:10 PM PST

JavaScript 是一种弱类型(或称动态类型)语言,即变量的类型是不确定的。

  x = 5; // 5  x = x + 'A'; // '5A'  

上面代码中,变量x起先是一个数值,后来是一个字符串,类型完全由当前的值决定,这就叫弱类型。

弱类型的好处是十分灵活,可以写出非常简洁的代码。但是,对于大型项目来说,强类型更有利,可以降低系统的复杂度,在编译时就发现类型错误,减轻程序员的负担。

一直有人尝试,让 JavaScript 变成强类型语言。在官方最终支持强类型之前,本文介绍三种现在就可用的解决方案。

(题图:摄于花莲,台湾,2012年6月)

一、TypeScript

TypeScript 是微软2012年推出的一种编程语言,属于 JavaScript 的超集,可以编译为 JavaScript 执行。 它的最大特点就是支持强类型和 ES6 Class

首先,安装TypeScript。

  $ npm install -g typescript  

然后,为变量指定类型。

  // greet.ts  function greet(person: string) {    console.log("Hello, " + person);  }    greet([0, 1, 2]);  

上面是文件 greet.ts 的代码,后缀名 ts 表明这是 TypeScript 的代码。函数 greet 的参数,声明类型为字符串,但在调用时,传入了一个数组。

使用 tsc 命令将 ts 文件编译为 js 文件,就会抛出类型不匹配的错误。

  $ tsc greeter.ts  greet.ts(5,9): error TS2345: Argument of type 'number[]'     is not assignable to parameter of type 'string'.  

二、Flowcheck

Flowcheck 是一个轻量级的类型断言库,可以在运行时(runtime)检查变量类型是否正确。

首先,安装Flowcheck。

  $ npm install -g flowcheck  

然后,编写一个声明了变量类型的脚本。

  function sum(a: number, b: number) {    return a + b;  }    sum('hello','world')  

接着,使用下面的命令,将脚本转换为正常的 JavaScript 文件。

  $ browserify -t flowcheck -t [reactify --strip-types] \  input.js -o output.js  

转换后的文件如下。

  var _f = require("flowcheck/assert");    function sum(a, b) {      _f.check(arguments, _f.arguments([_f.number, _f.number]));    return a + b;  }  

可以看到,代码中插入一个断言库。每次运行函数之前,会先执行断言,如果类型不符就报错。

  $ node output.js  // throw new TypeError(message);              ^  TypeError:     Expected an instance of number got "hello",     context: arguments / [number, number] / 0    Expected an instance of number got "world",    context: arguments / [number, number] / 1  

三、Flow

Flow 是 Facebook 在2014年发布的一个类型检查工具,用来检查 React 的源码。

安装命令如下。

  $ npm install --global flow-bin  

如果安装不成功(我就是如此),就需要自己从源码编译了。

Flow 的用法很多,我只举几个例子。前文介绍的两种工具,只能检查声明了类型的变量,而 Flow 可以推断变量类型。

  // hello.js  /* @flow */  function foo(x) {    return x*10;  }  foo("Hello, world!");  

上面是文件 hello.js ,该文件的第一行是注释,表明需要使用 Flow 检查变量类型。

  $ flow check  hello.js:7:5,19: string  This type is incompatible with  /hello.js:4:10,13: number  

运行 flow check 命令,得到报错信息:预期函数 foo 的参数是一个数值,但是实际为一个字符串。

Flow 也支持变量的类型声明。

  /* @flow */  function foo(x: string, y: number): string {    return x.length * y;  }  foo("Hello", 42);  

另一个有趣的功能是,Flow 可以将类型注释(annotation),转为类型声明。

  // annotation.js  /**    @param {number} x    @return {number}   */  function square(x) {    return x * x;  }  square(5);  

运行 flow port 命令,会得到下面的结果。

  $ flow port annotation.js  function square(x: number) : number {     return x * x;   }  

Flow 的更多介绍,可以阅读《Exploring Flow, Facebook's Type Checker for JavaScript》

本文的原始幻灯片点击这里(里面有更多内容)。

(完)

文档信息

2015年2月3日星期二

阮一峰的网络日志

阮一峰的网络日志


图灵访谈

Posted: 02 Feb 2015 07:47 PM PST

上周,我接受了图灵教育访谈,谈了自己的人生经历。

我的目的是卖书,如果你还没买《如何变得有思想》,请考虑一下吧。只有出版社赚到钱,我才有机会出下一本。

下面是书面访谈的原文。

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

Q - 图灵,A - 阮一峰

Q: 请介绍一下自己。

A: 我是70后,在上海出生和长大,大学糊里糊涂读了经济学。工作了几年,又去读了世界经济的研究生,毕业后,在上海一所本地高校当了老师,教财经类的课程。最近,去了支付宝的前端团队,在玉伯负责的"体验技术部"工作,目前主要从事JavaScript和Node.js的开发。

我翻译了《软件随想录》《黑客与画家》,出版了技术专著《ECMAScript 6入门》和博客文集《如何变得有思想》

Q: 你是怎么接触到互联网的?

A: 2000年前后,我第一次上网,一下子就着迷了。那时候,只有图书馆的机房能上网,除了吃饭,我从早上8点开馆一直待到晚上10点闭馆,只做三件事:在化云坊fanso.com听歌,在chinaren.com下棋,在水木清华BBS灌水。

Q: 你是怎么开始学习计算机的?

A: 我对计算机一直有兴趣,但从没想过去编程。因为我是文科生,学校里只教Word和Excel,对计算机没概念。大学快毕业了,为了打游戏和上网,才买了第一台个人电脑,CPU是奔腾133MHz。研究生时,我想考计算机二级C语言,买了指定教材,但发现看不懂。

那个时候是互联网泡沫,很容易申请免费的个人主页空间。我在网易(yeah.net)和亿唐(etang.com)申请了个人主页,从此开始接触到网页制作。中国青年出版社引进的台湾"旗标计算机丛书",对我帮助很大。那套书图文并茂,通俗易懂,我从那里学会了最简单的HTML语言。

Q: 你后来怎么学会编程?

A: 学了HTML以后,发现网页要做得漂亮,必须懂CSS。学了CSS以后,发现制作动态网页,必须懂一门计算机语言,就去学了asp。很快发现,asp不如php功能强,就去学了php和数据库MySQL。用了开源数据库,就离不开了Linux系统了。这个时候,又重新去学C语言,这一次学会了。

Q: 你是怎么开始写博客的?

A: 2003年以前,我制作网页都是使用微软的FrontPage ,需要一张张地制作,非常麻烦。后来听说博客软件可以动态生成网页、更换样式、站内搜索,就很动心。

我最早使用的博客软件是asp语言写的,忘了叫什么名字。2003年底,我看到了王建硕的博客,非常震惊,原来博客可以玩得这么精彩。他用的软件是MovableType,我照他的样子搭建了一个,一直用到现在。遗憾的是,王建硕已经换成WordPress了,MovableType也停止更新了,国内还用这个软件的人屈指可数。互联网时代就是这样,一切变得太快了,我觉得自己是个怀旧的人。

Q: 你的博客是怎么出名的?

A: 我其实不知道,一开始我只把博客当作私人园地,并没有想到有人会来看。但是,不知道是否错觉,Google对于MovableType生成的网页,会给很高的权重。我的很多文章排在Google的第一页,很多人这样发现了我的博客。

我的文风可能也有点帮助。我喜欢用口语,喜欢用短句,别人比较容易看懂。另外,比我水平更高、写得更好的人,很多都放弃写博客了,只有我还在写。所以,我的体会是,如果你认认真真写,认认真真排版,保证别人可以看懂,还时不时插入一些漂亮的插图,或者一段趣闻轶事,你坚持这么做,就可以出名。

Q: 你的博客早期都是谈文学的,怎么会转变为一个技术博客?

A: 大学里,我喜欢读小说和传记,曾经以为社会科学是自己一辈子的专业方向。后来发现,在中国搞社会科学没前途,除非愿意给政策背书。那时,我还喜欢写社会和政治评论,每次都有一大堆互相争吵、人身攻击的留言,无法得到任何共识。我还接到过有关部门的电话,要求删除文章

最终,我觉得文学、政治学、经济学、社会学之类的学科,都没多大用处,就渐渐不想走这条路了。它们也不是真的没用,就是在中国不行,改变不了现实,只会让自己走入绝境。在我看来,走技术这条路至少有一个好处。我曾经在一篇文章里写过:"(在国内,)如果你想不撒谎、不干坏事、并且被公正地对待,那么可能你只能去编程了。"

Q: 很多人批评你的技术文章,错误非常多,你怎么看?

A: 我一直是外行,从来不敢说自己是专家。对我来说,博客首先是一种知识管理工具,其次才是传播工具。我的技术文章,主要用来整理我还不懂的知识。我只写那些我还没有完全掌握的东西,那些我精通的东西,往往没有动力写。炫耀从来不是我的动机,好奇才是。

当然,我肯定只写那些我觉得对的东西,尽量对读者负责。但是没法保证,我觉得对的东西就是对的。网站流量越来越大,对我的争议也越来越多,好几次我都在文章前加上说明,这是初学者的笔记。我还会以这种方式写下去。我希望自己永远都能保持,那种不怕丢面子,敢于当众说蠢话的勇气。

Q: 你近期在忙什么?

A: 最近一两年,我的关注重点都将是Node(io.js)。我觉得,Node很适合作为前后端之间的中间层。我希望用Node、PostgreSQL、MongoDB、Nignx实现一个网站,会是一个开源项目。

如果有时间,我很希望能学一下Go语言,用它来写后端实现。我也很想学函数式编程,它带来了全新的思考方式,而且非常优雅。

[说明] 摄于艾西拉,摩洛哥,2014年8月

(完)

文档信息

2015年2月1日星期日

阮一峰的网络日志

阮一峰的网络日志


MVC,MVP 和 MVVM 的图示

Posted: 31 Jan 2015 07:00 PM PST

复杂的软件必须有清晰合理的架构,否则无法开发和维护。

MVC(Model-View-Controller)是最常见的软件架构之一,业界有着广泛应用。它本身很容易理解,但是要讲清楚,它与衍生的 MVP 和 MVVM 架构的区别就不容易了。

昨天晚上,我读了《Scaling Isomorphic Javascript Code》,突然意识到,它们的区别非常简单。我用几段话,就可以说清。

(题图:摄于瓦伦西亚,西班牙,2014年8月)

一、MVC

MVC模式的意思是,软件可以分成三个部分。

  • 视图(View):用户界面。
  • 控制器(Controller):业务逻辑
  • 模型(Model):数据保存

各部分之间的通信方式如下。

  1. View 传送指令到 Controller
  2. Controller 完成业务逻辑后,要求 Model 改变状态
  3. Model 将新的数据发送到 View,用户得到反馈

所有通信都是单向的。

二、互动模式

接受用户指令时,MVC 可以分成两种方式。一种是通过 View 接受指令,传递给 Controller。

另一种是直接通过controller接受指令。

三、实例:Backbone

实际项目往往采用更灵活的方式,以 Backbone.js 为例。

1. 用户可以向 View 发送指令(DOM 事件),再由 View 直接要求 Model 改变状态。

2. 用户也可以直接向 Controller 发送指令(改变 URL 触发 hashChange 事件),再由 Controller 发送给 View。

3. Controller 非常薄,只起到路由的作用,而 View 非常厚,业务逻辑都部署在 View。所以,Backbone 索性取消了 Controller,只保留一个 Router(路由器) 。

四、MVP

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。

1. 各部分之间的通信,都是双向的。

2. View 与 Model 不发生联系,都通过 Presenter 传递。

3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

五、MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。AngularEmber 都采用这种模式。

(完)

文档信息