JavaScript 专家级编程(三)
六、JavaScript IRL
Abstract
"头脑是模拟自己的模拟物."
-埃罗尔·奥扎恩
头脑是一种模拟自己的模拟物—埃罗尔·奥赞
兴奋起来;这一章是关于机器人、JavaScript 和其他的。不过说真的,机器人和 JavaScript 应该足够了。在这一章中,我将快速调查物理计算领域,以及用 JavaScript 编写的机器人如何融入其中。本章的大部分内容涵盖了使用监听 JavaScript 的机器与周围世界进行交互的方式。
硬件爱好者的日记
在我成长的过程中,我的弟弟马特得到了所有的二手技术。每当我们家升级一件消费电子产品时,旧版本就会被马特拿走,他几乎会立刻抱着它蜷缩在房间的地毯上,用双腿缠住猎物。然后,就像一只患有强迫症的秃鹰,他会开始用他的儿童螺丝刀有条不紊地拆开塑料尸体。最终,机器会停止运转,小的塑料齿轮、电线和电路板会散落出来。有时他会试着把它重新组装起来,或者把零件留着以后用。马特对一种完全不同的机器学习感兴趣。
硬件黑客并不是一个他已经超越的阶段。他变成了一个男孩巫师,用一把直角牧田螺旋枪代替了魔杖。15 岁时,他在一家名为 Dashboard Stereo 的公司找到了一份安装汽车音响系统的工作。他纠缠着老板达雷尔,直到他让步给了马特一份工作。马特是至少十年来最年轻的技术人员,但他已经比他们中的大多数人都要好。他是有史以来最年轻的移动电子认证专业(MECP)安装人员,年仅 15 岁。他在音响店工作到大学毕业,然后去了弗吉尼亚理工大学。毫不奇怪,马特成为了一名电气工程师,现在是麻省理工学院研究人员的一员。
消防水管
作为一名高中生,我被北卡罗莱纳艺术学校录取,这意味着我住在离家大约两个小时的地方。今年年初,马特把他的电动低音炮借给我,让我带回宿舍。不幸的是,我一到学校就意识到我把扬声器的插头忘在家里了。我去 Radio Shack 买了一个通用电源适配器,它看起来像一个手里剑,但有各种大小的插头,而不是金属点。我回到学校,把手里剑插在扬声器的后面,站在那里,手里拿着线的另一端看着墙壁。
从小到大,我一直梦想着像马特一样打开硬件,但在这里,我甚至被插上东西吓住了。我意识到我不知道我在做什么,所以我打电话给我爸爸。事实证明,这是个坏主意。我的父亲是另一个天才的机器语者;有时,当我哥哥和他没有任何共同语言时,他们至少会说瓦特和欧姆这两种共同语言。我解释了我不知道该用哪个插头的问题,这是他告诉我的:
Your problem is that you don't know the correct voltage or current to power the speakers. Imagine that your loudspeaker is running on water, so you hang it on the fire hose. Water flowing through the hose is the voltage, and the speed of water flow is the amperage.
我知道他觉得他已经用一个一年级学生就能理解的方式解释了。但我站在那里,把公用电话听筒贴在耳朵上,试图想象他刚才说的话。不过,我还不如打电话给中国,因为他在消防水管那里把我搞糊涂了。我回到我的宿舍,再次拿起线的一端。我想,会有多糟呢?
我在可用选项中选择了一个电源设置,并将电源线插入墙上的插座。我立刻听到扬声器里传来微弱的嗡嗡声,然后是一声巨响,然后就什么也没有了。我很快意识到我已经销毁了我哥哥的扬声器,我对我们家族拥有的与硬件交流的能力抱有的任何幻想都烟消云散了。灰色的烟雾慢慢地从扬声器的舷窗飘出。当然,马特发现后非常生气,可能是因为我没有问他,也可能是因为他不明白怎么会有人对电学基础知识如此无知。从那以后,我不敢再尝试了——也就是说,直到成年后我发现了 Arduino。
其他人的硬件
我在加州大学洛杉矶分校参加了一个演讲,我去听凯西·雷耶斯讲述他的项目“处理”Processing 是他和 Ben Fry 创建的一个程序,使艺术家能够用代码进行素描。Reas 最近加入了加州大学洛杉矶分校的设计媒体艺术系,我很有兴趣见到他本人,因为他在麻省理工学院上学时,我就一直关注着他的工作。作为那天晚上演讲的一部分,另一个人谈到了一种新的努力,那就是创造一种可以安装在单板上的廉价微控制器。该委员会和项目的名称是 Arduino,它试图让外行人也能接触到硬件,就像处理程序一样。目标是通过生产开源硬件来解放物理计算设备。
我作为一个感兴趣的观察者随意地关注着这个项目,并最终在一段时间后购买了自己的主板。当冲浪板到达时,它被放在我办公室的一个架子上,就像一个我没玩过的运动的奖杯一样放在那里。我不时地看着 Arduino,在电话会议期间把它从架子上拿下来,用手指触摸 PCB 的边缘,用拇指按压跳线引脚,就好像它们是一个迷你钉床一样。对我来说,这块小小的金属和硅胶包含了硬件的希望和危险。我非常想通过自己设计的硬件与这个世界互动,但我所能想象的只有从我哥哥的扬声器中飘出的灰色烟雾。
让我们开始身体接触吧
软件编程的一个很大的优点是很难把计算机的硬件搞砸。当然,你可能会不经意地通过写rm -rf /而不是rm -rf ./来擦除你的硬盘(我在这里是凭经验说的),但是硬盘仍然工作正常。软件更加宽容,允许更多的尝试和错误。硬件可以像火的审判(字面意思)。不小心放错线有可能会烧坏您的 Arduino 或电脑(或两者)。然而,硬件通过压电蜂鸣器播放的不可否认的警笛声在召唤我。在我的控制下,一个闪烁的 LED 灯的潜力比它应有的更有价值。
第一部分的大部分内容都是关于我个人在硬件方面的挫折和恐惧。我把这一节作为鼓励那些坐在硬件池边、戴着 JavaScript floaty 翅膀的人的一种方式。如果我能在这里学会游泳,你也能。进来吧;水(我是说电压)没问题。
物理计算
我知道我承诺过机器人,我会实现的,但是你在这一章学到的大部分东西都被归入物理计算的范畴。这个名字本身听起来非常模糊,几乎毫无意义。从定义开始会帮助你找到方向。维基百科是这样定义物理计算的:
Physical computing, in the broadest sense, refers to the construction of interactive physical systems by using software and hardware that can sense and respond to the simulated world. Although this definition is broad enough to cover intelligent car traffic control systems or factory automation processes, it is not often used to describe them. Broadly speaking, physical computing is a creative framework for understanding the relationship between human beings and the digital world. In practical use, this term usually describes handicraft art, design or DIY hobby projects, which use sensors and microcontrollers to convert analog inputs into software systems and/or control electromechanical devices such as motors, servo systems, lighting or other hardware. 1
在流行文化中,物理计算通常与工程局外人或新媒体艺术家联系在一起,他们不受专业工程的引力影响。许多最有趣的物理计算例子都采用了两种方法中的一种:
- 他们以一种意想不到的方式将计算机交织成一个现有的模拟物理过程。
- 他们将虚拟世界的规则、比喻或人工制品映射到物理空间。
尽管维基百科的定义暗示了物理计算的 DIY 性质,但这并不是说该领域没有消费级、大众市场电子产品的位置。微软 Kinect 是这种设备的一个完美例子。
Kinect 这个名字巧妙地影射了它的目的,那就是利用它的摄像头来读取玩家的身体手势,作为控制游戏的手段。这个名字是动力学(运动)和连接(数据传输)的融合,这是物理计算的两个关键方面。Kinect 是一个非常精致和非常复杂的物理计算设备的例子。
你可能会问自己这样一个问题:硅胶中区分物理计算设备和非物理计算设备的线是什么?考虑一下 Kinect 和数码摄像机之间的区别。两者之间的重要区别不是技术复杂性的不对称,而是 Kinect 使用视频捕捉作为更大的反馈处理循环的一部分,涉及玩家、游戏系统和潜在的远程服务器。或者,摄像机仅仅不加区别地存储所有可用的输入,并等待进一步的指令。
物理计算的意义不在于制造更多的东西。目标是在现实世界和虚拟世界之间建立新的通道,允许用户阅读、混合和转播他们周围的世界。
物联网
物理计算设备通常被称为“物联网”的一部分凯文·阿什顿(Kevin Ashton)创造了这个术语,他用这个术语来理论化一个世界,在这个世界中,所有设备将通过无所不在的网络和低功耗、廉价和混杂的传感器持续连接在一起。他感兴趣的是这些设备如何跟踪和分类自己,或者在用户需要时大声说话。他的重点是新兴的射频识别(RFID)标签领域,这是一种由外部电源供电时可以读写的小电路。RFID 标签现在在日常生活中无处不在。从信用卡到家庭宠物的颈背,它们被嵌入到一切事物中。阿什顿在 2009 年 7 月版的 RFID 杂志中解释了他的概念:
Today, computers-and the Internet-depend almost entirely on human beings to obtain information. About 50 gigabytes (one gigabyte is equal to 1024 megabytes) of data on the Internet are almost all captured and created by humans for the first time by typing, pressing the recording button, taking digital photos or scanning bar codes. Traditional diagram of Internet. . . Save the largest and most important routers among all people. The problem is that people's time, attention and accuracy are limited-all these mean that they are not very good at capturing data about things in the real world. This is a big event. We are material, so is our environment. . . You can't eat a little, burn them to keep warm or put them in your fuel tank. Ideas and information are important, but things are more important. However, today's information technology relies so much on data generated by people that our computers know more about ideas than things. If we have computers that know everything-using the data they collected without any help from us-we will be able to track and calculate everything, and greatly reduce waste, loss and cost. We will know when things need to be replaced, repaired or recalled, whether they are fresh or in the best condition. The Internet of Things has the potential to change the world, just like the Internet. Maybe even more so. 2 —Kevin Ashton
今天,这个术语已经被其他几个领域所采用。根据你的对话对象,物联网现在同时描述:
- 阿什顿定义中的库存和履行系统
- 像 Kinect 这样的物理计算设备
- 增强现实设备,将虚拟对象叠加到特定的真实空间中,只能通过虚拟舷窗(即智能手机)进行查看
- 虚拟物体以图案的形式存在,使用 3D 打印机等快速成型工具生产。
因为这是关于 JavaScript 机器人的一章,所以我将使用第二个定义。
为什么是 JavaScript
如前所述,物理计算不仅仅是硬件的事情。它实际上是物理和虚拟之间精心编排的 I/O 循环之舞。您选择的语言决定了舞蹈在用户看来有多轻松。事实证明,JavaScript 让这两个伙伴几乎毫不费力地拥抱在一起,但原因可能出乎你的意料。JavaScript 具有适合物理计算的技术和语义特征,并且不断增加的库使得硬件变得不那么困难。本节解释了为什么 JavaScript 是物理计算的最佳选择。
建造桥梁
NodeBots 的创始人克里斯·威廉姆斯(我将在后面讨论)已经思考了很多关于 JavaScript 如何增强机器人技术的问题。在从事一个使传感器能够通过各种无线频谱进行通信的项目时,他对其他图书馆使用的方法感到不满。他觉得这种方法虽然在技术上很熟练,但在语义上却很笨拙。在他看来,这些图书馆在他们期望的世界行为方式和实际运作方式之间遭遇了脱节。一段时间后,Williams 审阅了 Nikolai Onken 和 rn Zaefferer 关于“机器人 JavaScript”的演示提案。他们的提议宣称 JavaScript 可以用来控制现实世界中的设备。这激发了他的想象力,他制定了一个最小但富有表现力的语法:
$("livingroom").bind("motion", function() {
$(this).find("lights").brightness("75%").dimAfter("120s");
});
这个简单代码片段的美妙之处在于,用 Williams 的话来说:“将现实世界的对象和动作建模为可链接的、事件化的流程几乎是自然而然的事情。”这个被提议的语法激发了 Williams 编写 node-serialport,他把它看作是“硬件的网关”
反应式编程范例
Williams 的起居室灯光示例暗示了现实世界的一个基本特性,即它是在不同持续时间内执行的异步操作的集合。在他的模型中,起居室对象将事件监听器绑定到发生在其中的任何运动。一旦被触发,绑定的函数调用一个方法来打开灯。这些灯依次有自己的反应任务链要完成,首先打开,然后在给定的时间框架后变暗。
在前面的代码片段中提到的这种事件观察者模式在很多 JavaScript 库中非常常见,比如 jQuery。这种熟悉是 Williams 认为 JavaScript 是控制硬件的好选择的原因之一,因为即使没有硬件经验的开发者也可以利用他们的知识来构建交互式网页。
该代码片段还暗示,需要编写该框架来处理一个事件同时来自许多输入的世界。反应式系统的目标是响应被监视对象的状态变化,并将这些变化传播到任何其他相关对象。反应式系统的经典例子是一个电子表格,其中“c”列中的总和取决于“a”和“b”列的相加。通常,这种计算只会发生一次。如果“a”或“b”的值发生变化,“c”将不再正确。除非“c”被告知这一变化,否则它永远不会更新,从而永远不同步。然而,在反应式系统中,“c”会观察“a”和“b”的变化。一旦检测到变化,它将再次对“a”和“b”求和。重新计算其值的过程反过来会触发依赖于事件流中较高“c”的对象也做出反应。
给机器人编程时,机器人可能会使用各种不同的传感器同时跟踪许多不同的环境变量。然而,这些传感器可能在不同的时间间隔返回结果。因此,在硬件必须做出响应之前,反应式系统将有助于对输入进行聚合、重新处理和潜在的无效处理。与机器人相关的反应式编程系统的目标是处理现实世界的异步性质,并将其重新表述为硬件可以执行的一系列顺序步骤。在下一节中,您将开始使用 NodeBots 软件栈构建您的反应式系统。
节点机器人:快速、廉价、伺服控制
节点机器人是使用不可见的 JavaScript 系绳控制的机器人。这个系绳由一个节点服务器和一组库组成,这些库抽象了与硬件通信的大部分繁重工作。您将构建的节点机器人利用 Arduino 板来控制输出外设。但是,在您开始构建您的机器人之前,您必须首先了解所有这些技术是如何协同工作的。考虑图 6-1 所示的下图。它解释了您最终将构建的 Nodebot 的结构。
图 6-1。
Anatomy of a Nodebot
幸运的是,你将在这一章花大部分时间编写代码。这是因为当你在你的机器人图上移动到更低的位置时,创造奇迹所需的代码变得更加机器专用,表达性更差,而且表面上看起来写起来也没什么意思。但是,为了让您知道自己有多好,并确保您完全理解堆栈的各个部分如何协同工作,您将从膝盖以下开始向上编写代码。该过程将是重复编写使 LED 闪烁所需的代码,这在硬件上相当于“Hello world”。
您将首先使用本机 IDE 直接为 Arduino 编写,然后过渡到编写与节点串行端口握手的固件。最后,你会去 Firmata 然后是 Johnny-Five。
取代
我前面提到过,机器将使用 JavaScript 连接到主机。这与典型的方法有很大不同,典型的方法是编辑源文件,然后将其编译成字节码,这样就可以直接存储在 Arduino 的芯片上。只有这样,程序才能运行。这个开发周期被称为编辑-编译-运行-调试(ECRD),大多数 Arduino 机器人就是这样构建的。相比之下,节点机器人将机器人的大脑保存在主机上,并使用读取-评估-打印-循环(REPL)环境。这种方法有一些优点和缺点,我将在这里一一列举。
优势
- 由于主机和硬件之间的实时交互,鼓励实验。
- 降低了调试的复杂性,因为代码仍然可以在主机上访问,并且不会被编译成不同的形式,这可能会导致不一致。
- 在硬件的低级控制和高级业务逻辑之间提供了一个清晰的关注点分离。
- 由于主机提供的额外资源,程序可能会更复杂。
不足之处
- 需要持久的系绳,这可能会限制机器人的自主性。
- 增加了机器人运行所需的依赖性。
- 可能导致主机和机器人之间的响应延迟,这是由于通过系链发送消息需要时间。
何必呢?
多年来,当我在电话里缠着我的哥哥马特,向他咨询我最新的不切实际的硬件想法时,他通常会以同样的方式回答:“你为什么要这么做?”他的话总是戳破我的精神泡沫,我会沮丧地回到现实,因为真正的工程师会认为我的想法是愚蠢的。有几次他试图回答我的问题,我很快就迷失在概念和细节中,因为我没有参照系来理解。我知道他不是有意伤害我的感情,也许在他看来,他是在为我节省时间和精力,让我去追求他认为是幼稚的方法。当我开始对 JavaScript 机器人感到兴奋时,我问自己这个问题:真正的机器人工程师会对节点机器人嗤之以鼻,认为它们没有价值吗?为了找到答案,我问了一位真正的机器人工程师。
Raquel Vélez 是一名机械工程师,曾在加州理工学院接受培训,此后在机器人领域工作了近十年。她在 NodeBots 社区也非常活跃。因为 Vélez 是专业和业余机器人社区的内部人士,我觉得她可以回答“为什么这么麻烦”这个问题。当我向她提出这个问题时,她是这样说的:
Indeed, the node robot is still in its infancy; We won't run driverless cars with node soon. But the point is not that node will replace C++/Python-instead, by opening the robot community to JavaScript community, we are making robots that didn't exist before available to people all over the world. This influx, in turn, increases the diversity of people trying to solve difficult problems, thus promoting the development of all technologies (networks, robots).
她继续以这种方式比较和对比这两个社区:
Basically, you can't get it in the "traditional" robot industry with the ability of real open source and super-fast turnaround time. When I work in academia/industry, you must have a lot of money, experience and time to complete any important work. With NodeBots, you don't need any of these things-you can just start.
由于她提到的所有原因,甚至在 Vélez 给专家竖起大拇指之前,我就接受了这个想法,还因为我不需要征求任何人的许可就可以开始。如果你能凑齐不到 100 美元的零件和工具,基本上没有进入的障碍,我将在下面介绍。
先决条件
本章有各种外部和特定于系统的先决条件,需要满足这些条件才能逐步完成。在尝试复制 Nodebot 示例之前,请确保您已经花了必要的时间来确认您的环境满足以下先决条件。
一般
在安装任何东西之前,确保您的系统能够编译 Node 的任何和所有本机模块。在撰写本文时,需要 Python 2.x 使用 3.x 版本会导致失败,因为node-serialport依赖于node-gyp,而后者需要 Python 2.x
Windows 操作系统
必须安装 Visual Studio 2010+(速成版就可以)。如果您将使用 Arduino,请确保您安装了必要的驱动程序。 4
Mac OS X
你必须确保你已经安装了 xCode 命令行工具 5 (最低限度)。
Linux 操作系统
最有可能的是,除了一般的先决条件之外,您的系统不存在任何特殊的依赖关系。
购物单
在你建立你的机器人军队之前,你必须有一套基本的零件和少量的工具。以下是复制本章示例所需的最低购物清单。如果你认为 JavaScript 机器人可能会吸引你一段时间的兴趣,你可能会考虑购买一个预捆绑的工具包。这些包包括您需要的零件和一些其他好的组件。很多时候,这些工具以探索者、发明家或入门者的名字出售;并且可以通过各种本地和在线电子产品零售商获得。
- 一个 Arduino Uno R3 板卡 6 个
- 10 英尺。USB 2.0 认证的 480Mbps 型公到 B 型公电缆
- 几个基本的红色 5 毫米发光二极管
- 一包试验板跳线
- 微型伺服电机
- 安全眼镜
Arduino IDE
在本节中,您将使用本机 IDE 创建一个 Arduino 闪烁。您将编写一个简单的脚本,然后必须上传到 Arduino 板上。只有在使用 IDE 时才需要这两步过程;一旦您将一个节点串行端口添加到堆栈中,您就可以创建到 Arduino 板的持久连接。
设置
你首先需要下载 Arduino IDE 7 并成功安装。安装完成后,您需要将 LED 的较长引脚(正极)放在引脚插槽 13 中,较短引脚(负极)放在接地插槽中。您使用引脚 13 的原因是因为它已经内置了一个电阻。一旦安装完毕,你的电路板看起来应该如图 6-2 所示。
图 6-2。
Board layout for the blink example
烟气试验
要执行此测试,您需要按照一系列步骤让您的 LED 闪烁。
步骤 1:连接电路板
将 USB 线连接到 Arduino 和电脑。您应该会看到板上有一个小 LED 灯,并且一直亮着。此 LED 表示电源正在流向主板。
Note
在 Windows 机器上,硬件向导可能会提示您安装 Arduino 的驱动程序。您需要解压缩FTDI USB Drivers.zip,它可以在您随 IDE 下载的 Arduino 发行版的drivers文件夹中找到。从(高级)菜单选项中将向导指向这些驱动程序。
步骤 2:选择正确的电路板
确保您在 ide 中选择了正确的板。这可以通过从工具➤板子菜单中选择板来完成,如图 6-3 所示。
图 6-3。
Arduino IDE board selection menu Note
本章假设您正在使用 Arduino Uno。如果您使用的是另一种类型的主板,前面的截图不会 100%准确。
步骤 3:编写固件
Arduino IDE 使用了速写本的比喻,其中每一页都是可以加载到 Arduino 中的草图。草图以文件扩展名.ino保存。以下是您将上传到 Arduino 的草图。幸运的是,你不需要转录它,因为这个代码可以在 ide 的examples文件夹中找到(见图 6-4 )。
图 6-4。
Arduino IDE example selection menu
/*
Blink
Turns on an LED on for one second, then off for one second, repeatedly.
This example code is in the public domain.
*/
// Pin 13 has an LED connected on most Arduino boards.
// give it a name:
int led = 13;
// the setup routine runs once when you press reset:
void setup() {
// initialize the digital pin as an output.
pinMode(led, OUTPUT);
}
// the loop routine runs over and over again forever:
void loop() {
digitalWrite(led, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(led, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
这段代码应该是不言自明的;它只是初始化电路板,然后开始重复循环。在每次循环中,代码都会发出一个调用,将高值或低值写入 Arduino 的引脚 13。这段代码很难理解的一个方面是常量OUTPUT、HIGH和LOW实际上做了什么。
步骤 4:编译并上传固件
选择眨眼教程后,将出现一个新的草图窗口。这个新窗口的顶部有几个图标。找到复选标记图标并按下它。此操作告诉 IDE 验证代码并将其编译成适合上传到 Arduino 板的格式。如果一切正常,您应该会在界面底部看到编译完成的消息。
单击顶部菜单中的右箭头图标,这将把代码上传到 Arduino。您会看到底部附近出现一个进度指示器,它会随着代码传输到电路板而更新。一旦你看到完成上传,你应该会看到你的 Arduino 有节奏地为你闪烁一个 LED。
第五步:拔掉 Arduino
成功完成测试后,从计算机上拔下 USB 电缆,这将切断 Arduino 的电源。
节点串行端口
节点串口是 NodeBot 层蛋糕的基础。本章涉及的所有其他库都会以某种方式依赖于这个库。但是,在使用节点串行端口与 Arduino 通信之前,您需要创建自定义的.ino固件,它允许节点代码和 Arduino 之间的握手。
烟气试验
步骤 1:连接电路板
使用 USB 电缆将您的主板重新连接到计算机。您应该会看到板载 LED 变亮,表示主板已通电。
Note
如果您跳过了前面的 Arduino 示例,请参考该部分以确保您已经安装了所有必需的驱动程序。
步骤 2:选择正确的电路板
确保您在 ide 中选择了正确的电路板,就像您在前面的 Arduino IDE 示例中所做的那样。
步骤 3:编写固件
在 Arduino IDE 中打开一个新的草图文件,并转录以下代码:
int bytesRead = 0;
boolean isPinSet;
byte stored[2];
void setup()
{
Serial.begin(57600);
}
void loop()
{
while (Serial.available()) {
int data = Serial.read();
stored[bytesRead] = data;
bytesRead++;
if (bytesRead == 2) {
if (isPinSet == false) {
isPinSet = true;
pinMode(stored[0], OUTPUT);
} else {
digitalWrite(stored[0], stored[1]);
}
bytesRead = 0;
}
}
}
步骤 4:编译并上传固件
将前面的代码转录到草图文件中后,单击复选标记图标验证并编译源代码。如果您已经正确输入了所有内容,您应该会在界面底部看到消息“Done compiling”。接下来,单击右箭头将编译后的代码上传到 Arduino。当它传输代码时,您应该会看到一个进度指示器出现。一旦一切完成,您应该会在界面底部看到“上传完成”的消息。
步骤 5:安装节点串行端口
如果您的计算机上已经安装了节点和 npm,您可以像这样安装节点串行端口:
npm install serialport
第六步:编写程序
从您喜欢的文本编辑器中创建新文件,并键入以下代码。一旦转录,保存它为serial-blinky.js到你安装节点串口的同一个文件夹。
var serial = require("serialport"),
raddress = /usb|acm|com/i,
pin = 13;
serial.list(function(err, result) {
var read = new Buffer(0),
address, port, bite;
if (result.length) {
address = result.filter(function(val) {
// Match only address that Arduino cares about
// ttyUSB#, cu.usbmodem#, COM#
if (raddress.test(val.comName)) {
return val;
}
}).map(function(val) {
return val.comName;
})[0];
port = new serial.SerialPort(address, {
baudrate: 57600,
buffersize: 1
});
port.on("open", function() {
var bite;
function loop() {
port.write([pin, (bite ^= 0x01)]);
}
setInterval(loop, 500);
});
} else {
console.log("No valid port found");
}
});
现在使用以下命令从命令行运行您的代码:
node serial-blinky.js
如果一切正常,LED 应该开始为您闪烁。
Caution
如果出现“找不到模块‘串行端口’”错误,您需要将此草图保存在包含节点串行端口库的‘node _ modules’文件夹旁边。
第七步:拔掉 Arduino
成功完成该测试后,从计算机上拔下 USB 电缆。这样做应该会切断 Arduino 的电源。
近得危险
这种方法实际上比只为 Arduino 编写更麻烦,因为它需要两个紧密耦合的文件才能工作。如果您对 JavaScript 文件进行了实质性的修改,那么您需要在。ino 文件。这是因为节点串行端口是一个低级的库,只是为了通过串行端口进行通信,仅此而已;一分不少。值得庆幸的是,当您继续使用 Firmata 时,您将在抽象上更上一层楼。
格式(formata)
Firmata 是用于 Arduino 和主机之间通信的通用协议。在本例中,您将使用两种形式的 Firmata。第一个是固件。您将直接加载到 Arduino 上。第二个 Firmata 是与固件握手的节点库。在本节中,您将重新创建 blinking 示例,但是这次使用 Firmata 作为桥梁。
烟气试验
步骤 1:连接电路板
使用 USB 电缆将您的主板重新连接到计算机。您应该会看到板载 LED 变亮,表示该板已通电。
Note
如果您跳过了前面的 Arduino 示例,请参考该部分以确保您已经安装了所有必需的驱动程序。
步骤 2:选择正确的电路板
确保在 ide 中选择了正确的电路板,就像在 Arduino IDE 示例中一样。
步骤 3:找到串行端口
节点串行端口需要知道 Arduino 连接到哪个端口。要找到端口的路径,查看工具➤串口子菜单下,如图 6-5 所示。Arduino 将连接到带有复选标记的端口。记下这份参考资料,以便以后使用。
图 6-5。
Arduino IDE serial port selection menu
步骤 4:安装固件
要设置 REPL 开发环境,您必须在 Arduino 上安装 StandardFirmata 固件。幸运的是,这些代码与 IDE 捆绑在一起。只需选择文件➤范例➤ Firmata ➤标准 Firmata,如图 6-6 所示。这将打开一个新的草图窗口,所需的代码已经存在。现在点击右箭头,将编译好的代码上传到板上。
图 6-6。
Arduino IDE Firmata selection menu
上传完成后,您的 REPL 环境就可以使用了。此时,可以关闭原生 Arduino IDE 在本章的其余部分,您将不再需要它。
步骤 5:安装 Firmata 库
现在,您已经将标准 Firmata 固件加载到您的主板上,您需要安装 Firmata 节点库,它能够理解如何与之通信。从安装 node-serialport 的同一目录中,键入以下内容:
npm install firmata
第六步:编写程序
如果 Firmata 安装正确,你就可以重写你的闪烁程序了。在文本编辑器中,转录以下代码,并将其作为'firmata-blinky.js'保存在您用来存储之前示例的同一文件夹中:
/**
* Sample script to blink LED 13
*/
console.log('blink start ...');
var pin = 13;
var firmata = require('firmata');
var board = new firmata.Board('/dev/cu.usbmodem1411', function(err) {
var bite;
board.pinMode(pin, board.MODES.OUTPUT);
function loop() {
board.digitalWrite([pin, (bite ^= 0x01)]);
}
setInterval(loop, 500);
});
现在,使用以下命令从命令行运行您的代码:
node firmata-blinky.js
如果一切正常,您应该会看到 LED 开始为您闪烁。
第七步:拔掉 Arduino
成功完成该测试后,从计算机上拔下 USB 电缆,这将切断 Arduino 的电源。
真实的 REPL
现在您已经安装了 Firmata 固件,并与主机上的 Firmata 库进行了通信,您已经有了一个真正的 REPL 开发环境设置。这意味着(与您的节点串行端口版本不同),您不必在每次更改主机上的源代码时都更新固件。不幸的是,尽管 Firmata 很棒,但您必须编写的 JavaScript 代码仍然非常特定于领域。就像在 Arduino IDE 示例中一样,在您的代码正确运行之前,您需要理解几个不明确的常量和模式。要编写更加与硬件无关的代码,您需要在堆栈上再往上爬一层。前进到强尼五号!
强尼五号
里克·沃尔德伦对机器人很认真,以至于他造了自己的机器人来向妻子求婚。她不是工程师,也没有自己的机器人使者,而是用她最好的机器人声音告诉了沃尔德伦这个好消息。就我个人而言,我认为 Waldron 是 JavaScript 社区中一个崩溃的人物——一个愉快地利用自己的智力来取乐而不是牟利的人,但却认真地致力于推动社区和语言向前发展。
Waldron 创建了 Johnny-Five,这是一个开源的 Arduino 编程框架,位于 Firmata 和 Node 串行端口堆栈之上。Johnny-Five 有一个清晰的表达 API,感觉就像大多数开发人员习惯在其他环境中编写的 JavaScript。这是最接近柏拉图式的理想,是克里斯·威廉姆斯在他的假想起居室例子中提出的。我问了沃尔德伦关于 Johnny-Five 的事情,以及为什么他和其他人一样,认为 JavaScript REPL 环境是机器人编程的理想环境。他是这样回应的:
All hardware is implicitly synchronized; It exists in the real world. If you let something move, it takes real time to move. This means that any program interacting with hardware must know these time constraints and be able to provide an effective control mechanism. Traditionally, this is realized by multithreading and interrupt-based programming model. I believe that the single-threaded, round-based execution model can provide the same level of efficient control. Consider a simple sensor connected to Arduino; Traditionally, we call some functions repeatedly to read and process the values of analog sensors, and conditionally execute other parts of the program according to the changes of the values. When using Johnny-Five framework to write the same program in JavaScript, the programming model becomes an observer in the form of event bus. If the value of the sensor changes, the listener will be notified. When programming output, the idea is the same, but it has greater influence. Suppose we want our program to move a servo mechanism back and forth from 0 degrees to 180 degrees; Using our servo data manual, we calculate that the whole journey of 180 degrees takes 1000 milliseconds. If you write in Arduino C, you need a delay (1000) after the first move, which will block the whole one-second execution process. If this is in a loop, then each loop has a pause time of 1 second. If the program must also read sensors for certain conditions, these sensors will also be blocked for 1000 milliseconds. In JavaScript on Node.js, using Johnny-Five, tasks that need "delay" or "loop" will not prevent execution. Instead, they are scheduled tasks that will be called in a later execution round, allowing the rest of the program to continue normally. Round-based execution model is actually not a part of JavaScript language; This is an example of an embedded environment, such as browser, or in this case, the round-based execution of Node.js. Node.js is implemented in the form of libuv, which provides an asynchronous, event-based execution environment. This model is an implicit simulation of the explicit loop () in Arduino C.
Waldron 的方法非常符合本章前面提到的反应式编程范例的精神。状态变化在整个框架中传播的方式意味着您可以编写更少的代码来有效地为真实世界建模。在下一节中,您将重新创建闪烁的 LED。然后,您将通过创建一个更高级的示例来探索 Johnny-Five 的 REPL 环境。
烟气试验
步骤 1:连接电路板
使用 USB 电缆将您的主板重新连接到计算机。您应该会看到板载 LED 变亮,表示该板已通电。
第二步:安装强尼五号
此步骤假设您已经将 StandardFirmata 固件刷新到 Arduino 上。如果您尚未完成此步骤,请参考本章前面的 Firmata 部分。在安装 node-serialport 和 Firmata 的同一目录中,键入以下内容:
npm install johnny-five
第三步:写一个程序
假设 Johnny-Five 安装正确,您就可以重写闪烁的 led 示例了。在文本编辑器中,转录以下代码,并将其作为'johnny-blinky.js'保存在您用来存储之前示例的同一文件夹中:
var five = require("johnny-five"),
board = new five.Board();
board.on("ready", function() {
(new five.Led(13)).strobe();
});
现在,使用以下命令从命令行运行您的代码:
node johnny-blinky.js
如果一切正常,LED 应该开始为您闪烁。
第四步:拔掉 Arduino
成功完成测试后,从计算机上拔下 USB 电缆,这将切断 Arduino 的电源。
摆弄强尼五号
只要看看 Johnny-Five 闪烁 LED 所需的行数,就应该很清楚,这个框架确实使 Arduino 的编写变得更加容易。然而,你才刚刚开始!在下一个示例中,您将使用 REPL 控制台实时控制微型服务器电机。通过这个过程,你将更多地了解 Johnny-Five 如何在内部对硬件建模,以及如何利用这些知识来改进你自己的程序。
第一步:准备板子
在这个例子中,你将使用 Johnny-Five 控制一个微型伺服系统。如果你有你的马达,将数据线插入第 10 号插脚,将电源线插入第 5 号插脚。最后,将接地线插入其中一个可用的接地引脚(参见图 6-7 )。
图 6-7。
Wiring diagram for servo example Note
此图显示了直接连接到 Arduino 引脚插槽的电线。然而,在现实中你可能需要使用跳线来连接你的伺服到 Arduino。
第二步:连接电路板
使用 USB 电缆将您的主板重新连接到计算机。您应该会看到板载 LED 变亮,表示主板已通电。
第三步:写一个程序
现在,您将编写一个简单的程序来与您的伺服系统进行交互。在文本编辑器中,将以下代码转录到一个文件中。将文件另存为“servo.js”,保存在示例中使用的同一目录下。
var five = require("johnny-five"),
board = new five.Board();
board.on("ready", function() {
var servo = new five.Servo(10);
this.repl.inject({
servo: servo
});
servo.center();
servo.on("move", function(err, degrees) {
console.log("move", degrees);
});
});
现在,使用以下命令从命令行运行您的代码:
node servo.js
如果一切正常,您应该在终端窗口中看到伺服中心和以下输出:
1374513199929 Board Connecting...
1374513199933 Serial Found possible serial port /dev/cu.usbmodem1411
1374513199934 Board -> Serialport connected /dev/cu.usbmodem1411
1374513203157 Board <- Serialport ready /dev/cu.usbmodem1411
1374513203158 Repl Initialized
>>
在 REPL 控制台中,键入以下内容:
this.servo.move(90)
应该发生两件事:您应该看到伺服旋转了 90 度,并看到 Johnny-Five 呈现给控制台的硬件状态的表示(您将在下一节中详细探讨)。
第四步:拔掉 Arduino
成功完成测试后,从计算机上拔下 USB 电缆,这将切断 Arduino 的电源。
五号还活着
当您向 Johnny-Five 的 REPL 实例发出命令时,它会返回一个表示当前环境状态的 JavaScript 对象。在您的伺服示例中,在您发出移动命令后,Johnny-Five 返回了一个看起来有点像这样的对象:
{
board: {
ready: true,
firmata: {...},
register: [ [Circular] ],
id: '98880E34-5D9E-49A9-8BA0-89496E54F765',
debug: true,
pins: { '0': [Object], '1': [Object], '2': [Object], '3': [Object], '4': [Object], '5': [Object], '6': [Object], '7': [Object], '8': [Object], '9': [Object], '10': [Object], '11': [Object], '12': [Object], '13': [Object], '14': [Object], '15': [Object], '16': [Object],'17': [Object], '18': [Object], '19': [Object] },
repl: {
context: [Object],
ready: false,
_events: {}
},
_events: { ready: [Function] },
port: '/dev/cu.usbmodem1411'
},
firmata: {...},
_maxListeners: 10,
MODES: {
INPUT: 0,
OUTPUT: 1,
ANALOG: 2,
PWM: 3,
SERVO: 4
},
I2C_MODES: {
WRITE: 0,
READ: 1,
CONTINUOUS_READ: 2,
STOP_READING: 3
},
STEPPER: {
TYPE: [Object],
RUNSTATE: [Object],
DIRECTION: [Object]
},
HIGH: 1,
LOW: 0,
pins: [ [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object]],
analogPins: [14, 15, 16, 17, 18, 19],
version: { major: 2, minor: 3 },
firmware: { version: [Object], name: 'StandardFirmata.ino'},
currentBuffer: [],
versionReceived: true,
sp: {
domain: null,
_events: [Object],
_maxListeners: 10,
options: [Object],
path: '/dev/cu.usbmodem1411',
fd: 11,
readStream: [Object]
}
},
id: '946C9829-5DB0-4EA2-8283-6249CC8E25F6',
pin: 10,
mode: 4,
range: [0, 180],
type: 'standard',
specs: {
speed: 0.17
},
history: [{ timestamp: 1374513576399, degrees: 90 }],
interval: null,
isMoving: true,
_events: {
move: [Function]
}
}
在这个对象中,您不仅可以看到 Arduino 硬件在引脚和端口方面的表现,还可以看到所描述的 Firmata 以及您所连接的伺服系统的功能。除了棋盘的当前状态,还有一个历史数组,其中包含一段时间内的变化列表。当然,当您试图调试一段时间内多个输入和输出之间的复杂交互时,这是非常宝贵的。
我不能过分夸大在飞行中摆弄约翰尼五 REPL 环境的能力是多么神奇。正如 Raquel Vélez 早些时候指出的那样,她对 NodeBots 的部分兴奋之处在于,你可以快速构建原型。使用 REPL 环境,你可以在控制台上交互式地测试硬件直觉,在你把东西提炼成精确组合的程序之前,先粗略地画出草图。
福克斯机器人
虽然你确实得到了一个在你控制下旋转的伺服系统,但我很难称之为机器人。实际上,你可以花一整本书来解释和构建节点机器人。因此,我将这一章的范围限制在解释足以给你必要的背景来自己探索它们。以下是使用这种方法对机器人编程的几个关键概念:
- 在对硬件进行编程时,有可能发生火灾或其他现实世界中的灾难,但这并不意味着它会发生。
- 当你以一种意想不到的方式将计算机交织到现有的模拟物理过程中时,有趣的事情就发生了。
- 当你将虚拟世界的特性映射到物理空间时,有趣的事情就会发生。
- 网络感知对象可以被视为物联网的一部分。
- 反应式编程范式通过观察聚合对象之间的数据流来处理状态变化。
- 反应式编程特别适合于将异步世界转换成一系列可链接的事件流程。
- 许多传统的硬件开发使用编辑-编译-运行-调试(ECRD)过程,该过程允许硬件在独立的环境中运行,但是开发和调试可能会很慢。
- 节点机器人使用读取-评估-打印-循环(REPL)环境,这允许更快的开发和实时编码。然而,这种方法要求硬件被持久地束缚。
JS 社区的一部分人对 JavaScript 机器人的兴趣显而易见。以至于 NodeBot 开发人员已经产生了他们自己的网站、聚会和会议;甚至创造了一个国际节点机器人日,让书呆子们聚在一起互相焊接。如果你像我一样被这种潜力所吸引,我鼓励你去寻找其他有相似兴趣的人,然后开始建设!
额外资源
本章中的 Arduino 图表是使用 Fritzing 制作的,Fritzing 是一种开源硬件计划,旨在支持设计师、艺术家、研究人员和爱好者使用交互式电子设备进行创造性工作。它有一套很棒的工具和教程来帮助硬件新手规划并最终制作出他们自己的设计。点击这里了解更多:fritzing.org/。
Footnotes 1
http://en.wikipedia.org/wiki/Physical_computing
2
http://en.wikipedia.org/wiki/Internet_of_Things
3
http://voodootikigod.com/nodebots-the-rise-of-js-robotics/
4
http://arduino.cc/en/Main/Software
5
https://developer.apple.com/xcode/
6
http://arduino.cc/en/Main/arduinoBoardUno
7
http://arduino.cc/en/Main/Software
七、风格
Abstract
风格是不断浮出水面的主题的实质。
-维克多雨果
It is the essence that the style subject is constantly called to the surface. -victor hugo
我的目标是让你成为更好的 JavaScript 程序员。为此,我将在这一章教你关于风格的知识。我不是在谈论时尚,因为我认为大多数程序员都通不过这个测试——除非动漫展上有时装。我会解释风格的重要性,它是如何形成的,它是如何传播的,以及当你需要杀死它时,要寻找什么迹象。具体来说,我将研究应用于编写 JavaScript 的风格。一旦有了评估好技术的背景,我将向您介绍编程风格的元素,这些元素在我作为一名专业软件开发人员的这些年里很好地服务了我。
什么是风格?
风格经常被用来衡量质量。当某人被描述为有风格或时髦时,它几乎普遍被认为是一种补充。如果有人的风格受到质疑,通常是与另一个人的风格相比较。“我的风格是最好的,所以我向你挑战,”这位 20 世纪 70 年代的武术明星尖叫道。
时尚是一种新的方法,一种独特的视角,或者对一个主题的洞察力。一种风格的应用可以变得如此突出,以至于它扩展了活动本身,例如,通过说一所房子是以弗兰克·劳埃德·赖特风格建造的。绘画中的个人风格几乎可以在一夜之间变成一场艺术运动。风格像谣言一样传播。它是最初的迷因,一种永远改变你看待主题方式的思维病毒。风格是传播新思想的管道。
风格如何影响程序员?我有好消息给那些倾向于算法的人。不管一种风格看起来有多个人化,要让它存在下去,它必须是可重复的。风格必须被编成一系列的步骤或规则的组合,可以被遵循,然后被其他人认可。如果风格是质量的衡量标准,同时又是可重复的,那么它是可以被教授的。问问斯特伦克和怀特就知道了。
小威廉·斯特伦克在康奈尔大学任教授时写了《风格的要素》。他从 7 条语言使用规则和 11 条写作原则开始。他的学生埃尔文·布鲁克斯·怀特在近 40 年后修订了这本书,并增加了另外 4 条规则。这本书的目的是给有抱负的作家和语法学家一个评估他们自己作品的框架。
根据怀特的说法,斯特伦克决定写“小书”是出于对那些因阅读这位作家糟糕的写作而痛苦的人的同情:“威尔觉得读者大部分时间都处于严重的困境中,在沼泽中挣扎,任何试图写英语的人都有责任迅速耗尽交换,让读者站在干燥的地面上,或者至少扔一根绳子。”
多年来,这本书一直很受那些学习高效写作的人的欢迎,现在它被亲切地简称为“斯特伦克和怀特”这并不是说这本书受到了普遍的喜爱或追随。《纽约时报》援引多萝西·帕克的话说:“如果你有任何渴望成为作家的年轻朋友,你能帮他们的第二大忙就是送他们一本《文体要素》第一,当然是在他们高兴的时候,现在就开枪。"
许多人认为这些规定太过严格和固执己见。怀特说,斯特伦克认为“犹豫不决比犯错更糟糕。”斯特伦克的主张是,时尚不仅是激情,也是划定界限的能力,允许一个想法蓬勃发展,同时迫使另一个想法消亡。风格就像正弦波,吸引一些人,排斥另一些人。
什么是程序化风格?
如前所述,Stunk 和 White 写这本书不仅是为了授权和培训作者,也是为了让读者免于陷入他们心目中的文本陷阱。同样,好的编程风格服务于两种受众:开发人员和处理人员。代码应该在语法和技术上写得很好。下面的部分描述了我认为在编程风格的应用中必不可少的品质。
一致性
通过对代码库重复应用规则,可以确保一致性。一致性减少了源代码中的噪音,使编码者的意图更加清晰。如果一个开发人员试图拼凑如何阅读你的代码,你阻止了他理解它做什么。一致性与代码的外观有关,例如,命名约定、空格的使用和方法签名。它还涉及如何编写代码(例如,确保所有函数在所有上下文中返回可预测的结果)。
表示
代码本质上是一种符号语言,其中隐含着可变性和抽象性。因此,开发人员必须找到一种方法使代码对读者有意义和相关。相关性可以通过精确命名变量和函数来实现。当审查一个类、方法或变量时,读者应该通过阅读代码来理解代码的角色和职责。如果一个函数只能通过看作者留下的评论来理解,那么这应该是代码缺乏表现力的线索。
简洁
努力做到刚刚好。好的编程,就像好的写作一样,是关于目的的清晰,而不仅仅是紧凑。它应该是降低一个功能的复杂性,而不是它的有用性。
抑制
风格永远不应该压倒主题。在这一点上,风格成为主题,然后它是一个肤浅的技巧,一盘被过多的华丽破坏了的菜。我想起了我在大学里看到的一套极简主义的国际象棋。每一块都是白色或黑色的立方体,大小都一样。这些碎片只是重量不同;越重要的部分越重。这套国际象棋既美观又难下。在编程中,聪明是致命的。程序员必须克制自己,不要在语言中使用让代码潮人高兴的内部笑话,但要让源代码难以理解和维护。
JavaScript 风格指南
风格指南仅仅是指南。它们是为了给你指明正确的方向,但它们充其量只是一个不可改变的事实。编码理论是不断变化的,重要的是不要把自己锁在这些规则应用的教条方法中。正如我的克莱德·福勒教授在我的画室绘画课上告诉我的那样,“你必须用手思考。”他的意思是,你必须通过行动来思考,同时保持与工作保持临界距离的能力。
这份风格指南是通过编译、回顾和考虑我多年来在自己的工作中所做的选择,以及评估 JavaScript 社区中我所钦佩的个人和开发团队的编码实践而创建的。这种风格指南应该被视为输入和影响的融合,而不是单个人的创造性输出。你可以在本章末尾找到一个附加资源列表,其中包含我在撰写本文时考虑的其他指南和文档。本指南分为两个部分:“视觉清晰度规则”和“计算效率规则”
视觉清晰度规则
- 写得清晰而有表现力:在考虑代码视觉清晰度的好准则时,记住这条规则很重要。当命名变量和函数,或者组织代码时,记住你是为人类而写,而不是为编译器。
- 遵循现有的惯例:如果你在一个团队中工作,或者被雇佣来写代码,你不是在为自己写作。因此,你应该让你的风格与现有的生态系统共存,但不牺牲质量。
- 只用一种语言编写:在可能的情况下,不要使用 JavaScript 作为其他语言的载体。这意味着抵制编写内嵌 HTML 或 CSS 的冲动。清晰的代码加强了关注点的分离。
- 强制统一列宽:在源代码中争取一致的行长度。长线条使眼睛疲劳,降低理解能力。长的行也会导致不必要的水平滚动。行业标准是每行 80 个字符。
文档格式
理解一个程序的源代码通常需要读者在心里编译代码。这个过程需要读者的持续关注,任何分心都会将读者从他们的心流中驱逐出去。格式不正确或不一致的信号源会成为信号源信号的视觉噪声。本节提供了一些约定和指南,允许格式支持源,而不是降低它的重量。
命名规格
JavaScript 是一种由括号、数字和字母组成的简洁语言。让你的代码对人类有表达能力的唯一方法之一是给你的变量、函数和属性起一个有意义的名字。在选择名称时,它应该描述该对象的角色和职责。使用模糊或生硬的名字,如doStuff或item1,就像告诉读者去弄清楚,而他们通常不会。
选择具有有意义的、表达性的和描述性的名字的变量和函数。为读者而写,而不是为编译器。
// Bad
var a = 1,
aa = function(aaa) {
return '' + aaa;
};
// Good
var count = 1,
toString = function(num) {
return '' + num;
};
常数
在运行时引擎支持的地方,应该使用关键字const来声明常量。当const关键字不可用时,常量应该属于一个名称空间或对象。这样做有助于组织元素并防止命名冲突。在这两种情况下,常量都应该用大写字母书写,空格用下划线代替。
// Bad
MEANING_OF_LIFE = 43;
// Good
const MEANING_OF_LIFE = 43;
// Good
com.humansized.MEANING_OF_LIFE = 42;
// Good
Math.PI
其他命名约定
命名约定应该赋予变量或对象额外的含义。这样做是为了暗示它们的功能和语义目的。例如,JavaScript 中没有正式的类,但是类是组织代码的一种常见模式。因此,应该成为类的函数应该通过使用命名约定将自己与普通函数区分开来,即使运行时进程将对它们一视同仁。这种命名惯例有时被称为匈牙利符号。
变量应为 CamelCase:
myVariableName
类应该是 PascalCase:
MyAwesomeClass
函数应该是 CamelCase:
isLie(cake)
名称空间应该是 CamelCase,并使用句点作为分隔符:
com.site.namespace
匈牙利符号不是必需的,但可以用来表达对象是通过库或框架构造的,或者依赖于库或框架。
// jQuery infused variable
var $listItem = $("li:first");
// Angular.js uses the dollar sign to refer to angular-dependent variables
$scope, $watch, $filter
常量和变量
变量和常量定义总是在作用域的顶部,因为当运行时引擎处理代码时,变量会被提升到顶部。因此,在顶部声明变量更符合解析源代码时的情况:
// Bad
function iterate() {
var limit = 10;
for (var x = 0; x < limit; x++) {
console.log(x);
}
}
// Good
function iterate() {
var limit = 10,
x = 0;
for (x = 0; x < limit; x++) {
console.log(x);
}
}
通过始终使用var、let或const声明变量来避免污染全局名称空间:
// Bad
foo = 'bar';
// Good
var foo = 'bar';
let foo = 'bar';
const foo = 'bar';
使用单个var声明声明多个变量,但是用换行符分隔每个变量。这减少了不需要的字符,同时保持了源代码的可读性:
// Bad
var foo = "foo";
var note = makeNote('Huge Success');
// Good
var foo = "foo",
note = makeNote('Huge Success');
最后声明未赋值的变量。这使得读取器知道它们是需要的,但是延迟了初始化:
var foo = "foo",
baz;
不要在条件语句中分配变量,因为这通常会掩盖错误:
// Bad because it is easily misread as an equality test.
if (foo = bar) {...}
不要用变量名混淆函数参数,因为这会使代码更难调试:
// Bad
function addByOne(num) {
var num = num + 1;
return num;
}
// Good
function addByOne(num) {
var newNum = num + 1;
return newNum;
}
空白行
一个空行应该总是在注释的开始之前,因为它允许注释与它所引用的代码在视觉上分组:
var foo = "foo";
// Too compressed
function bar(){
// Hard to know here things are.
return true;
}
// Cleanly delineated
function baz() {
// Now return true
return true;
}
应该使用空行来分隔逻辑上相关的代码,因为读者可以在视觉块中处理相关的代码:
// Bad
var = wheels;
wheels.clean()
car.apply(wheels);
truck.drive();
// Good
var = wheels;
wheels.clean()
car.apply(wheels);
truck.drive();
逗号
删除对象声明中的尾部逗号,因为它们会破坏一些运行时环境:
// Bad
var foo = {
bar: 'baz',
foo: 'bar',
};
// Good
var foo = {
bar: 'baz',
foo: 'bar'
};
不要使用逗号开头的格式。有些人认为逗号优先格式提供了更好的可读性,因为它强调了逗号,因此提供了集合中元素的可视分隔:
var fruits = [ 'grapes'
, 'apples'
, 'oranges'
, 'crackers'
, 'cheese'
, 'espresso'
];
然而,大多数 JavaScript 开发世界使用逗号作为最后的格式,正如 Brendan Eich 指出的, 2 这两种风格不能很好地混合,当两种风格结合时很容易漏掉一个错误的逗号:
var fruits = [ 'grapes',
, 'apples'
, 'oranges'
, 'crackers'
, 'cheese'
, 'espresso'
];
分号
JavaScript 确定分号在某些上下文中是可选的,但在其他上下文中是必需的。让事情更加混乱的是,ECMAScript 规范对如何自动插入分号做出了规定:
Some ECMAScript statements (empty statements, variable statements, expression statements, do-while statements, continue statements, break statements, return statements and throw statements) must end with semicolons. Such semicolons may always appear explicitly in the source text. However, for convenience, in some cases, such semicolons can be omitted from the source text. The way to describe these situations is that in these cases, semicolons are automatically inserted into the source code tag stream.
您不应该添加无意义的分号,但它们应该用于清楚地描述逻辑语句的结尾,即使它们是自动插入的候选项。
空白
应该删除行尾和空行中的空白。开发人员不应该混合使用空格和制表符,在函数声明中,每个逗号后面都应该有空格。所有这些规则都有助于确保在各种可用的开发环境中以一致的视觉方式呈现源代码:
// Bad
findUser(foo,bar,baz)
makeSoup( );
var foo = { };
var arr = [ ];
// Good
findUser(foo, bar, baz)
空白不应出现在空函数或对象文字中,因为这会降低可读性:
makeSoup();
var foo = {};
var arr = [];
支架和大括号
只有在编译器需要或者有助于从外部源描述内部内容时,才使用括号和大括号。括号应该出现在需要它们的行上:
// Bad
if (hidden)
{
...
}
// Good
if (hidden) {
}
可读性应该胜过简洁。让自动代码压缩器来操心如何让代码变得更小:
// Bad
if (condition) goTo(10);
// Good
if (condition) {
goTo(10);
}
与括号和大括号一起使用的空格
在前面和括号之间添加空格以提高可读性:
// Less Readable
if(foo,bar,baz){}
// More readable
if (foo, bar, baz) {
}
前面的规则有几个例外:
// No whitespace needed when there is a single argument
if (foo) ...
// No whitespace when a parenthesis is used to form a closure
;(function () {...})
// No whitespace when brackets are used as a function argument
function sortThis([2, 3, 4, 1])
用线串
为了一致性,应该使用单引号来构造字符串,同时也是为了帮助区分对象文字和 JSON,后者需要双引号。
// Bad
var foo = "Bar";
// Good
var foo = 'Bar';
应该重新考虑长度超过预定字符行限制的字符串。并且,如果需要,应该将它们连接起来。
功能
方法签名必须一致。如果函数在一个上下文中返回一个变量,它应该在所有上下文中返回一个变量:
// Bad
var findFoo(isFoo){
if ( isFoo === 'Bar' ) {
return true;
}
}
// Good
var findFoo(isFoo) {
if ( isFoo === 'Bar' ) {
return true;
}
return false;
}
虽然不是必需的,但从函数中提前返回可以使意图更加清晰:
// Good
var findFoo = function(isFoo) {
if ( isFoo === 'Bar' ) {
return true;
}
return false;
}
评论
注释永远不应该跟在陈述后面:
var findFoo = function(isFoo); // Do not do this
应该谨慎使用注释;过度使用注释会让作者觉得他们的代码缺乏表现力。评论应该始终作为一个完整的思想来写。多行注释应该总是使用 Multiline 语法,因为它使您能够将使用单行语法编写的注释视为单独的项,而不是前一行的延续。
// Some really
// bad multiline comment
/**
* A well-formed multiline comment
* so there...
*/
计算效率的规则
计算效率和视觉清晰度同样重要。请记住下面的例子:
- 为连接而写:现代应用经常将 JavaScript 源代码转换成一个流线型的文件用于生产。您应该对您的脚本进行防御性编程,以防止操作上下文和范围损坏中的切换。
- 保持代码浏览器不可知:通过将特定于浏览器的代码抽象到接口中,保持业务逻辑不受这些代码的影响。随着浏览器的流行和过时,这将使你的代码保持一个干净的升级路径。
- 抵制
eval()的使用:它经常是恶意代码执行的注入点。 - 抵制使用
with():它会使代码的含义难以理解。3 - 保持原型的原始性:永远不要修改 Array.prototype 这样的内置函数的原型,因为它会悄悄地破坏其他人的代码,而这些代码需要标准的行为。
相等比较和条件评估
用===代替==并使用!==而不是!=因为 JavaScript 的动态性意味着在测试等式时有时会过于宽松。
当只是测试“真实性”时,您可以强制这些值:
if (foo) {...}
if (!foo) {...}
测试空性时:
if (!arr.length) { ... }
在检验真理时,你必须明确:
// Bad because all of these will be coerced into true
var zero = 0,
empty = "",
knull = null,
notANumber = NaN,
notDefined;
if (!zero || !empty || !knull || !notANumber || !notDefined ) ...
// Bad
var truth = "foo",
alsoTrue = 1
if (truth && alsoTrue) ...
// Good
if (foo === true) ...
常量和变量
删除变量时,将其设置为 null,而不是调用#delete或将其设置为 undefined:
// Bad because undefined means the variable is useful but as yet has no value
this.unwanted = undefined;
/**
* Bad because calling delete is much slower than reassigning a value.
* Use delete if you want to remove the attribute from an objects list of keys.
*/
delete this.unwanted;
// Good
this.unwanted = null;
函数表达式
函数表达式是链接到变量的函数对象。因此,它们可以用比函数声明更多的方式来编写:
// Anonymous Function
var anon = function () {
return true;
}
// Named Function
var named = function named() {
return true;
};
// Immediately-invoked function, hides its contents from the executing scope.
(function main() {
return true;
})();
函数表达式是在解析时定义的。因此,不要将他们的名字挂在范围的顶部。然而,函数表达式比函数声明更受欢迎,因为在旧的浏览器中存在某些错误。
// Bad - Runtime Error
iGoBoom();
var iGoBoom = function () {
alert('boom');
}
// Good
iGoBoom();
function iGoBoom() {
alert('boom');
}
不要在 block 语句中使用函数声明;它们不是 ECMAScript 的一部分。请改用函数表达式:
// Bad
if (ball.is(round)) {
function bounce(){
// Statements Continue
}
return bounce()
}
// Good
if (ball.is(round)) {
var bounce = function () {
// Statements Continue
}
}
在提高清晰度的地方打破连锁方法:
// Bad
jQuery.map([1,3,2,5,0], function(a) { return a + a; }).sort(function(a, b) { return a - b;});
// Good
jQuery.map([1,3,2,5,0], function(a) { return a + a; })
.sort(function(a, b) { return a - b;});
不要在函数中使用相同的名称来隐藏本机参数对象:
// Bad
var foo = function(arguments) {
alert(arguments.join(' '));
}
// Good
var foo = function(args) {
alert(args.join(' '));
}
目标
对象文字符号应该比new Object() when creating an empty object because the object literals scope does not need to be first resolved and therefore performs better. Additionally, the object literal syntax is less verbose:更受青睐
// Ok
var person = new Object();
person.firstName = "John";
person.lastName = "Doe";
// Better
var person = {
firstName: "John",
lastName: "Doe"
}
不要将保留字覆盖为键,因为这样做会模糊对这些属性的访问,这可能会产生意想不到的后果:
// Bad
var person = { class : "Person" };
// Good
var person = { klass : "Person" };
数组
为了清晰和简洁,使用字面语法创建一个new Array()。
// Verbose
var arr = new Array();
// Succinct
var arr = [];
关注点分离
只写由程序负责的代码。让您的代码远离视图层和模板代码:
var view = {
title: "Joe",
calc: function () {
return 2 + 4;
}
}, output;
// Bad
output = '<div><h5>' + title + '</h5><p>' + calc() + '</div>';
// Good
var output = Mustache.compilePartial('my-template', view);
将 JavaScript 排除在 HTML 之外:
// Bad
<button onclick="doSomething()" id="something-btn">Click Here</button>
// Good
var element = document.getElementById("something-btn");
element.addEventListener("click", doSomething, false);
Note
JavaScript 中有很多模板库,比如 mustache.js、 4 可以帮助你从 JavaScript 中提取 HTML。
运营背景和范围
在可能的情况下,将您的代码包装在立即调用的函数表达式(IIFE)中。它使您的代码免受他人的污染,并使其更容易抽象成可重用的模块。
// Good
;(function( window, document, undefined) {
// My Awesome Library
...
})(this, document);
为持续时间不可知的代码执行而设计,这可以防止您的代码累积可能不再相关的请求。
// Bad because this might take longer than 100 milliseconds to complete.
setInterval(function () {
findFoo();
}, 100);
// Good this will only be called again once findFoo has completed.
;(function main() {
findFoo();
setTimeout(main, 100);
})();
为了防止破坏,声明操作上下文的社区代码(例如,use strict))应该包装在模块的生命中,或者在需要时包装在函数中:
// Bad
var bar = findLooseyGoosey();
"use strict";
var foo = findStrictly();
// Good
var bar = findLooseyGoosey();
;(function () {
"use strict";
var foo = findStrictly();
})();
var findStrictly = function() {
"use strict";
}
强迫
使用显式转换而不是隐式强制,因为它使代码库更具声明性:
var num = '1';
// Bad implicit coercion
num = +num;
// Good expressive conversion
num = Number(num);
强制风格
正如我前面所讨论的,要使风格存在,它必须被编成一系列可以重复的规则。在团队环境中编写代码的最大挑战之一是在开发人员之间保持统一的风格。幸运的是,对于个人和团队来说,有几种方法可以确保遵循风格。
美容师
美化程序是通过使用一系列格式约定将样式统一应用于源代码来处理代码的程序。通常,美化程序被连接到一个工作流程中,当保存一个被关注的文件时,它们会自动运行。美化器也用于从源文件中解包或移除混淆(巧合的是,代码打包有时也称为丑化)。两个流行的美化器是 JS 美化器和 CodePainter,其灵感来源于微软 Word 的 format painter。许多美化器允许您使用配置对象或命令行参数手动指定格式规则。
我们来快速看一下 JS 美化界面和选项。首先,你必须从 NPM 下载安装 JS 美化。在这个例子中,提供了一个-g标志,它全局安装 JS 美化:
npm -g install js-beautify
安装后,您可以直接从命令行进行美化,如下所示:
js-beautify jquery.min.js
下面是 JS 美化支持的命令行和美化器选项的列表。
CLI 选项:
-f, --file Input file(s) (Pass '-' for stdin). Can also be passed directly.
-r, --replace Write output in-place, replacing input
-o, --outfile Write output to file (default stdout)
--config Path to config file
--type [js|css|html] ["js"]
-q, --quiet Suppress logging to stdout
-v, --version Show the version
-h, --help Show this help
更漂亮的选择:
-s, --indent-size Indentation size [4]
-c, --indent-char Indentation character [" "]
-l, --indent-level Initial indentation level [0]
-t, --indent-with-tabs Indent with tabs, overrides -s and -c
-p, --preserve-newlines Preserve existing line-breaks (--no-preserve-newlines disables)
-m, --max-preserve-newlines Maximum number of line-breaks to be preserved in one chunk [10]
-j, --jslint-happy Enable jslint-stricter mode
-b, --brace-style [collapse|expand|end-expand] ["collapse"]
-B, --break-chained-methods Break chained method calls across subsequent lines
-k, --keep-array-indentation Preserve array indentation
-x, --unescape-strings Decode printable characters encoded in xNN notation
-w, --wrap-line-length Wrap lines at next opportunity after N characters [0]
--good-stuff Warm the cockles of Crockford's heart
通过 IDE 实施
许多流行的集成开发环境(ide)提供了多种方法来调整和配置它们的功能,以支持个人的格式需求。通过宏和格式化引擎,这些编辑器使开发人员能够自动处理关于使用空白、行尾或制表符等的决策。理论上,这些工具应该给开发人员一种方法来处理样式指南中一些容易实现的格式化成果。然而,如前所述,有许多因素,如团队偏好、语言要求和个人选择,都会影响风格的定义。这些可变性使得任何开发人员都不太可能保持理智,不得不手动配置他们的 ide 来支持每个新项目的样式需求。
为了解决对灵活的项目级配置系统的需求,开发人员已经开始采用允许他们指定样式规则作为项目配置设置的一部分的工具。最流行的项目之一是编辑器配置项目。项目的维护者是这样描述目标的:
EditorConfig helps developers define and maintain a consistent coding style between different editors and ide. The EditorConfig project consists of a file format for defining coding styles and a set of text editor plug-ins, which enable the editor to read the file format and follow the defined styles. EditorConfig files are easy to read, and they work well with the version control system.
一旦安装到支持的 IDE 中,EditorConfig 插件就会扫描一个名为.editorconfig的隐藏配置文件,然后相应地调整格式设置。
在下一节中,我将描述 EditorConfig 可以控制的一些属性,以及开发人员如何实施编码风格的基线。考虑下面的例子,其中.editorconfig config 放在 JavaScript 应用的根目录中:
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# Top most config file
root = true
# Base style guide to apply to all files unless overridden by lower rules.
[*]
# Define end of line options
# Available options are "lf", "cr", or "crlf"
end_of_line = lf
# Define character set options
# "latin1", "utf-8", "utf-8-bom", "utf-16be" or "utf-16le"
# Note: Use of "utf-8-bom" is discouraged.
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# Commonly user-defined settings
indent_style = space
indent_size = 2
# Indentation override for all JS under lib directory
[lib/**.js]
indent_size = 4
# Markdown file configurations
[*.md]
trim_trailing_whitespace = false
正如您在前面的配置文件中看到的,EditorConfig 文件为开发人员提供了一个易于使用的工具来执行某些高级格式化任务。不幸的是,这个工具从来没有打算强制执行前面定义的一些语义最佳实践。为了统一实施 JavaScript 风格,需要一个专门为此工作设计的工具。输入 JSHint。
通过 JSHint 实施样式
JSHint 最初是由 Anton Kovalyov 编写的,它是实施代码风格的另一个很好的选择。JSHint 最初是道格拉斯·克洛克福特 JSLint 项目的一个分支。这两个程序的工作方式基本相同:一行一行地遍历源文件,并列出潜在问题或偏离可接受风格的地方。
许多人认为 JSLint 过于固执己见,尽管 JSLint 的目标是检测 JavaScript 程序中的潜在错误和疏忽,但它也迫使开发人员以任意的形式编写 JavaScript,这并不一定是对他们现有方法的改进。JSLint 的源代码暗示了这种紧张关系:
"WARNING: JSLint will hurt your feelings."
Kovalyov 松开了 JSLint 的螺钉,并试图将关于风格的观点与静态代码分析的需要分开。通过这样做,JSHint 成为了原版的一个更友好、更温和的版本。JSHint 网站在描述其目标时提到了这一点:
Our goal is to help JavaScript developers write complex programs without worrying about spelling mistakes and language traps. We believe that static code analysis programs-and other code quality tools-are important and beneficial to the JavaScript community, so we should not alienate their users.
如前所述,JSHint 的目标之一是提供一种配置 linter 的方法,使得它只强制执行团队或个人寻求推广的编码约定。通常,JSHint 的选项分为四个主要类别:遗留、可执行、可放宽和环境选项。每个类别都包含许多不同的选项——事实上,太多了,无法在此一一列举。为了说明这一点,我将给出每个类别的几个典型例子,但是如果您感兴趣,我鼓励您详细阅读文档。
- 可执行选项:顾名思义,这些额外的选项可以由 jsHint 执行。这里有几个例子:
camelcase (true | false) // This option allows you to enforce camelCase style for all variable names.
undef (true | false) // Prevents you from defining variables which are initially undefined. Often times when this happens it is because a variable was declared but never used at all.
- 宽松的选择:一些对一个人来说是最佳实践的规则对另一个人来说却很烦人。JSHint 知道这一点,并提供了一组选项来减少默认情况下由 linter 触发警告的情况。例如:
evil (true | false) // It is almost universally agreed that the use of eval is a bad idea because it exposes a conduit for a third-party to inject malicious code and have the host application execute it.
debug (true | false) // This option allows you to suppress warnings about any use of the debugger statement in the code.
- 环境选项:这一类别中的选项定义由其他库(如 jQuery 或 Nodejs)公开的任何全局变量。
jquery (true | false) // whether or not to expose the global $ or jQuery variables.
Caution
在你继续之前给你一个警告。JSHint 等静态代码分析工具只验证代码的语法结构。这对于捕捉小错误或风格不一致是一个巨大的素材,否则可能会从日常开发的裂缝中溜走。然而,这些工具不能告诉你所写的代码是否真的达到了预期的目的。为此,开发人员需要在各种不同的环境下测试代码,以确保它能按预期执行。
摘要
在这一章中,你学到了风格是一个过程的独特方法。对于风格的存在,它必须被编纂成一系列可重复的步骤。与 JavaScript 相关的风格旨在使代码更具表现力,更易于阅读和理解,并以最小化可能引入 bug 的潜在缺陷的方式编写。
程序员在交易时应该记住几条规则。格式和命名约定要一致。通过使用描述变量和函数用途的名称来表达。通过编写包含关注点分离的模块化代码来保持简洁,其中函数和变量只有一个任务。保持克制,拥抱 JavaScript 的简洁,但不要以牺牲可读性为代价。
你可以在这里找到更多关于美化者的信息:
你可以在这里找到更多风格指南:
- “写作原则一致,惯用 JavaScript:
https://github.com/rwldrn/idiomatic.js/ - 谷歌 JavaScript 风格指南:
http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml - Airbnb JavaScript 风格指南:
https://github.com/airbnb/javascript - jQuery 风格指南:
http://contribute.jquery.org/style-guide/js/
Footnotes 1
http://www.nytimes.com/2009/04/22/books/22elem.html?_r=0
2
https://mail.mozilla.org/pipermail/es-discuss/2011-September/016802.html
3
http://yuiblog.com/blog/2006/04/11/with-statement-considered-harmful/
4
https://github.com/ja/nl/mustache.js/
八、工作流程
Abstract
一场反常的暴风雪让我明白了如何通过改进我的工作流程来使 JavaScript 应用开发更快、更有趣、表面上更有利可图。本章的目标是教其他人如何做同样的事情。
一场反常的暴风雪让我明白了如何通过改进我的工作流程来使 JavaScript 应用开发更快、更有趣、表面上更有利可图。本章的目标是教其他人如何做同样的事情。
不要铲雪
不要把行动和成就混为一谈。—约翰·伍登
我们在堪萨斯州遇到了一场巨大的暴风雪,人们亲切地称之为“奥兹国的暴风雪”像许多有学龄儿童的人一样,我们的房子是分开的。我们的孩子期待着放学后的一天,在外面嬉戏,回到温暖的可可杯,舒适地坐在炉火旁。我和妻子担心这场暴风雪会把我们淹没在工作的雪崩中。
像所有善良的堪萨斯人一样,在暴风雨的那一天,我尽职尽责地准备好与大自然母亲战斗。我给自己穿了好几次衣服,让我的四肢裹上层层温暖。然后我摇摇摆摆地走进车库,从挂着塑料红色雪铲的挂板上取下它。我想象自己是一个维京人,从石制壁炉上方打开他沾满鲜血的战斧。我打开车库门,走向车道上洁白的原始景观。
铲了几分钟后,“诚实劳动”的新鲜感已经消失了。取而代之的是麻木地意识到,我将不得不做一上午挖掘车道的苦差事。像许多其他非专业铲雪人员一样,我做的是这项工作,而不是支付我的账单,即设计和开发软件。然后,我开始计算在我已经浪费的计费时间里,我本可以购买多少台吹雪机,深感沮丧。
在这一点上,我对我的情况以及它与软件开发的关系有了一个顿悟。我遇到的是工作流程问题。我从事的是一项暂时重要但从长远来看毫无价值的任务。上午的大部分时间,我都在把车道上的雪沿着我的院子边缘堆成令人印象深刻的白色小山。这个过程花了我几个小时,但很快太阳就会抹去这一艰苦工作的所有证据。
我开始想,在我的日常开发过程中,有哪些任务是像铲雪一样的。这些任务似乎是必不可少且不可避免的,但最终可以通过更好的工具或更清晰的视角来更快地完成。
什么是工作流
当用 JavaScript 或任何语言开发一个项目时,每个项目都会经历不同的阶段。经理们发现给这些阶段命名很有用(例如,“计划”、“开发”、“测试”和“部署”)。然后,他们将日常任务分成一个阶段或另一个阶段。当他们这样做时,他们采用了工作流,简单地说就是定义和应用规则的过程,以控制任务、数据和附属资料如何以及何时从一个人传递到另一个人。
工作流通常与团队遵循的更大的开发方法紧密结合。例如,敏捷团队可能会遵循强调紧密迭代和较小开发阶段的工作流。而瀑布团队可能会强制执行一个工作流规则,确保在没有完整的规范之前不能构建任何东西。工作流的目标是最大化生产力和最小化复杂性。
然而,这种愿望说起来容易做起来难。通常,工作流的正确实现是一种平衡行为,只有通过定义足够精确的规则才能完全遵循,而不会限制正在开发的过程或产品中的创新或改进。当一个工作流减慢了开发速度的时候,就是它需要被重新评估的时候。
明智的 JavaScript 开发工作流程
尽管我之前说过工作流通常是由团队或者开发方法决定的,但是开发人员也有自己的工作流。本节描述了 JavaScript 开发的合理的个人工作流程,它分为六个阶段:工具选择、引导、开发、测试、构建和支持。图 8-1 显示了该工作流程。
图 8-1。
A diagram that visualizes the workflow process
工具选择
在你建造任何东西之前,你必须选择合适的工具。在这一阶段,开发人员通过对编码环境做出重要选择,并确定所需的任何外部资源(例如,库或应用框架),为应用奠定基础。工具的选择对应用有持久的影响,甚至在选择工具的人离开之后。您不仅是在为自己选择开发栈,也是在为后来的其他开发人员选择开发栈。本节致力于了解如何选择正确的工具,从哪里获得它,以及如何使它保持最新。
在我年轻的时候,我受训成为一名艺术家。我们画画时,我的绘画教授克莱德·福勒给学生们讲课。我们把画架随意地放在房间中央的模型或静物周围。克莱德会慢慢地绕着画室的外环转,停下来向某个学生提供反馈;但通常只是自言自语。有一天,当我正沉迷于正确地渲染袋子褶皱中形成的阴影时,他对全班同学说,对艺术知之甚少的人会说这样的话:“我不懂艺术,但我知道我喜欢什么。”事实上,他断言他们真的在说,“我不懂艺术,但我喜欢我所知道的。”直到后来,我才真正理解这个概念。当选择正确的工具时,你喜欢你所知道的东西的心态是不稳定的。如果你正处于压力之下,要尽可能快地表现出进步,那就更是如此。在这个阶段,目标应该是选择适合项目的工具,而不是适合开发人员的工具。
我知道开发人员很容易选择他们最熟悉或最擅长的工具。这具有直观的意义,因为如果你精通一种工具,那么你在使用它时会更有效率。虽然这可能是真的,但它也让开发人员成为被称为“工具法则”的思维定势的牺牲品这意味着你总是选择你最喜欢的工具,即使它对于手头的任务来说是错误的选择。
亚伯拉罕·马斯洛完美地总结了这一点,他说:“我认为,如果你唯一的工具是一把锤子,那么把一切都当成钉子是很诱人的。”换一种说法,你可能是用锤子的专家,但没人愿意住在只用锤子盖的房子里。
订购工具
直到最近,如果您想将外部 JavaScript 添加到您的应用中,您要么复制代码并粘贴到您现有的脚本文件中,要么下载源代码的副本并使用 Script 标记将其包含到您的页面中。在我讨论这些工具的实际集成之前,让我快速地转移话题,首先讨论如何对它们进行排序。
选好工具后,你需要知道去哪里买。作为一个思维实验,想象一下你在现实生活中可能会如何购买一把锤子。最有可能的是,你会选择一家商店购买。选择商店时,你要考虑商店本身的几个因素——可能是价格、便利性和退货政策。现在想象一下,我们把这三个方面重新投射到软件工具上。
价格
价格是将这个工具集成到您的项目代码库中所花费的成本(时间、精力、理智)。当评估一个软件工具的价格时,你想知道它的编写、支持和测试有多好。将价格视为使用该工具所承担风险的一个因素。目标是以最小的代价获得最大的价值。考虑一下使用 jQuery 的代价,它是世界上最流行的 JavaScript 库之一。它拥有庞大的用户基础(支持),由该领域的专家编写(代码质量)。最后,开发人员已经将它集成到几乎所有可以运行 JavaScript 的平台中(经过测试)。因此,jQuery 很可能比你自己花一个周末写的库价格更低。
便利
对我来说,在网上买锤子比开车去当地的大商店要方便得多。除非我马上需要它;然后当地商店轻而易举地赢了。假设我想要的软件工具是 jQuery。去它的网站下载我自己的副本并把它直接放在我的应用的源文件夹中似乎很方便。这样做相当于去附近的五金店买一把锤子。但是,如果我后来想更改 jQuery 的版本,会发生什么情况呢?我需要回到网站重新下载吗?我如何首先知道是否有新版本?我需要一次又一次地去它的网站等待新的发布吗?突然,这似乎不太方便。在软件世界中,这个任务通常由包管理器来处理,我将在后面详细解释。
退货政策
拿到锤子后讨厌了怎么办?如果我买它的商店不接受退货,我已经有效地把这个锤子的价格加到了我买的下一个锤子上,因为我不能退货。发展中也是如此。如果将一个工具集成到您的代码库中需要花费大量的精力,那么以后提取它也很可能需要同样多(或者更多)的精力。对于一个软件工具来说,有一个好的返回策略,意味着从你的代码库中提取它是很容易的。这就是包管理器的用武之地。
JavaScript 包管理器
包管理器是管理软件工具集合的应用,它实现自动安装、配置、更新和从各种平台上删除它们的方法。包管理器解决了工作流开发中的三个主要问题:依赖性管理、升级路径保护和软件工具管理。包管理并不是什么新鲜事。许多编程语言,如 Perl 或 Ruby,多年来一直享有完善的包管理器。
直到最近,人们还很少意识到 JavaScript 需要类似的解决方案。许多人认为脚本语言不需要额外的开销,处理这种情况的最好方法就是复制并粘贴到一个工作应用中。随着 JavaScript 的流行和使用的增长,包管理的需求和选择也在增长。这里只是 JavaScript 包管理的流行选择的一个例子:NPM、鲍尔、安德和组件!
为了说明为什么在 JavaScript 开发工作流中集成包管理器是值得努力的,我将一次一个地探索它们解决的隐含问题。我将以鲍尔为例。
人民的凉亭
我选择 Bower 作为包管理器来演示,不仅因为它提供了完美的双关语,还因为 Bower 专注于前端。对于许多 JavaScript 开发人员来说,前端是他们花费大部分时间的地方。Bower 支持许多不同的包类型,它甚至拥有一个强大的 API,开发人员可以与之交互并对其进行编程。让我们来看看如何让 Bower 运行起来。讽刺的是,Bower 是通过另一个包管理器(npm)分发的。因此,首先您需要安装 npm。npm 准备就绪后,Bower 可以安装在一条线上:
npm install -g bower
要安装最新的最棒的 jQuery,您可以简单地这样写:
bower install jquery
这个命令使 Bower 从远程服务器克隆适当的存储库。Bower 在开发人员系统的本地缓存中维护自己的组件目录。在本地缓存后,Bower 会制作一份软件副本,并将其放在与运行安装命令的路径相关的 bower_components/目录中。
您可以轻松地查看特定版本的 jQuery:
bower install jquery#1.6.0
如果您检查签出的代码,您会发现一个 jquery 目录。在这个文件中,您只会发现两个文件:component.json 和 jquery.js。
鲍尔小心
在继续之前,必须说明的是,Bower 不对上传到其存储库的包进行任何验证或确认。很容易认为这些工具集合是经过审查的,或者是以某种方式正式提供的。他们不是。你不会因为某样东西适合你的嘴而吃它。你也不应该仅仅因为 Bower 有可用的包就安装它。
依赖性管理
到目前为止,我一直用锤子来比喻软件工具。诚然,这种心理形象有点做作,但也有误导性。锤子的伟大之处在于,在你理解了它的工作原理之后,每次你使用它时,它的表现都是一样的。你不必担心你选择的螺丝刀会影响锤子的功能。不幸的是,发展中的情况并非如此。大多数软件工具依赖于一系列其他程序。这种分层是编程的本质所固有的,意味着这些工具之间形成了依赖链。这些链条经常缠绕在你的电脑内部。如果另一个程序修改了共享链中的一个链接,它会对你的系统造成严重破坏。而且作为一个额外的奖励,它经常默默地做到这一点。
包管理器试图通过使用配置文件来保护这些链,它们就像一个配方一样遵循这些配置文件。配置中的每个属性都告诉软件包管理器如何安装软件,以及它所依赖的程序(如果有的话)。在包管理器中有一个惯例,将一个配方放在源代码树的根中。Bower 将这些配置文件称为 bower.json 文件。下面是一个 jQuery 的 bower.json 文件的例子:
{
"name": "jquery",
"version": "1.8.0",
"main": "./jquery.js",
"ignore": [],
"dependencies": {},
"devDependencies": {},
"gitHead": "a81132c96b530736a14a48aad3916b676d102368",
"_id": "jquery@1.8.0",
"repository": {
"type": "git",
"url": "git://github.com/components/jquery.git"
}
}
这个对象的结构非常简单:
- 名称:这是必需的。当然,这也是你项目的名字。
- 版本:这是一个语义版本号。
- main:这是可以在其中找到软件的端点的字符串或数组。
- 忽略:一些应用定期生成文件作为开发人员的辅助,例如记录文件的异常或创建隐藏的资源文件夹。通常,这些文件只对工具的创建者重要,安装软件包的开发人员可以忽略它们。该指令允许您指定要忽略的各种路径。
- 依赖性:这是 Bower 开始为您做繁重工作的地方。这个指令是一个 JavaScript 散列,它定义了其他工具及其细节的列表,比如软件在生产中运行所需的版本号。Bower 要么在本地缓存中查找兼容版本,要么从远程位置下载。
- devDependencies:一些工具指定只在开发期间需要的依赖。许多编写良好的工具还附带了验证功能的单元测试。创建者可以将测试框架添加到依赖列表中,然后 Bower 会在开发过程中包含它,在为产品编译代码时忽略它。
- gitHead:随着项目的发展,新的代码会取代旧的代码。Git 为每个提交分配一个唯一的散列,这允许 Bower 引用软件项目生命周期中的一个精确时刻。通过这种方式,Bower 可以检查特定版本的 jQuery 或任何其他项目。
- _id:用于引用特定组件的内部 id。
- 存储库:描述用于存储软件工具的源代码控制的位置和类型的散列。
保护升级路径
许多软件包管理器,比如 Homebrew,在系统范围内安装软件包。这通常意味着您不能同时安装一个以上版本的工具。
如前所述,鲍尔采取了不同的方法。Bower 试图只管理前端所需的软件,并且一次只管理一个应用。通过划分每个应用的依赖关系,开发人员不必担心指定最新版本的 jQuery 会如何影响以前使用旧版本的应用。早些时候,Bower 检查了 jQuery 的一个旧版本。如果您后来决定签出 jQueryUI,您可以这样做:
bower install jquery-ui
在检查 bower_components 目录时,您会看到 bower 添加了一个新文件夹:jquery-ui。等待;还有呢!如果您重新调查 jquery 文件夹,请注意 Bower 自动将它更新为较新的版本,因为在 jquery-ui 的 bower.json 文件中,它列出了 jQuery 的一个特定依赖项:
"dependencies": {
"jquery": ">= 1.8"
},
如您所见,它需要新版本的 jQuery。任何高于 1.8 的版本都可以工作。Bower 签出了 jQuery 的最新版本,并替换了旧版本。
相对于自己动手的方法,包管理器提供的最后一个好处是,它们提供了一种方法来轻松处理关于查找、集成和删除工具的常见任务。您已经看到了安装工具是多么容易。卸载一个也很容易:
bower uninstall jquery
conflict jquery-ui depends on jquery
bower warn To proceed, run uninstall with the --force flag
请参见已卸载...哦,等等,实际上这个命令失败了,因为 jQuery-ui 依赖于 jQuery。如果现在卸载 jQuery,将不再满足 jQuery-ui 的一个依赖项。如果您仍然希望看到世界毁灭,您可以通过在命令末尾提供- force 标志来强制卸载。
除了 Bower 能够节省您的时间之外,它还具有一些节省时间的功能,可以让您更容易地找到并安装软件。Bower 提供了一个强大的界面来搜索和查找与您的兴趣相关的包。例如,如果您想查看有哪些 jQuery 或相关插件可用,您可以首先像这样搜索 Bower:
bower search jque
Search results:
- jquery git://github.com/components/jquery.git
- jquery-ui git://github.com/components/jqueryui
jquery.cookie git://github.com/carhartl/jquery-cookie.git
... results continue...
如果您想查看您已经在本地安装了哪些包,您可以让 Bower 为您列出它们:
bower ls
/Users/heavysixer/Desktop/bower
■??]
ε□□□□□□□□□□□
ε──??″
请注意,Bower 不仅列出了您已经安装的包,还列出了每个包的依赖项。
之前您试图卸载 jQuery,但是被 Bower 的依赖管理器阻止了。如果这个命令成功了,Bower 仍然有一个隐藏的包的本地缓存,您可以使用它在以后重新安装。如果您想清除本地缓存,您可以这样做:
bower cache-clean
Bower 致力于解决整个包管理问题的一小部分:前端开发的组件控制。Bower 的开发人员明白,尽管这是一个有待解决的开放性问题,但他们的部分成功取决于 Bower 与构建堆栈中的其他流程集成的能力。
如今,许多应用都经历了一个分层部署过程,在这个过程中,代码被发送到一个编程传送带上,进行净化、缩小、模糊、打包和部署。为了让开发人员采用 Bower,它必须找到一种方法与其他外部流程共存。Bower 的解决方案是公开一个简单的高级 API,允许程序员编写脚本。难道你不知道吗,它是用 JavaScript 写的!下面是它如何工作的一个例子:
var bower = require('bower');
bower.commands
.search('jquery', {})
.on('packages', function(packages) {
/* packages is a list of packages returned by searching for 'jquery' */
});
在这个片段中,您可以看到一些外部脚本需要 Bower。一旦定义完毕,Bower 实例就会被发出一个命令,要求它搜索任何名称中带有“jquery”的可用包。Bower 的 API 被设计成可选地发出事件来响应发出的命令。调用脚本可以为这些发出的事件注册一个侦听器。
在玩具脚本中,您正在监听“packages”事件,当调用该事件时,它允许您遍历 Bower 返回的 jQuery 包列表。Bower 发出的其他一些常见事件是数据、结束、结果和错误。
在这一节中,您学习了如何选择工具,从哪里获得工具,以及如何将管理这些工具的繁重工作交给像 Bower 这样的包管理人员。
在下一节中,您将探索如何使用工具来生成代码,这将帮助您进行开发。开始自举吧!
拔靴带
在道格拉斯·恩格尔巴特(Douglas Engelbart)通过他的 Bootstrap Alliance 推广“Bootstrap”概念之前,这个术语描述的是独自完成一项通常需要多人完成的任务的不合理尝试。想象一下靠自己努力奋斗的荒谬。随着时间的推移,这个词开始反映企业家不屈不挠的内在精神,他们利用有限的资源快速创业。自力更生的努力就像建造一架通往天堂的梯子,它主要是用胶带粘在一起的。
与开发相关的自举涉及到程序员试图用代码生成器、插件、框架和现有代码快速启动代码库。在这个阶段不编写自定义代码。相反,开发人员利用任何现成的东西来解决他们项目的一般特性。
自举不仅仅是通过将一套部件组合在一起来解决一般性的问题,还包括使用代码来编写代码。像 Ruby on Rails 这样的框架将代码生成的概念融入到了它们的 DNA 中。它们有接受自定义参数的生成器,允许开发人员快速创建定制的代码块。由于其剪切粘贴文化,JavaScript 很难理解这个概念。相反,JavaScript 中的自举通常包括将大量的库转储到一个文件夹中,并将它们连接到 HTML 页面中。在过去的两年里,JavaScript 社区对生成器充满了兴趣。
这种对生成器的支持在 Yeoman 中表现得最为明显,这是一个由 Google 开发人员编写的固执己见的工作流工具。和 Rails 一样,Yeoman 强调代码编写代码的概念。历史上,自耕农是王室的一种文书随从。顾名思义,Yeoman 项目试图将一些管理开发工作流的单调工作从开发人员身上剥离出来。
正如我用 Bower 解释了关于工具选择的突出问题一样,我将同样用 Yeoman 解释与 JavaScript 开发工作流相关的引导和任务自动化。本演示将带您完成安装和配置 Yeoman 的过程,并搭建一个基本的 AngularJS 应用。
使用约曼
在让约曼开始工作之前,你必须安装它。npm 已经就绪,您只需在控制台中键入以下命令:
npm install -g yo grunt-cli bower
Note
如果您使用的是 Yeoman 之前的版本,您可能需要清除 npm 缓存才能使用该命令。以前,您不能使用-g 标志来指定全局安装。您可以强制 npm 清除缓存并像这样更新 Yeoman:NPM 缓存清除&& npm 更新-g yo
这段代码安装了 Yeoman、grunt 命令行界面(CLI)和 Bower 包管理器(如果您正在学习,应该已经安装了)。从 install 命令就可以看出,Yeoman 通过将一系列相关技术结合在一起来帮助开发人员。美国约曼公司利用这些项目为四个主要目标服务,这些目标将在下面的章节中讨论。
脚手架
Yeoman 让开发者有机会使用各种预定义的模板作为构建的基础。这些模板中有许多是基于知名项目构建的,比如 HTML5 样板、Twitter Bootstrap 或 AngularJS。
现成的 Yeoman 官方支持几个生成器:webapp、angular、backbone、bbb、ember、chromeapp、chrome-extension、bootstrap、mocha 和 karma。我把它作为一个练习留给读者去探索每一个。
约曼提供了一种直接从 npm 下载和安装新发电机的机制。在搭建 AngularJS 应用之前,必须确保安装了 AngularJS 生成器:
npm install generator-angular
安装后,您可以使用以下代码搭建 AngularJS 应用:
yo angular
这段代码调用 AngularJS 生成器。一旦运行,程序通过一系列是或否的问题提示开发人员,同时试图确定更多关于项目基本需求的信息。为了简单起见,我对所有这些问题的回答都是否定的。完成后,Yeoman 会生成类似如下的输出:
create app/styles/bootstrap.css
create app/index.html
create component.json
create package.json
create Gruntfile.js
invoke angular:common:/Users/heavysixer/Desktop/yeomanapp/node_modules/generator-angular/app/index.js
create .bowerrc
create .editorconfig
create .gitattributes
create .jshintrc
create app/.buildignore
create app/.htaccess
create app/404.html
create app/favicon.ico
create app/robots.txt
create app/styles/main.css
create app/views/main.html
create test/runner.html
create .gitignore
invoke angular:main:/Users/heavysixer/Desktop/yeomanapp/node_modules/generator-angular/app/index.js
create app/scripts/app.js
invoke angular:controller:/Users/heavysixer/Desktop/yeomanapp/node_modules/generator-angular/app/index.js
create app/scripts/controllers/main.js
create test/spec/controllers/main.js
这是一个很好的开始!Yeoman 创建了一个合理的应用结构,并连接了所有 AngularJS 依赖项。就像电视上的那个人说的,“但是等等;还有呢!”
虽然这个生成器创建了一个完整的 AngularJS 应用,但也有一些更小的生成器可以用来创建 AngularJS 框架的各个方面。以下是一些例子:
yo angular:controller myController
yo angular:directive myDirective
yo angular:filter myFilter
包装管理
如果您需要向项目中添加一个新的依赖项,使用 Yeoman 很容易,因为它集成了 Bower。让我们将 Angular-UI 项目添加到应用中。此代码添加了一组有用的 AngularJS 过滤器和指令:
bower install angular-ui
同样,如果一切正常,您应该会在终端窗口看到如下输出:
bower cloning git://github.com/angular-ui/angular-ui.git
bower caching git://github.com/angular-ui/angular-ui.git
bower fetching angular-ui
bower checking out angular-ui#v0.4.0
bower copying /Users/heavysixer/.bower/cache/angular-ui/bd4cf7fc7bfe2a2118a7705a22201834
bower installing angular-ui#0.4.0
在 Bower 为自己的目的缓存 angular-ui 源代码之后,它会在 Yeoman 创建的 app 目录中的 bower_components 文件夹中放置一个副本。Bower 将这种浅层复制作为依赖性管理过程的一部分。
内置服务器
Yeoman AngularJS 生成器的目的是快速搭建一个基本的 AngularJS 应用,开发人员可以开始修改它以满足自己的需求。像许多现代 JavaScript 框架一样,AngularJS 是数据驱动的。这通常意味着连接到一个服务器来拉回资源以显示给用户。不幸的是,浏览器中的安全限制阻止您将本地文件拖到浏览器中,然后发出远程 AJAX 请求。幸运的是,Yeoman 内置了一个很棒的服务器。
从项目的根文件夹中,您可以通过在控制台中键入以下命令来快速启动 AngularJS 应用:
grunt server
Note
如果您没有安装 Ruby 或 Compass gem,您可能会在尝试运行服务器时收到一条警告消息。您可以安装 Ruby 和 Compass gem,或者使用-- force环境标志:grunt server --force强制服务器在没有它们的情况下启动。
您应该看到 Grunt 在您的计算机上启动一个作为进程运行的服务器,然后打开您的默认浏览器,其中已经加载了您的应用。无需首先将网站部署到网络上就能快速启动服务器的能力可以节省大量时间,但这还不是最酷的部分。Yeoman 服务器实际上是一个 LiveReload 服务器。这意味着除了提供站点的本地文件,服务器还会监视它们的变化。当它发现你改变了一个文件,它会自动重新加载浏览器。
虽然这看起来是一个微不足道的增加,但是想象一下你浪费了多少时间,让你的 IDE 移动到你的浏览器并点击重新加载。从长远来看,杀死这种重复的任务真的会增加。既然我们新开发的应用已经在浏览器中启动并运行,是时候进入工作流程的下一个阶段了:开发。
发展
在开发阶段,程序员编写代码,测试产品断言,并追踪 bug。这些任务中有许多涉及大量重复的体力劳动。这样,开发人员就成了瓶颈,因为他们一次只能完成一项任务。正如您在引导阶段所看到的,任务自动化在提高开发过程中的生产速度和质量方面起着重要的作用。
它在两个方面提高了速度:它通常可以比开发人员更快地完成这些任务,并且许多任务可以并行运行,这改善了曾经是一系列连续步骤的情况。
引导部分主要关注编写代码的代码。在开发阶段,您探索通过捕捉简单错误或实施最佳实践来提高开发人员编写的代码质量的工具。
一箱咖啡脚本
CoffeeScript 是编译成 JavaScript 的精品语言。CoffeeScript 深受 Ruby 的影响,并从它那里借用了很多简洁的语法。与 Ruby 不同,CoffeeScript 中的代码缩进很重要。这是因为 CoffeeScript 在编译过程中使用缩进来帮助定义代码的词法范围。
CoffeeScript 有时会被严肃的 JavaScript 程序员认为是不必要的抽象,只是用另一种要学习的微语言来搅浑开发的水。在他们看来,JavaScript 作为一种语言已经很容易编写和阅读了。因此,让另一种语言为他们做这件事没有任何好处。让我在下一节阐述我支持 CoffeeScript 的理由。
Note
以下示例假设您已经安装了 CoffeeScript。关于安装 CoffeeScript 的更多信息可以在这里找到: http://coffeescript.org/
少写
如果说写代码是最花时间的过程,那么少写代码,得到同样的结果是好事,对吧?CoffeeScript 有一个非常简洁的语法,允许您编写如下内容:
square = (x) -> x * x
根据 CoffeeScript 编译器的设置,它会将前面一行编译成如下所示:
(function() {
var square;
square = function(x) {
return x * x;
};
}).call(this)
本节的目的是解释为什么应该使用 CoffeeScript,而不是如何编写它。现在,我要说的是,在 CoffeeScript 中,单箭头定义了一个函数,圆括号定义了该函数可以接受的参数。重要的一点是,CoffeeScript 可以将一条非常简洁的语句外推到任何 JavaScript 开发人员都应该能够阅读的 JavaScript 源文件中。
固执己见的翻译
正如您在前面的例子中看到的,CoffeeScript 编译器不仅仅是创建了我们函数的一对一翻译。CoffeeScript 试图遵循 JavaScript 中常见的最佳实践,并在可能的情况下,为您实施这些实践。在培训团队中的新开发人员时,我经常让他们先从 CoffeeScript 开始,然后再转到 JavaScript。通过这种方式,我可以谈论 CoffeeScript 修改代码背后的原因。CoffeeScript 对这个简单的方法做了几个重要的修改,值得一次讨论一个。
第一个修改是 CoffeeScript 编译器将函数包装到一个立即调用的函数表达式(IIFE)中。通过将函数封装到一个闭包中,CoffeeScript 保护代码不被其他脚本意外覆盖。
IIFE 还为与其他源文件的连接准备代码。文件的连接和缩小是自动构建系统中的常见任务。通过将所有文件合并成一个文件,浏览器必须发出更少的请求,这加快了站点的呈现速度。不幸的是,有时连接会破坏代码。出现这种情况有多种原因,但一个常见的错误是一个或多个文件在开头或结尾没有换行符。这可能导致代码一起运行,从而产生错误。
CoffeeScript 对我们代码的下一个增强更加微妙,因为它是关于如何编写代码的意见。在原始函数中,您使用了函数表达式而不是函数声明。CoffeeScript 使得编写函数声明变得几乎不可能,但是它这样做有一个很好的理由。
早期版本的 Internet Explorer(版本 8 和更低版本)有一个作用域问题,即命名函数可以同时被视为声明和表达式。
CoffeeScript 通过几乎完全使用函数表达式来回避整个问题。事实上,CoffeeScript 允许函数声明的唯一地方是在定义类的时候。
除了执行函数表达式,CoffeeScript 还将变量定义为局部变量,并将其声明移到了函数块的顶部。通过这样做,CoffeeScript 可以保护您避免任何不可预见的变量提升问题,这些问题可能是在定义函数之前调用函数而引起的。
最后但同样重要的是,CoffeeScript 从函数中返回值,即使您没有明确请求它。就像在 Ruby 中一样,CoffeeScript 假设函数体中的最后一个元素应该是返回值。因为 CoffeeScript 实施了这个约定,所以所有的函数签名都受益于基本级别的一致性。
快速失败
乍一看,这似乎有悖常理,但 CoffeeScript 无法编译成 JavaScript 实际上是一件好事。顾名思义,JavaScript 是一种脚本语言,浏览器不需要编译就可以执行。
CoffeeScript 只有在语法正确的情况下才会编译成 JavaScript。现在,这并不意味着代码会如你所愿,但至少在浏览器执行它时,它将是有效的 JavaScript。
统一团队代码
大多数专业开发人员作为团队的一部分以小组的形式编写代码。通常,团队有一个风格指南,他们遵循该指南来保持代码的可读性和统一性。这些约定可以涵盖从如何命名变量到代码缩进多少个空格的范围。使用 CoffeeScript 不会解决所有这些问题,但它至少会保证编译后的 JavaScript 具有某种程度的一致性。
我推广 CoffeeScript 的一个原因是它遵循了一系列旨在提高代码质量的最佳实践。CoffeeScript 通过控制源代码的最终形式(就像它在生命中包装我们的代码时一样)或通过限制您可以首先编写的代码类型(就像他们使编写函数声明变得困难时一样)来实施它的最佳实践集。
CoffeeScript 通常被认为是一种爱它或恨它的技术。不幸的是,许多人甚至在尝试之前就下定了决心。有时候,当我哄骗一个固执的开发者时,我觉得我在和我的孩子说话,“你不一定要喜欢它,但是你一定要试一试。”
不管你个人对 CoffeeScript 的感觉如何,这种语言的流行是不可否认的,支持它的工具生态系统也在不断发展。默认情况下,Yeoman 会自动观察 CoffeeScript 文件并为您编译它们,现在许多生成器最近也增加了对 CoffeeScript 的支持。例如,如果您想使用 CoffeeScript 而不是普通的 JavaScript 生成 AngularJS 项目,您可以提供可选的“- coffee”参数。完整的命令如下所示:
yo angular --coffee
棉绒收集器
CoffeeScript 对代码风格的自动执行为源代码提供了一层事实上的代码分析。就好像有人越过你的肩膀看着你说,“你不会真的想这么写的;我来帮你修吧。”这种做法疏远了一些人。对那些开发人员来说幸运的是,有其他工具可以提供静态代码分析,而无需为您编写代码。
JSHint 最初是由 Anton Kovalyov 编写的,是代码分析任务的另一个很好的选择。JSHint 最初是道格拉斯·克洛克福特 JSLint 项目的一个分支。这两个程序的工作方式基本相同:一行一行地遍历源文件,并列出潜在问题或偏离可接受风格的地方。
许多人觉得 JSLint 太固执己见了。尽管 JSLint 的目标是检测 JavaScript 程序中的潜在错误和疏忽,但它也迫使开发人员以任意形式编写 JavaScript,这并不一定是对他们现有方法的改进。JSLint 的源代码暗示了这种紧张:
"WARNING: JSLint will hurt your feelings."
Kovalyov 松开了 JSLint 的螺钉,并试图将关于风格的观点与静态代码分析的需要分开。通过这样做,JSHint 成为了原版的一个更友好、更温和的版本。JSHint 的网站在描述目标时提到了这一点:
Our goal is to help JavaScript developers write complex programs without worrying about spelling mistakes and language traps. I believe that static code analysis programs-and other code quality tools-are important and beneficial to the JavaScript community, so we should not alienate their users.
JSHint 的目标之一是提供一种配置 linter 的方法,以便它只强制执行团队或个人寻求推广的编码约定。JSHint 的选项分为三个主要类别:可实施的、可放松的和环境选项。
每个类别包含许多不同的选项;事实上,不胜枚举。相反,我将为每个类别提供几个典型的例子来说明这一点,但是我鼓励那些感兴趣的人详细阅读文档。
可执行期权
顾名思义,这些额外的选项可以由 JSHint 强制执行。这里有两个例子:
camelcase (true | false) //:此选项允许您对所有变量名强制使用 camelCase 样式。undef (true | false) //:防止您定义最初未定义的变量。这种情况经常发生,一个变量被声明了,但是从来没有被使用过
可放宽的选项
对一个人来说是最佳实践的规则对另一个人来说却很烦人。JSHint 知道这一点,并提供了一组选项来减少默认情况下由 linter 触发警告的情况。这里有两个例子:
- 几乎所有人都认为使用 eval 是个坏主意,因为它暴露了第三方注入恶意代码并让宿主应用执行代码的渠道。
debug (true | false) //:该选项允许您禁止在代码中使用调试器语句的警告。
环境选项
此类别中的选项定义了由其他库(如 jQuery 或 Nodejs)公开的任何全局变量。这里有一个例子:
jquery (true | false) //:是公开全局$还是 jQuery 变量。
幸运的是,作为自动化构建过程的一部分,Yeoman 被自动配置为 lint 任何 JavaScript,这一点我将在后面介绍。您可以通过编辑 JSHint 资源文件来修改默认的 JSHint 设置,该文件位于应用目录的根目录中。该文件被命名为. jshintrc。
在继续之前有一个警告:静态代码分析工具,比如 JSHint,只验证代码的语法结构。这对于捕捉小错误或风格不一致是一个巨大的素材,否则可能会从日常开发的裂缝中溜走。然而,这些工具无法告诉您所编写的代码是否真的达到了预期的目的。为此,开发人员需要在各种不同的环境下测试代码,以确保代码按预期执行。
测试
测试可能意味着任何事情,从断言应用执行了它被设计的任务,到它在各种平台(如桌面、电话或平板电脑)上看起来是否正确。编写有效的测试并知道首先要测试什么可以改进工作流程的这个阶段。同样,自动化是关键,不仅在为开发人员运行测试时如此,在跨多个平台分布测试用例时也是如此。
我还应该提到,测试并不总是遵循开发过程。包含测试驱动开发(TDD)或行为驱动开发(BDD)的方法遵循测试优先的范例。这些开发人员从编写描述需要编写的功能的测试开始。然后运行测试以确保它们失败。一旦编写了一个测试,编码才开始。
许多方法还指出,开发人员应该只编写足够通过测试的代码。希望代码库可以更精简,因为不需要的功能不会被添加。我把开发之后的测试放在这一章中,主要是因为我相信这是大多数人认为的流程一起流动的顺序。实际上,开发和测试可以是紧密耦合的阶段,它们一起振荡。
在这一节中,我将介绍几个与测试相关的工具,并演示如何将它们有效地集成到您的工作流程中。
如何测试
我敢打赌,除了 JavaScript 之外,大多数使用其他语言的专业开发人员都会将测试作为他们正常工作流程的一部分。JavaScript 在这方面仍然落后的原因与典型的 JavaScript 开发人员的素质关系不大,更多的是因为 JavaScript 可以在如此多的不同平台和上下文中运行。在大多数语言中,编写测试是最难的部分,但是在 JavaScript 中是运行测试。幸运的是,许多聪明的开发人员一直在努力工作,慢慢解决这个问题。以自动化的方式可靠地运行 JavaScript 测试有几个可行的选择。
JavaScript 测试人员通常分为两个阵营:使用独立引擎(如 V8 或 Rhino)进行测试的,以及在浏览器中运行的。我将演示两个测试程序:Karma 和 PhantomJS。
因果报应
Karma 是一个最初由 AngularJS 团队在编写 AngularJS 时并行开发的测试运行程序。它是测试框架不可知的,这意味着您可以使用任何您最熟悉的 JavaScript 测试库。它有一个内置的文件监视器,开发人员可以对其进行配置,以便在监视器看到源文件发生变化时自动运行相关的测试。
Karma 被设计成在实际的设备和浏览器上运行测试,这意味着测试得到了代码在目标设备/平台上将如何执行的真实表示。Karma 构建时考虑到了更大的工作流程,并为持续集成工具(如 Jenkins、Travis 和 Teamcity)提供了各种入口点。
Karma 唯一的依赖项是 Node.js,您应该已经安装了它。Karma 团队建议您通过 npm 在全球范围内安装项目,可以这样做:
$ npm install -g karma
# Start Karma
$ karma start
运行 start 命令应该会打开一个浏览器,在活动选项卡中运行 Karma。您应该会在控制台中看到类似这样的内容:
INFO [karma]: Karma server started at http://localhost:8080/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 26.0 (Mac)]: Connected on socket id TPVQXqXCvrM2XhRwABfC
到目前为止,业力并没有那么有帮助;它只是闲置在打开的浏览器中,因为没有测试要运行——或者有吗?如果查看 Yeoman 生成的目录结构,应该会看到一个 main.js 文件。它位于/test/spec/controllers/目录中。现在您有一个测试要运行,您只需要配置 Karma 来运行它,这需要一点点配置。
作为引导过程的一部分,Yeoman 已经为您生成了一个配置文件。如果您查看根目录,您应该看到一个名为 karma.conf.js 的文件。幸运的是,开发人员对该文件进行了很好的注释,并且选项非常容易理解。
默认情况下,Karma 被设置为以集成模式运行,但是如果您在 Karma 配置文件中手动将 singleRun 更改为 true,您可以指示 Karma 按需运行测试:
// Karma configuration
// base path, that will be used to resolve files and exclude
basePath = '';
// list of files / patterns to load in the browser
files = [
JASMINE,
JASMINE_ADAPTER,
'app/components/angular/angular.js',
'app/components/angular-mocks/angular-mocks.js',
'app/scripts/*.js',
'app/scripts/**/*.js',
'test/mock/**/*.js',
'test/spec/**/*.js'
];
// list of files to exclude
exclude = [];
// test results reporter to use
// possible values: dots || progress || growl
reporters = ['progress'];
// web server port
port = 8080;
// cli runner port
runnerPort = 9100;
// enable / disable colors in the output (reporters and logs)
colors = true;
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel = LOG_INFO;
// enable / disable watching file and executing tests whenever any file changes
autoWatch = false;
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers = ['Chrome'];
// If browser does not capture in given timeout [ms], kill it
captureTimeout = 5000;
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun = false;
保存更改后,当您重新运行 Karma start 命令时,您应该会看到不同的结果:
karma start
浏览器应该出现一瞬间,然后又消失了。检查控制台,您应该看到相关的位应该在底部,看起来应该有点像这样:
INFO [karma]: Karma server started at``http://localhost:8080
INFO [launcher]: Starting browser Chrome
INFO [Chrome 26.0 (Mac)]: Connected on socket id UpRyiPnI-M9x4d35NiqQ
Chrome 26.0 (Mac): Executed 1 of 1 SUCCESS (0.084 secs / 0.013 secs)
如你所见,你启动了 Karma,它又启动了 Chrome,Chrome 最终为你运行了 Jasmine 测试。你明白我说的依赖管理是什么意思吗?在控制台输出的最后,您可以看到您的单个测试在几分之一秒内运行。
拥有最多的鬼魂
PhantomJS 是您将调查的下一个测试跑步者。不像 Karma,它是一个顽固的测试者,PhantomJS 试图在一个不可见的接口中复制整个 web 栈(DOM 遍历、CSS 选择、JSON 解析、Canvas 和 SVG 处理)。这有时被称为无头浏览器。PhantomJS 通过在上面放置一个强大的 JavaScript API 来增强普通浏览器的特性。
开发人员可以使用 API 来完成各种有用的任务,例如以编程方式捕获屏幕截图、监控网站性能或模拟用户与网站的交互。PhantomJS API 还允许开发人员使用熟悉的库(如 jQuery)来编写 API 脚本,这使得启动和运行速度更快。
在被单下,PhantomJS 只是 Webkit。这意味着,当编写测试时,程序员必须意识到,结果可能不会真实地反映代码在其他浏览器(例如,Internet Explorer)上的行为。与 Karma 不同,它只是一个测试运行者,PhantomJS 认为测试运行只是它擅长的许多用例之一。
测试运行基础设施不像 Karma 中那样容易访问。幸运的是,PhantomJS 拥有活跃的用户群,并且已经编写了几个附加项目来让 phantom 轻松运行测试。PhantomJS 生态系统中有几个测试项目值得一提,包括 casperJS、Poltergeist 和 GhostDriver。
不幸的是,让它们运行起来超出了本章的范围。相反,让我们专注于将幻像整合到因果报应中。当 Karma 之前运行测试时,浏览器弹出一瞬间运行测试,然后自动关闭。
通过切换到 PhantomJS,您可以完全避免这种情况,因为测试将在一个不可见的无头浏览器中运行。幸运的是,这种集成很容易实现。您只需要重新打开 karma.conf.js 文件,并将 browsers 数组中的单个条目改为 PhantomJS。
一旦保存并关闭文件,您应该再次触发 Karma start 命令。这一次,不会出现浏览器窗口,您应该会在控制台输出中看到略有不同的结果:
INFO [karma]: Karma server started at``http://localhost:8080
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.7 (Mac)]: Connected on socket id 2WUOvjjU9KSbb442Kkt9
PhantomJS 1.7 (Mac): Executed 1 of 1 SUCCESS (0.034 secs / 0.007 secs)
Karma 这次使用了幻想曲,作为奖励,用了将近一半的时间来运行测试!既然您已经了解了如何作为 JavaScript 开发工作流的一部分可靠地运行测试,那么让我们花一点时间来探索能够并且应该编写的测试类型。
测试什么
要正确测试应用,您必须从各种不同的方面着手。您希望单独测试代码,然后在集成到最终部署的环境中时再测试一次。同时,您还可以关注另一个测试流,看看代码执行得有多好。
通常,测试属于四个测试类别之一:单元、集成、性能和兼容性。我将在本节的剩余部分介绍每一类工具。
单元测试
单元测试测试单个代码单元,例如,一个较大类的特定函数。单元测试使您能够孤立地进行测试,以确保您的函数在最基本的级别上完成了预期的任务。有几个优秀的 JavaScript 测试框架:Mocha、QUnit 和 Jasmine,仅举三个例子。下面是在每个框架中编写的相同测试:
/* Written in Mocha */
var assert = require("assert")
describe('truth test', function(){
it('should know that true is equal to true', function(){
assert.equal(true, true);
})
})
/* Written in QUnit */
test( "truth test", function() {
ok( true === true, "is true!" );
});
/* Written In Jasmine */
describe("truth test", function() {
it("should know true is equal to true", function() {
expect(true).toBe(true);
});
});
集成测试
集成测试有时被称为端到端测试,因为它们一起测试一组较小的功能,以确保较大的任务按计划进行。集成测试主要用于执行一个场景,该场景代表了软件可能如何使用的潜在用例。这些测试通常需要访问额外的资源,比如外部 API 或浏览器 cookies。碰到这些外部元素会导致测试变慢,所以它们经常被模拟出来,用一个代表预期结果的虚拟对象来代替。
接下来是 AngularJS 应用主控制器的源代码。这段代码后面是一个 Jasmine 测试,也是 Yeoman 自动创建的。巧合的是,这个测试是您在检查各种测试跑步者时重复运行的相同测试。
'use strict';
/* app/scripts/controllers/main.js */
angular.module('DesktopApp')
.controller('MainCtrl', function ($scope) {
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
});
/* /test/spec/controllers/main.js */
'use strict';
describe('Controller: MainCtrl', function () {
// load the controller's module
beforeEach(module('DesktopApp'));
var MainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope: scope
});
}));
it('should attach a list of awesomeThings to the scope', function () {
expect(scope.awesomeThings.length).toBe(3);
});
});
请注意,这个测试中的大部分代码实际上是在模拟应用实际运行时的状态。这就是我所说的模拟大环境的方方面面。
一旦创建了主控制器的实例,测试就验证了包含三个元素的数组被绑定到$scope 变量的预期。测试框架将此计入通过的测试,并最终向测试运行人员报告这些结果。
性能测试
性能测试确保代码尽可能高效地工作。如前所述,PhantomJS 可用于自动化网站的网络监控。典型的用例是使用 onResourceRequested 和 onResourceReceived 属性来测量请求和响应周期的持续时间。然而,这对于程序员来说没有 devOps 中的人有用。
当我想到开发人员级别的性能测试时,它通常包括像在单元测试中一样隔离单个功能,并测量各种不同浏览器的性能。这种测试不需要每次迭代都运行一次,因为一旦你建立了结果,它就不会改变(除非你改变你的函数)。出于这个原因,我通常只使用 jsPerf 网站,它获取一个代码片段,在各种不同的浏览器中运行它,并向您返回一个报告。
兼容性测试
JavaScript 应用被部署到平台和主机应用的多样化生态系统中。兼容性测试是对开发一次并在任何地方部署的不合理愿望的考验。通过兼容性测试,开发人员可以看到相同的代码如何在各种不同的设备、浏览器等上执行。这些测试主要关注各种平台之间的差异,这通常意味着应用如何可视化地呈现,以及平台提供或限制哪些启示。因此,这些测试通常依赖于可视化的报告,而不是简单的通过-失败统计数据,这些统计数据会显示在控制台窗口中。
在一个屋檐下收集(更不用说购买和维护)不断增长的设备和浏览器列表,并对它们进行单独测试,将是最不具生产力的事情。幸运的是,有几种技术已经出现来满足这种需求。然而不幸的是,你可能需要带上你的信用卡。下面是一些提供兼容性测试的产品的简要介绍。
浏览器堆栈
根据其网站,该公司提供“所有桌面和移动浏览器的即时访问”它的付费服务让开发者可以访问各种虚拟机,从那里他们可以测试他们正在开发的产品。Browserstack 还提供屏幕截图服务,开发者可以提供一个 URL,Browserstack 反过来在许多不同的浏览器上创建结果页面的屏幕截图。
邦伊普
该工具可用于自动化多浏览器设备测试。Bunyip 可用于在您自己的设备农场上收集浏览器,但它也提供了与 Browserstack 等其他工具的集成。
Adobe Inspect
Inspect 是一项免费增值服务,允许您同步各种设备。使用 Inspect,作为开发人员,您可以更改代码,保存结果,然后观察所有连接的设备和浏览器的更新。就像 Browserstack 一样,Adobe Inspect 提供截图服务,还提供远程检查工具,可用于在远程设备上动态更改 HTML、CSS 和 JavaScript。
您可能想知道为什么我没有提到 PhantomJS,特别是因为它是免费和开源的。诚然,PhantomJS 确实提供了截图功能,而且因为它可以通过编程来捕捉它们,它们甚至可以被串成一个视频。然而,PhantomJS 只是 Webkit,因此不是真正的兼容性测试工具。
建筑物
一旦开发人员完成了一个特性并准备好与世界共享,他们就将代码部署到生产中。运输代码的艺术可以是一整本书的主题,并且已经超出了本书和一般 JavaScript 的范围。相反,本节将关注创建本地构建,这意味着将源代码准备成适合上传到 Web 或包含到更大的部署流中的形式。
如您所见,大部分 JavaScript 工作流都是以一种让程序员尽可能容易开发的形式编写代码。这可能意味着使用本地包管理器来编组依赖项或高级语言,如 CoffeeScript 作为 JavaScript 的代理。通常,其他工具如 HAML 被用来代替 HTML,SASS 被用来代替 CSS。这些工具的存在是为了让开发更有趣、更高效、更少出错。
然而,这些技术有一个巨大的缺点:没有浏览器能理解它们。因此,构建阶段的大部分时间致力于将人类易于阅读的代码转换成机器能够理解的源代码。在典型的构建过程中有几个共同的步骤:编译、分析、连接、优化、测试和通知。像往常一样,我将在接下来的部分中详细解释每个步骤。
汇编
JavaScript 只有在以 CoffeeScript 等其他形式启动时才会被编译。构建器进程通常遍历 CoffeeScript 文件,并将它们发送给编译器。然后,结果通常会保存到临时的构建目录中。
分析
如前所述,静态代码分析在确保交付的代码满足或超过预定的质量阈值,并且符合大型团队定义的风格惯例方面起着重要的作用。这种分析通常由 JSHint 之类的工具来执行,我在前面已经介绍过了。这一阶段的失败可以暂停构建,或者只是在通知阶段通过向控制台或日志文件写入报告来警告开发人员。
除了代码的静态分析,这个过程还可以使用伊斯坦布尔等工具,这是一个 JavaScript 的测试覆盖工具。伊斯坦布尔可以报告任何在测试期间没有被调用的代码区域。
串联
感觉到的应用的缓慢很大程度上是由于下载应用所依赖的所有相关源文件所需的请求数量。通过将整个源代码连接成一个文件,网站的性能将会提高。
通常,框架代码和库会跳过这一步,因为它们中的许多已经托管在其他地方的内容分发网络(cdn)上。Web 浏览器允许跨多个域的并行下载,这意味着利用 CDN 至少有两个好处。它可以通过并行浏览器请求加速初始下载,并减少剩余连接代码的文件大小。
最佳化
一旦原始 JavaScript 被编译成单个文件,构建器进程就会尽可能地减小文件大小。通常,这意味着使用诸如 UglifyJS 或 Google 的 closure 编译器之类的程序。这些压缩机中的一些比其他的更具侵略性。例如,闭包编译器试图在转换过程中使源代码“更好”。这可能意味着重写代码的某些方面,或者删除它认为没有使用的代码。
测试
所有这些对源代码的压缩、优化和美化可能会无意中破坏某些东西。因此,在发布代码之前,最好最后一次测试代码。大多数构建过程被设计为在测试失败时停止,从而降低了用错误版本覆盖生产中的代码的风险。
通知
有几个受众对构建过程的结果感兴趣。第一类是开发人员,第二类是等待将编译好的代码循环到更大的部署周期中的任何外部流程。对于感兴趣的人来说,通知可能意味着创建一个描述构建结果的报告,这可能简单到是失败还是通过。
该报告还可以概述关于代码质量和测试覆盖率的发现。一旦代码变得干净,它就可以被提交回源代码库,此时任何提交后挂钩都可以被触发。任何持续集成工具,如 Travis 或 Cruise Control 监听这些触发器,现在都知道一个新的构建已经准备好了。
继续学习 Yeoman,您现在将了解它是如何处理构建过程的。约曼实际上将这项任务委托给了其他人——同样,选择的工具是 Grunt。在引导过程中,Yeoman 为 Grunt 创建了一个配置文件,名为 Gruntfile.js,这并不奇怪。您已经尝试了其中的两个:grunt 服务器和 grunt 测试。然而,默认任务是构建过程。您可以通过在控制台中键入以下命令来开始构建过程:
grunt
当各个任务被单独调用时,您的控制台开始滚动,在过程结束时,您应该看到消息“完成,没有错误”在控制台里。现在,在应用目录中应该有一个名为 Dist 的新文件夹。该文件夹包含运行 AngularJS 应用所需的所有新编译的 JavaScript 文件。
恭喜你!您几乎已经到达了开发工作流的末尾。剩下的最后一点是如何在代码离开巢穴时支持它。
支持
一个开发人员生活中的悲哀事实是,在某个时候,软件将被发布到野外,在源代码内部的某个地方有一个非故意的故障。本章研究了将这些错误的检查和保护集成到开发工作流中的各种方法。
然而,有时这些技术是不够的,因此支持部署的代码必须是工作流的一部分。在这个阶段,开发人员使用工具和技术来尽可能快地跟踪和消除任何错误。
支持分两个阶段:当异常发生时得到通知和按需重新创建 bug,这样就可以隔离出有问题的来源。首先,我将讨论一个用于触发异常通知的工具,然后我将简要介绍如何将生产中的 bug 映射到开发源代码。
JavaScript 中的错误报告
许多现代应用框架都内置了异常通知。通常,当发生错误时,异常被代码块捕获,以便堆栈跟踪和环境变量可以被打包成报告,该报告通常被邮寄给开发人员。从这个报告中,开发人员有更好的机会来拼凑出哪里出错了。有完整的产品,如 errorCeption,专门为您解析、绘制和报告这些内容。错误报告器的基础很容易组合在一起。本质上,您只想将侦听器绑定到窗口对象的 onerror 事件。
以下是一个过于简化的示例,只是为了让您了解大致情况:
window.onerror = function(msg, url, lineNum) {
$.ajax({
url: " http://someserver.com/exception-notifier ",
type: "get",
data: {
message: msg,
url: url
lineNumber: lineNum
},
success: function(data) {
alert(“Error Reported”);
}
});
}
解开毛衣
不幸的是,这种方法并不完全安全。还记得构建过程修改 JavaScript 源代码的时候吗?所有这些压缩、混淆和连接会使调试产品代码变得像穿毛衣上的松线一样困难。过不了多久,你就只剩下一堆纱线,其他什么都没有了。这是因为压缩程序通常会缩短变量名,并从源代码中删除换行符。因此,通告程序返回的变量、方法名和行号将与未压缩的 JavaScript 不匹配。可以想象,这使得开发人员更难将错误的原因追溯到原始代码。幸运的是,近年来开发者,更重要的是浏览器,已经开始接受一个叫做源地图的概念。
源映射是编译后的文件和未压缩的 JavaScript 源之间的映射。这个映射是在编译时通过向编译器提供特殊指令而生成的。一旦编译器创建了映射,它就可以被支持浏览器的开发工具自动解析。
现在,对生成源地图的支持仍然不稳定,但是主要的编译器,包括 Google 的 Closure 编译器,都可以生成它们。另外很重要的一点是,源代码地图并不是 JavaScript 的专利。它们旨在成为任何可以缩小的文件类型的标准;所以 CSS 也支持源码图。
摘要
本章详细剖析了构建 JavaScript 应用的现代开发工作流程。有几个关键点希望你带走。
你应该尽量减少铲雪,这意味着做一些目前可能是必不可少的工作,但对项目的长期进展没有任何好处。
明智地选择您的技术组合;你经常不仅为你自己,也为你之后的每一个人做决定。选择适合工作的工具;不仅仅是你最擅长的那个。
拥抱自动化;如果你发现自己一天要手动完成一个过程好几次,那就想办法将其机械化。寻找在代码质量和编程风格方面实施社区标准的工具。这些工具不仅能帮助你发现小错误,还能为所有团队成员之间的一致性提供基线。
编写测试并持续运行它们。它们不仅证明了你的软件是可行的,而且给了你和你的团队信心去做未来的改变,而不用担心它会悄悄地破坏现有的特性。为人类而写,让构建过程去操心如何让它更小更高效。
当代码上线时,开发人员的工作流程不会停止;总会有不被考虑的边缘情况或平台。因此,当这些错误发生时,建立支持流程是很重要的。