2015年12月24日星期四

阮一峰的网络日志

阮一峰的网络日志


Git 协作流程

Posted: 23 Dec 2015 10:15 PM PST

Git 作为一个源码管理系统,不可避免涉及到多人协作。

协作必须有一个规范的流程,让大家有效地合作,使得项目井井有条地发展下去。"协作流程"在英语里,叫做"workflow"或者"flow",原意是水流,比喻项目像水流那样,顺畅、自然地向前流动,不会发生冲击、对撞、甚至漩涡。

本文介绍三种广泛使用的协作流程:

  • Git flow
  • Github flow
  • Gitlab flow

如果你对Git还不是很熟悉,可以先阅读下面的文章。

一、功能驱动

本文的三种协作流程,有一个共同点:都采用"功能驱动式开发"(Feature-driven development,简称FDD)。

它指的是,需求是开发的起点,先有需求再有功能分支(feature branch)或者补丁分支(hotfix branch)。完成开发后,该分支就合并到主分支,然后被删除。

二、Git flow

最早诞生、并得到广泛采用的一种协作流程,就是Git flow

2.1 特点

它最主要的特点有两个。

首先,项目存在两个长期分支。

  • 主分支master
  • 开发分支develop

前者用于存放对外发布的版本,任何时候在这个分支拿到的,都是稳定的分布版;后者用于日常开发,存放最新的开发版。

其次,项目存在三种短期分支。

  • 功能分支(feature branch)
  • 补丁分支(hotfix branch)
  • 预发分支(release branch)

一旦完成开发,它们就会被合并进developmaster,然后被删除。

Git flow 的详细介绍,请阅读我翻译的中文版《Git 分支管理策略》

2.2 评价

Git flow的优点是清晰可控,缺点是相对复杂,需要同时维护两个长期分支。大多数工具都将master当作默认分支,可是开发是在develop分支进行的,这导致经常要切换分支,非常烦人。

更大问题在于,这个模式是基于"版本发布"的,目标是一段时间以后产出一个新版本。但是,很多网站项目是"持续发布",代码一有变动,就部署一次。这时,master分支和develop分支的差别不大,没必要维护两个长期分支。

三、Github flow

Github flow 是Git flow的简化版,专门配合"持续发布"。它是 Github.com 使用的协作流程。

3.1 流程

它只有一个长期分支,就是master,因此用起来非常简单。

官方推荐的流程如下。

第一步:根据需求,从master拉出新分支,不区分功能分支或补丁分支。

第二步:新分支开发完成后,或者需要讨论的时候,就向master发起一个pull reqest(简称PR)。

第三步:Pull Request既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。对话过程中,你还可以不断提交代码。

第四步:你的Pull Request被接受,合并进master,重新部署后,原来你拉出来的那个分支就被删除。(先部署再合并也可。)

3.2 评价

Github flow 的最大优点就是简单,对于"持续发布"的产品,可以说是最合适的流程。

问题在于它的假设:master分支的更新与产品的发布是一致的。也就是说,master分支的最新代码,默认就是当前的线上代码。

可是,有些时候并非如此,代码合并进入master分支,并不代表它就能立刻发布。比如,苹果商店的APP提交审核以后,等一段时间才能上架。这时,如果还有新的代码提交,master分支就会与刚发布的版本不一致。另一个例子是,有些公司有发布窗口,只有指定时间才能发布,这也会导致线上版本落后于master分支。

上面这种情况,只有master一个主分支就不够用了。通常,你不得不在master分支以外,另外新建一个production分支跟踪线上版本。

四、Gitlab flow

Gitlab flow 是 Git flow 与 Github flow 的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是 Gitlab.com 推荐的做法。

4.1 上游优先

Gitlab flow 的最大原则叫做"上游优先"(upsteam first),即只存在一个主分支master,它是所有其他分支的"上游"。只有上游分支采纳的代码变化,才能应用到其他分支。

Chromium项目就是一个例子,它明确规定,上游分支依次为:

  1. Linus Torvalds的分支
  2. 子系统(比如netdev)的分支
  3. 设备厂商(比如三星)的分支

4.2 持续发布

Gitlab flow 分成两种情况,适应不同的开发流程。

对于"持续发布"的项目,它建议在master分支以外,再建立不同的环境分支。比如,"开发环境"的分支是master,"预发环境"的分支是pre-production,"生产环境"的分支是production

开发分支是预发分支的"上游",预发分支又是生产分支的"上游"。代码的变化,必须由"上游"向"下游"发展。比如,生产环境出现了bug,这时就要新建一个功能分支,先把它合并到master,确认没有问题,再cherry-pickpre-production,这一步也没有问题,才进入production

