2015年3月31日星期二

阮一峰的网络日志

阮一峰的网络日志


React 入门实例教程

Posted: 31 Mar 2015 12:10 AM PDT

现在最热门的前端框架,毫无疑问是 React

上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑。

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。

这个项目本身也越滚越大,从最早的UI引擎变成了一整套前后端通吃的 Web App 解决方案。衍生的 React Native 项目,目标更是宏伟,希望用写 Web App 的方式去写 Native App。如果能够实现,整个互联网行业都会被颠覆,因为同一组人只需要写一次 UI ,就能同时运行在服务器、浏览器和手机(参见《也许,DOM 不是答案》)。

既然 React 这么热门,看上去充满希望,当然应该好好学一下。从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目。但是,好的 React 教程却不容易找到,这一方面因为这项技术太新,刚刚开始走红,大家都没有经验,还在摸索之中;另一方面因为 React 本身还在不断变动,API 一直在调整,至今没发布1.0版。

我学习 React 时,就很苦恼。有的教程讨论一些细节问题,对入门没帮助;有的教程写得不错,但比较短,无助于看清全貌。我断断续续学了几个月,看过二十几篇教程,在这个过程中,将对自己有帮助的 Demo 都收集下来,做成了一个库 React Demos

下面,我就根据这个库,写一篇全面又易懂的 React 入门教程。你只需要跟着每一个 Demo 做一遍,就能初步掌握 React 。当然,前提是你必须拥有基本 JavaScript 和 DOM 知识,但是你读完就会发现,React 所要求的预备知识真的很少。

零、安装

React 的安装包,可以到官网下载。不过,React Demos 已经自带 React 源码,不用另外安装,只需把这个库拷贝到你的硬盘就行了。

 $ git clone git@github.com:ruanyf/react-demos.git 

如果你没安装 git, 那就直接下载 zip 压缩包

下面要讲解的10个例子在各个 Demo 子目录,每个目录都有一个 index.html 文件,在浏览器打开这个文件(大多数情况下双击即可),就能立刻看到效果。

需要说明的是,React 可以在浏览器运行,也可以在服务器运行,但是本教程只涉及浏览器。一方面是为了尽量保持简单,另一方面 React 的语法是一致的,服务器的用法与浏览器差别不大。Demo11 是服务器首屏渲染的例子,有兴趣的朋友可以自己去看源码。

一、HTML 模板

使用 React 的网页源码,结构大致如下。

 <!DOCTYPE html> <html>   <head>     <script src="../build/react.js"></script>     <script src="../build/JSXTransformer.js"></script>   </head>   <body>     <div id="example"></div>     <script type="text/jsx">       // ** Our code goes here! **     </script>   </body> </html> 

上面代码有两个地方需要注意。首先,最后一个 script 标签的 type 属性为 text/jsx 。这是因为 React 独有的 JSX 语法,跟 JavaScript 不兼容。凡是使用 JSX 的地方,都要加上 type="text/jsx" 。

其次,React 提供两个库: react.js 和 JSXTransformer.js ,它们必须首先加载。其中,JSXTransformer.js 的作用是将 JSX 语法转为 JavaScript 语法。这一步很消耗时间,实际上线的时候,应该将它放到服务器完成。

 $ jsx src/ build/ 

上面命令可以将 src 子目录的 js 文件进行语法转换,转码后的文件全部放在 build 子目录。

二、React.render()

React.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。

 React.render(   <h1>Hello, world!</h1>,   document.getElementById('example') ); 

上面代码将一个 h1 标题,插入 example 节点(查看 demo01),运行结果如下。

三、JSX 语法

上一节的代码, HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写(查看 Demo02 )。

 var names = ['Alice', 'Emily', 'Kate'];  React.render(   <div>   {     names.map(function (name) {       return <div>Hello, {name}!</div>     })   }   </div>,   document.getElementById('example') ); 

上面代码体现了 JSX 的基本语法规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析。上面代码的运行结果如下。

JSX 允许直接在模板插入 JavaScript 变量。如果这个变量是一个数组,则会展开这个数组的所有成员(查看 demo03 )。

 var arr = [   <h1>Hello world!</h1>,   <h2>React is awesome</h2>, ]; React.render(   <div>{arr}</div>,   document.getElementById('example') ); 

上面代码的arr变量是一个数组,结果 JSX 会把它的所有成员,添加到模板,运行结果如下。

四、组件

React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。React.createClass 方法就用于生成一个组件类(查看 demo04)。

 var HelloMessage = React.createClass({   render: function() {     return <h1>Hello {this.props.name}</h1>;   } });  React.render(   <HelloMessage name="John" />,   document.getElementById('example') ); 

上面代码中,变量 HelloMessage 就是一个组件类。模板插入 <HelloMessage /> 时,会自动生成 HelloMessage 的一个实例(下文的"组件"都指组件类的实例)。所有组件类都必须有自己的 render 方法,用于输出组件。

组件的用法与原生的 HTML 标签完全一致,可以任意加入属性,比如 <HelloMessage name="John" /> ,就是 HelloMessage 组件加入一个 name 属性,值为 John。组件的属性可以在组件类的 this.props 对象上获取,比如 name 属性就可以通过 this.props.name 读取。上面代码的运行结果如下。

添加组件属性,有一个地方需要注意,就是 class 属性需要写成 className ,for 属性需要写成 htmlFor ,这是因为 class 和 for 是 JavaScript 的保留字。

五、this.props.children

this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点(查看 demo05)。

 var NotesList = React.createClass({   render: function() {     return (       <ol>       {         this.props.children.map(function (child) {           return <li>{child}</li>         })       }       </ol>     );   } });  React.render(   <NotesList>     <span>hello</span>     <span>world</span>   </NotesList>,   document.body ); 

上面代码的 NoteList 组件有两个 span 子节点,它们都可以通过 this.props.children 读取,运行结果如下。

这里需要注意,只有当子节点多余1个时,this.props.children 才是一个数组,否则是不能用 map 方法的, 会报错。

六、React.findDOMNode()

组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。

但是,有时需要从组件获取真实 DOM 的节点,这时就要用到 React.findDOMNode 方法(查看 demo06 )。

 var MyComponent = React.createClass({   handleClick: function() {     React.findDOMNode(this.refs.myTextInput).focus();   },   render: function() {     return (       <div>         <input type="text" ref="myTextInput" />         <input type="button" value="Focus the text input" onClick={this.handleClick} />       </div>     );   } });  React.render(   <MyComponent />,   document.getElementById('example') ); 

上面代码中,组件 MyComponent 的子节点有一个文本输入框,用于获取用户的输入。这时就必须获取真实的 DOM 节点,虚拟 DOM 是拿不到用户输入的。为了做到这一点,文本输入框必须有一个 ref 属性,然后 this.refs.[refName] 就指向这个虚拟 DOM 的子节点,最后通过 React.findDOMNode 方法获取真实 DOM 的节点。

需要注意的是,由于 React.findDOMNode 方法获取的是真实 DOM ,所以必须等到虚拟 DOM 插入文档以后,才能使用这个方法,否则会返回 null 。上面代码中,通过为组件指定 Click 事件的回调函数,确保了只有等到真实 DOM 发生 Click 事件之后,才会调用 React.findDOMNode 方法。

React 组件支持很多事件,除了 Click 事件以外,还有 KeyDown 、Copy、Scroll 等,完整的事件清单请查看官方文档

七、this.state

组件免不了要与用户互动,React 的一大创新,就是将组件看成是一个状态机,一开始有一个初始状态,然后用户互动,导致状态变化,从而触发重新渲染 UI (查看 demo07 )。

 var LikeButton = React.createClass({   getInitialState: function() {     return {liked: false};   },   handleClick: function(event) {     this.setState({liked: !this.state.liked});   },   render: function() {     var text = this.state.liked ? 'like' : 'haven\'t liked';     return (       <p onClick={this.handleClick}>         You {text} this. Click to toggle.       </p>     );   } });  React.render(   <LikeButton />,   document.getElementById('example') ); 

上面代码是一个 LikeButton 组件,它的 getInitialState 方法用于定义初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。当用户点击组件,导致状态变化,this.setState 方法就修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。