只有紧急情况,才允许跳过上游,直接合并到下游分支。

4.3 版本发布

对于"版本发布"的项目,建议的做法是每一个稳定版本,都要从master分支拉出一个分支,比如2-3-stable2-4-stable等等。

以后,只有修补bug,才允许将代码合并到这些分支,并且此时要更新小版本号。

五、一些小技巧

5.1 Pull Request

功能分支合并进master分支,必须通过Pull Request(Gitlab里面叫做 Merge Request)。

前面说过,Pull Request本质是一种对话机制,你可以在提交的时候,@相关人员团队,引起他们的注意。

5.2 Protected branch

master分支应该受到保护,不是每个人都可以修改这个分支,以及拥有审批 Pull Request 的权力。

GithubGitlab 都提供"保护分支"(Protected branch)这个功能。

5.3 Issue

Issue 用于 Bug追踪和需求管理。建议先新建 Issue,再新建对应的功能分支。功能分支总是为了解决一个或多个 Issue。

功能分支的名称,可以与issue的名字保持一致,并且以issue的编号起首,比如"15-require-a-password-to-change-it"。

开发完成后,在提交说明里面,可以写上"fixes #14"或者"closes #67"。Github规定,只要commit message里面有下面这些动词 + 编号,就会关闭对应的issue。

  • close
  • closes
  • closed
  • fix
  • fixes
  • fixed
  • resolve
  • resolves
  • resolved

这种方式还可以一次关闭多个issue,或者关闭其他代码库的issue,格式是username/repository#issue_number

Pull Request被接受以后,issue关闭,原始分支就应该删除。如果以后该issue重新打开,新分支可以复用原来的名字。

5.4 Merge节点

Git有两种合并:一种是"直进式合并"(fast forward),不生成单独的合并节点;另一种是"非直进式合并"(none fast-forword),会生成单独节点。

前者不利于保持commit信息的清晰,也不利于以后的回滚,建议总是采用后者(即使用--no-ff参数)。只要发生合并,就要有一个单独的合并节点。

5.5 Squash 多个commit

为了便于他人阅读你的提交,也便于cherry-pick或撤销代码变化,在发起Pull Request之前,应该把多个commit合并成一个。(前提是,该分支只有你一个人开发,且没有跟master合并过。)

这可以采用rebase命令附带的squash操作,具体方法请参考我写的《Git 使用规范流程》

(完)

文档信息

2015年12月15日星期二

阮一峰的网络日志

阮一峰的网络日志


有没有安全的工作?

Posted: 14 Dec 2015 06:12 PM PST

如果你经常使用互联网,可能知道有一种东西叫做Flash。

它是一种软件,用来制作网页游戏、动画,以及视频播放器。只要观看网络视频,基本都会用到它。

七八年前,它是最热门的互联网技术之一。如果不安装Flash,很多网站根本打不开。那时还流行用它制作动画,随便一个作品,就有几十万、上百万的浏览量。电视台甚至开辟栏目,播放网上流行的Flash动画。各大互联网公司都有专门的Flash工程师,还是属于那种比较抢手、收入较高的工程师。我记得那个时候,社会上也有大量的Flash培训班,它们的招生广告都写着保证就业。

后来,Flash就不行了。2010年,乔布斯宣布,苹果手机不会使用Flash,因为影响手机性能。再后来,新的技术兴起,它就开始没落了。上个月,BBC发表一篇报道,名字就叫《Flash还能活多久?》。话音刚落,一周后,这项技术的拥有者Adobe公司宣布,放弃Flash这个名字,软件将重新定位,只用来制作动画。

说了这么多,我并不是感叹Flash这项技术的没落,这也是很正常的事,而是感叹那些从事Flash开发的工程师,他们该怎么办呢?你在一个领域钻研多年,都成了专家,突然之间那个领域过时了,你的所学所长没人需要了,那将是怎样的处境?

那些年里,我在上海遇见过一个朋友。他开了一家软件公司,专门面向海外市场开发Flash游戏。公司不大,十几个人,那时正是最好的年景,每个月都有几十万、甚至上百万人民币进账,看上去前景一片大好。可是,谁能想到Flash技术突然就会不行了呢?开始时,公司还能维持,后来手机游戏起来了,Flash游戏的市场顿时萎缩。我见过他的招聘广告,改招手机游戏的开发者。再后来,就再没听到过他的消息。

当一种技术消亡的时候,与它相关的工作岗位也就消亡了。这种事情在技术行业特别多,因为技术的升级换代太快了。