由于 this.props 和 this.state 都用于描述组件的特性,可能会产生混淆。一个简单的区分方法是,this.props 表示那些一旦定义,就不再改变的特性,而 this.state 是会随着用户互动而产生变化的特性。

八、表单

用户在表单填入的内容,属于用户跟组件的互动,所以不能用 this.props 读取(查看 demo08 )。

 var Input = React.createClass({   getInitialState: function() {     return {value: 'Hello!'};   },   handleChange: function(event) {     this.setState({value: event.target.value});   },   render: function () {     var value = this.state.value;     return (       <div>         <input type="text" value={value} onChange={this.handleChange} />         <p>{value}</p>       </div>     );   } });  React.render(<Input/>, document.body); 

上面代码中,文本输入框的值,不能用 this.props.value 读取,而要定义一个 onChange 事件的回调函数,通过 event.target.value 读取用户输入的值。textarea 元素、select元素、radio元素都属于这种情况,更多介绍请参考官方文档

九、组件的生命周期

组件的生命周期分成三个状态:

  • Mounting:已插入真实 DOM
  • Updating:正在被重新渲染
  • Unmounting:已移出真实 DOM

React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。

  • componentWillMount()
  • componentDidMount()
  • componentWillUpdate(object nextProps, object nextState)
  • componentDidUpdate(object prevProps, object prevState)
  • componentWillUnmount()

此外,React 还提供两种特殊状态的处理函数。

  • componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
  • shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用

这些方法的详细说明,可以参考官方文档。下面是一个例子(查看 demo09 )。

 var Hello = React.createClass({   getInitialState: function () {     return {       opacity: 1.0     };   },    componentDidMount: function () {     this.timer = setInterval(function () {       var opacity = this.state.opacity;       opacity -= .05;       if (opacity < 0.1) {         opacity = 1.0;       }       this.setState({         opacity: opacity       });     }.bind(this), 100);   },    render: function () {     return (       <div style={{opacity: this.state.opacity}}>         Hello {this.props.name}       </div>     );   } });  React.render(   <Hello name="world"/>,   document.body ); 

上面代码在hello组件加载以后,通过 componentDidMount 方法设置一个定时器,每隔100毫秒,就重新设置组件的透明度,从而引发重新渲染。

另外,组件的style属性的设置方式也值得注意,不能写成

 style="opacity:{this.state.opacity};" 

而要写成

 style={{opacity: this.state.opacity}} 

这是因为 React 组件样式是一个对象,所以第一重大括号表示这是 JavaScript 语法,第二重大括号表示样式对象。

十、Ajax