我再举一个例子。苹果手机出现之前,最流行的手机都使用Nokia公司开发的"塞班"操作系统。你可能还记得,它的典型标志就是"九宫格"菜单。那时,塞班工程师也是非常抢手的,彻底掌握它那一套开发技术,我估计至少要一两年时间。后来,智能手机流行,塞班一败涂地。2010年,诺基亚宣布放弃塞班,改用微软的操作系统。再后来,Nokia自己也没了,所有手机工程师都遣散了。我知道,Nokia中国有一个资深工程师,选择重进大学去读MBA学位。

试想一下,你花了多年的心血,孜孜不倦地投入和练习,终于掌握了一门赖以谋生的手艺,还进入了世界排名第一位的通信业跨国公司。就在你觉得人生终于有一点安全感的时候,一切就变了,几年之间,曾经的巨无霸土崩瓦解,不仅你的职位没了,更可怕的是,以前的产品已经没人用了,全世界现在不生产任何塞班设备。你的手艺的价值变成了零。

有人说,可以再学习、然后重新就业啊,塞班不行了,可以学习苹果手机开发。没错,说得完全正确。但是,你以前的积累没了,需要从零开始。跟现在刚刚走出校门的学生,站在同一条起跑线上,学习同样的东西。说实话,虽然你有几年开发经验,但很可能并没有那些20岁的年轻人学得快。在一个高速变化的行业,经验有时候不是帮助,而是障碍,因为以前的那套行不通了。退一步说,就算你重新学习了,但苹果手机的开发也在变,你得不停地追赶新东西 。一个人的人生,能经受得起多少次从零开始呢?

"终身学习"这个词完全没错,但是想通过"终身学习"保持职业竞争力,我觉得不太可能。

程序员,乃至其他很多技术岗位,其实是青春饭。只有底层的技术,还有一些稳定性,越接近应用层,技术的升级换代就越快。你学会一门技术,然后吃上三十年,这种事情越来越少见了。更常见的是,几年以后,你会的东西就淘汰了,你被迫重新学习新东西,或者重新就业。

为什么中国很少见35岁以上的程序员?因为他们上学时学习的东西都淘汰了,必须和年轻人一起学习新技术。你很难比年轻人更有竞争力,其中最关键的是,雇佣刚走出校门的学生,比雇佣你便宜得多。

其他行业的升级换代,不如技术行业那么夸张和激进。职业的安全感可以保持得更久一些,但远不是高枕无忧。技术正在取代人力劳动,比如财务会计这样的行业,随着电子支付的兴起,将来肯定不会需要这么多财务人员。"互联网+"从某个方面说,就是使用互联网技术取代一部分人力,更便宜地服务更多的顾客。

回到本文的题目,世界上有没有安全的工作?应该是有的吧,但真的不多。公务员可能比较安全,因为这个职业改变得比较缓慢,而且没有技术升级的压力。医生和律师,也比较安全,因为对于这些行业,经验很重要,但技术正在把它们的成本降下来。厨师和物流,也是比较安全的行业,因为技术再进步,也总需要人来做饭和送货,但是它们是纯粹的体力劳动,没有进入门槛,供给非常大,想拿到高工资不容易。

最终来说,人类社会的就业形态正在发生深刻的改变,"终生职业"越来越少了。每个人都应该尽早打算,如果明天你的职业消失了,你该怎么办?

[说明] 原文发表在《财新周刊》(2015年12月11日)的专栏。

(完)

文档信息

2015年12月9日星期三

阮一峰的网络日志

阮一峰的网络日志


常用 Git 命令清单

Posted: 08 Dec 2015 05:54 PM PST

我每天使用 Git ,但是很多命令记不住。

一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。

下面是我整理的常用 Git 命令清单。几个专用名词的译名如下。

  • Workspace:工作区
  • Index / Stage:暂存区
  • Repository:仓库区(或本地仓库)
  • Remote:远程仓库

一、新建代码库

  # 在当前目录新建一个Git代码库  $ git init    # 新建一个目录,将其初始化为Git代码库  $ git init [project-name]    # 下载一个项目和它的整个代码历史  $ git clone [url]  

二、配置

Git的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。

  # 显示当前的Git配置  $ git config --list    # 编辑Git配置文件  $ git config -e [--global]    # 设置提交代码时的用户信息  $ git config [--global] user.name "[name]"  $ git config [--global] user.email "[email address]"  

三、增加/删除文件

  # 添加指定文件到暂存区  $ git add [file1] [file2] ...    # 添加指定目录到暂存区,包括子目录  $ git add [dir]    # 添加当前目录的所有文件到暂存区  $ git add .    # 删除工作区文件,并且将这次删除放入暂存区  $ git rm [file1] [file2] ...    # 停止追踪指定文件,但该文件会保留在工作区  $ git rm --cached [file]    # 改名文件,并且将这个改名放入暂存区  $ git mv [file-original] [file-renamed]  

四、代码提交

  # 提交暂存区到仓库区  $ git commit -m [message]    # 提交暂存区的指定文件到仓库区  $ git commit [file1] [file2] ... -m [message]    # 提交工作区自上次commit之后的变化,直接到仓库区  $ git commit -a    # 提交时显示所有diff信息  $ git commit -v    # 使用一次新的commit,替代上一次提交  # 如果代码没有任何新变化,则用来改写上一次commit的提交信息  $ git commit --amend -m [message]    # 重做上一次commit,并包括指定文件的新变化  $ git commit --amend   ...  

五、分支

  # 列出所有本地分支  $ git branch    # 列出所有远程分支  $ git branch -r    # 列出所有本地分支和远程分支  $ git branch -a    # 新建一个分支,但依然停留在当前分支  $ git branch [branch-name]    # 新建一个分支,并切换到该分支  $ git checkout -b [branch]    # 新建一个分支,指向指定commit  $ git branch [branch] [commit]    # 新建一个分支,与指定的远程分支建立追踪关系  $ git branch --track [branch] [remote-branch]    # 切换到指定分支,并更新工作区  $ git checkout [branch-name]    # 建立追踪关系,在现有分支与指定的远程分支之间  $ git branch --set-upstream [branch] [remote-branch]    # 合并指定分支到当前分支  $ git merge [branch]    # 选择一个commit,合并进当前分支  $ git cherry-pick [commit]    # 删除分支  $ git branch -d [branch-name]    # 删除远程分支  $ git push origin --delete   $ git branch -dr   

六、标签

  # 列出所有tag  $ git tag    # 新建一个tag在当前commit  $ git tag [tag]    # 新建一个tag在指定commit  $ git tag [tag] [commit]    # 查看tag信息  $ git show [tag]    # 提交指定tag  $ git push [remote] [tag]    # 提交所有tag  $ git push [remote] --tags    # 新建一个分支,指向某个tag  $ git checkout -b [branch] [tag]  

七、查看信息

  # 显示有变更的文件  $ git status    # 显示当前分支的版本历史  $ git log    # 显示commit历史,以及每次commit发生变更的文件  $ git log --stat    # 显示某个文件的版本历史,包括文件改名  $ git log --follow [file]  $ git whatchanged [file]    # 显示指定文件相关的每一次diff  $ git log -p [file]    # 显示指定文件是什么人在什么时间修改过  $ git blame [file]    # 显示暂存区和工作区的差异  $ git diff    # 显示暂存区和上一个commit的差异  $ git diff --cached []    # 显示工作区与当前分支最新commit之间的差异  $ git diff HEAD    # 显示两次提交之间的差异  $ git diff [first-branch]...[second-branch]    # 显示某次提交的元数据和内容变化  $ git show [commit]    # 显示某次提交发生变化的文件  $ git show --name-only [commit]    # 显示某次提交时,某个文件的内容  $ git show [commit]:[filename]    # 显示当前分支的最近几次提交  $ git reflog  

八、远程同步

  # 下载远程仓库的所有变动  $ git fetch [remote]    # 显示所有远程仓库  $ git remote -v    # 显示某个远程仓库的信息  $ git remote show [remote]    # 增加一个新的远程仓库,并命名  $ git remote add [shortname] [url]    # 取回远程仓库的变化,并与本地分支合并  $ git pull [remote] [branch]    # 上传本地指定分支到远程仓库  $ git push [remote] [branch]    # 强行推送当前分支到远程仓库,即使有冲突  $ git push [remote] --force    # 推送所有分支到远程仓库  $ git push [remote] --all  

九、撤销

  # 恢复暂存区的指定文件到工作区  $ git checkout [file]    # 恢复某个commit的指定文件到工作区  $ git checkout [commit] [file]    # 恢复上一个commit的所有文件到工作区  $ git checkout .    # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变  $ git reset [file]    # 重置暂存区与工作区,与上一次commit保持一致  $ git reset --hard    # 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变  $ git reset [commit]    # 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致  $ git reset --hard [commit]    # 重置当前HEAD为指定commit,但保持暂存区和工作区不变  $ git reset --keep [commit]    # 新建一个commit,用来撤销指定commit  # 后者的所有变化都将被前者抵消,并且应用到当前分支  $ git revert [commit]  

十、其他

  # 生成一个可供发布的压缩包  $ git archive  

(完)

文档信息

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 

十四、参考链接

(完)

文档信息