组件的数据来源,通常是通过 Ajax 请求从服务器获取,可以使用 componentDidMount 方法设置 Ajax 请求,等到请求成功,再用 this.setState 方法重新渲染 UI (查看 demo10 )。

 var UserGist = React.createClass({   getInitialState: function() {     return {       username: '',       lastGistUrl: ''     };   },    componentDidMount: function() {     $.get(this.props.source, function(result) {       var lastGist = result[0];       if (this.isMounted()) {         this.setState({           username: lastGist.owner.login,           lastGistUrl: lastGist.html_url         });       }     }.bind(this));   },    render: function() {     return (       <div>         {this.state.username}'s last gist is         <a href={this.state.lastGistUrl}>here</a>.       </div>     );   } });  React.render(   <UserGist source="https://api.github.com/users/octocat/gists" />,   document.body ); 

上面代码使用 jQuery 完成 Ajax 请求,这是为了便于说明。React 没有任何依赖,完全可以使用其他库。

十一、参考链接

  1. React's official site
  2. React's official examples
  3. React (Virtual) DOM Terminology, by Sebastian Markbåge
  4. The React Quick Start Guide, by Jack Callister
  5. Learning React.js: Getting Started and Concepts, by Ken Wheeler
  6. Getting started with React, by Ryan Clark
  7. React JS Tutorial and Guide to the Gotchas, by Justin Deal
  8. React Primer, by Binary Muse
  9. jQuery versus React.js thinking, by zigomir

(完)

文档信息

2015年3月13日星期五

阮一峰的网络日志

阮一峰的网络日志


使用 Make 构建网站

Posted: 13 Mar 2015 12:27 AM PDT

网站开发正变得越来越专业,涉及到各种各样的工具和流程,迫切需要构建自动化。

所谓"构建自动化",就是指使用构建工具,自动实现"从源码到网页"的开发流程。这有利于提高开发效率、改善代码质量。

本文介绍如何使用make命令,作为网站的构建工具。以下内容既是make语法的实例,也是网站构建的实战教程。你完全可以将代码略作修改,拷贝到自己的项目。

(题图:国家考古博物馆,西班牙,摄于2014年8月)

一、Make的优点

首先解释一下,为什么要用Make。

目前,网站项目(尤其是Node.js项目)有三种构建方案。

我觉得,make是大型项目的首选方案。npm run可以认为是make的简化形式,只适用于简单项目,而Grunt、Gulp那样的工具,有很多问题。

(1)插件问题

Grunt和Gulp的操作,都由插件完成。即使是文件改名这样简单的任务,都要写插件,相当麻烦。而Make是直接调用命令行,根本不用担心找不到插件。

(2)兼容性问题

插件的版本,必须与Grunt和Gulp的版本匹配,还必须与对应的命令行程序匹配。比如,grunt-contrib-jshint插件现在是0.11.0版,对应Grunt 0.4.5版和JSHint 2.6.0版。万一Grunt和JSHint升级,而插件没有升级,就有可能出现兼容性问题。Make是直接调用JSHint,不存在这个问题。

(3)语法问题

Grunt和Gulp都有自己的语法,并不容易学,尤其是Grunt,语法很罗嗦,很难一眼看出来代码的意图。当然,make也不容易学,但它有复用性,学会了还可以用在其他场合。

(4)功能问题

make已经使用了几十年,全世界无数的大项目都用它构建,早就证明非常可靠,各种情况都有办法解决,前人累积的经验和资料也非常丰富。相比之下,Grunt和Gulp的历史都不长,使用范围有限,目前还没有出现它们能做、而make做不到的任务。

基于以上理由,我看好make。

二、常见的构建任务

下面是一些常见的网站构建任务。

  • 检查语法
  • 编译模板
  • 转码
  • 合并
  • 压缩
  • 测试
  • 删除

这些任务用到 JSHinthandlebarsCoffeeScriptuglifyjsmocha 等工具。对应的package.json文件如下。

 "devDependencies": {     "coffee-script": "~1.9.1",     "handlebars": "~3.0.0",     "jshint": "^2.6.3",     "mocha": "~2.2.1",     "uglify-js": "~2.4.17" } 

我们来看看,Make 命令怎么完成这些构建任务。

三、Makefile的通用配置

开始构建之前,要编写Makefile文件。它是make命令的配置文件。所有任务的构建规则,都写在这个文件(参见《Make 命令教程》)。

首先,写入两行通用配置。

 PATH  := node_modules/.bin:$(PATH) SHELL := /bin/bash 

上面代码的PATH和SHELL都是BASH变量。它们被重新赋值。

PATH变量重新赋值为,优先在 nodemodules/.bin 目录寻找命令。这是因为(当前项目的)node模块,会在 nodemodules/.bin 目录设置一个符号链接。PATH变量指向这个目录以后,调用各种命令就不用写路径了。比如,调用JSHint,就不用写 ~/node_modules/.bin/jshint ,只写 jshint 就行了。

SHELL变量指定构建环境使用BASH。

四、检查语法错误

第一个任务是,检查源码有没有语法错误。

 js_files = $(shell find ./lib -name '*.js')  lint: $(js_files)     jshint $? 

上面代码中,shell函数调用find命令,找出lib目录下所有js文件,保存在变量js_files。然后,就可以用jshint检查这些文件。

使用时调用下面的命令。

 $ make lint 

五、模板编译

第二个任务是编译模板。假定模板都在templates目录,需要编译为build目录下的templates.js文件。

 build/templates.js: templates/*.handlebars     mkdir -p $(dir $@)     handlebars templates/*.handlebars > $@  template: build/templates.js 

上面代码查看build目录是否存在,如果不存在就新建一个。dir函数用于取出构建目标的路径名(build),内置变量$@代表构建目标(build/templates.js)。

使用时调用下面的命令。

 $ make template 

六、Coffee脚本转码

第三个任务是,将CofferScript脚本转为JavaScript脚本。

 source_files := $(wildcard lib/*.coffee) build_files  := $(source_files:lib/%.coffee=build/%.js)  build/%.js: lib/%.coffee     coffee -co $(dir $@) $<  coffee: $(build_files) 

上面代码中,首先获取所有的Coffee脚本文件,存放在变量sourcefiles,函数wildcard用来扩展通配符。然后,将变量sourcefiles中的coffee文件名,替换成js文件名,即 lib/x.coffee 替换成 build/x.js 。

使用时调用下面的命令。

 $ make coffee 

七、合并文件

使用cat命令,合并多个文件。

  JS_FILES := $(wildcard build/*.js) OUTPUT := build/bundle.js  concat: $(JS_FILES)     cat $^ > $(OUTPUT) 

使用时调用下面的命令。

 $ make concat 

八、压缩JavaScript脚本

将所有JavaScript脚本,压缩为build目录下的app.js。

 app_bundle := build/app.js  $(app_bundle): $(build_files) $(template_js)     uglifyjs -cmo $@ $^  min: $(app_bundle) 

使用时调用下面的命令。

 $ make min 

还有另一种写法,可以另行指定压缩工具。

 UGLIFY ?= uglify  $(app_bundle): $(build_files) $(template_js)     $(UGLIFY) -cmo $@ $^  

上面代码将压缩工具uglify放在变量UGLIFY。注意,变量的赋值符是 ?= ,表示这个变量可以被命令行参数覆盖。

调用时这样写。

 $ make UGLIFY=node_modules/.bin/jsmin min 

上面代码,将jsmin命令给变量UGLIFY,压缩时就会使用jsmin命令。

九、删除临时文件

构建结束前,删除所有临时文件。

 clean:     rm -rf build 

使用时调用下面的命令。

 $ make clean 

十、测试

假定测试工具是mocha,所有测试用例放在test目录下。

 test: $(app_bundle) $(test_js)     mocha 

当脚本和测试用例都存在,上面代码就会执行mocha。

使用时调用下面的命令。

 $ make test 

十一、多任务执行

构建过程需要一次性执行多个任务,可以指定一个多任务目标。

 build: template concat min clean 

上面代码将build指定为执行模板编译、文件合并、脚本压缩、删除临时文件四个任务。

使用时调用下面的命令。

 $ make build 

如果这行规则在Makefile的最前面,执行时可以省略目标名。

 $ make 

通常情况下,make一次执行一个任务。如果任务都是独立的,互相没有依赖关系,可以用参数 -j 指定同时执行多个任务。

 $ make -j build 

十二、声明伪文件

最后,为了防止目标名与现有文件冲突,显式声明哪些目标是伪文件。

 .PHONY: lint template coffee concat min test clean build 

十三、Makefile文件示例

下面是两个简单的Makefile文件,用来补充make命令的其他构建任务。

实例一。

 PROJECT = "My Fancy Node.js project"  all: install test server  test: ;@echo "Testing ${PROJECT}....."; \     export NODE_PATH=.; \     ./node_modules/mocha/bin/mocha;  install: ;@echo "Installing ${PROJECT}....."; \     npm install  update: ;@echo "Updating ${PROJECT}....."; \     git pull --rebase; \     npm install  clean : ;     rm -rf node_modules  .PHONY: test server install clean update 

实例二。

 all: build-js build-css  build-js:    browserify -t brfs src/app.js > site/app.js  build-css:   stylus src/style.styl > site/style.css  .PHONY build-js build-css 

十四、参考链接

(完)

文档信息

2015年2月22日星期日

阮一峰的网络日志

阮一峰的网络日志


也许,DOM 不是答案

Posted: 21 Feb 2015 08:20 PM PST

有一个词"手机网站"(mobile web),指供手机浏览的网站,但它是不存在的。

人们提到"移动互联网"的时候,其实专指另外一样东西:手机App。

一、Web App vs. Native App

比起手机App,网站有一些明显的优点。

  • 跨平台:所有系统都能运行
  • 免安装:打开浏览器,就能使用
  • 快速部署:升级只需在服务器更新代码
  • 超链接:可以与其他网站互连,可以被搜索引擎检索

但是,现实是怎样呢?

(1)体验差。手机App的操作流畅性,远超网站。

(2)业界不支持。所有公司的移动端开发重点,几乎都是原生app。

(3)用户不在乎。大多数用户都选择使用手机app,而不是网站。

如果将来有一天,Web app会成为主流,一定有一个前提,那就是它的性能可以赶上Native app。

二、为什么Web app有性能瓶颈?

Web app输给Native app的地方,不是界面(UI),而是操作性能。主要是互动(interaction)和动画(animation)这两方面,会出现卡顿(jank),用户会感觉到明显的时滞,有时简直慢得难以忍受。

Web app的性能瓶颈,主要有以下原因。

(1)Web基于DOM,而DOM很慢。浏览器打开网页时,需要解析文档,在内存中生成DOM结构,如果遇到复杂的文档,这个过程是很慢的。可以想象一下,如果网页上有上万个、甚至几十万个形状(不管是图片或CSS),生成DOM需要多久?更不要提与其中某一个形状互动了。

(2)DOM拖慢JavaScript。所有的DOM操作都是同步的,会堵塞浏览器。JavaScript操作DOM时,必须等前一个操作结束,才能执行后一个操作。只要一个操作有卡顿,整个网页就会短暂失去响应。浏览器重绘网页的频率是60FPS(即16毫秒/帧),JavaScript做不到在16毫秒内完成DOM操作,因此产生了跳帧。用户体验上的不流畅、不连贯就源于此。

(3)网页是单线程的。现在的浏览器对于每个网页,只用一个线程处理。所有工作都在这一个线程上完成,包括布局、渲染、JavaScript执行、图像解码等等,怎么可能不慢?

(4)网页没有硬件加速。网页都是由CPU处理的,没用GPU进行图形加速。

上面这些原因,对于PC还不至于造成严重的性能问题,但是手机的硬件资源相对有限,用户互动又相对频繁,结果跟Native app一比,就完全落在了下风。

三、FlipBoard的解决方案

FlipBoard原本是一个手机App,最近开始部署Web版本,结果就遇到了上面的问题:Web版的体验不佳。

上周,他们将解决方案公布在网站上,结果引起了业界轰动,因为这是一个史无前例的解决方案:

---- 他们没有使用DOM,而是将整个网站用canvas输出!

你可以用手机打开flipboard.com,体验一下,看看跟Native app有没有差别。如果你没有帐号,可以直接打开这里这里

这个方案的出发点是这样的:如果将网页变成了一个个canvas,用户就等于在跟图片互动,这样就绕开了DOM,降低了操作时滞。而且,canvas可以被硬件加速,这样就提高了性能。具体的技术细节,可以参考原文。canvas的转化基于React框架实现,FlipBoard 开发了一个专门的库React-canvas,已经开源。

这个方案引发了很多争议(这里这里),主要是canvas只是一个位图,本身没有语义,如果要在它上面实现UI,等于HTML语言已有的东西都要再发明一遍,比如如何实现超链接、如何实现CSS效果等等。一些最简单的东西都变得很麻烦,因为canvas不是自适应的(responsive),文字在哪里断行,都要自己计算,而且用户也无法选中文本。另外,怎么让搜索引擎检索网页,解决起来也不是很容易。

但是不管怎样,这是一个有意义的尝试。

四、未来的路

James Long对FlipBoard的尝试,写了一篇评论《Radical Statements about the Mobile Web》。本文就受到了那篇文章的启发。

在文中,James Long对未来的Web app提出了几点预测,我认为很值得分享。

(1)多线程浏览器。每个网页应该由多个线程进行处理,主线程只负责布局和渲染,而且应该在16毫秒内完成,JavaScript由worker线程执行,这样就不会发生堵塞了。Mozilla正在开发的Servo就是这样一个项目。

(2)DOM的异步操作。JavaScript对DOM的操作不再是同步的,而是触发后,交给Event Loop机制进行监听。

(3)非DOM方案。浏览器不再将网页处理成DOM结构,而是变为其他结构。React的Virtual DOM方案就是这一类的尝试,还有更激进的方案,比如用数据库取代DOM。

(完)

文档信息

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 项目。

(完)

文档信息