本文在我们的《 JavaScript:最佳实践》 一书中有介绍 。 紧随现代JavaScript的快速变化的最佳实践。
毫无疑问,JavaScript生态系统变化很快。 随着ES2015(aka ES6)的引入,不仅新工具和框架的引入和开发迅速,语言本身也发生了巨大变化。 可以理解,许多文章抱怨如今学习现代JavaScript开发有多么困难。
在本文中,我将向您介绍现代JavaScript。 我们将研究该语言的最新发展,并概述当前用于编写前端Web应用程序的工具和技术。 如果您只是刚开始学习该语言,或者几年来您都没有碰过它,并且想知道曾经知道的JavaScript发生了什么,那么这篇文章适合您。
Node.js是运行时,允许使用JavaScript编写服务器端程序。 可能有全栈JavaScript应用程序,其中应用程序的前端和后端都是用相同的语言编写的。 尽管本文侧重于客户端开发,但是Node.js仍然扮演着重要角色。
Node.js的到来对JavaScript生态系统产生了重大影响,引入了npm软件包管理器并普及了CommonJS模块格式。 开发人员开始构建更多创新工具,并开发新方法来模糊浏览器,服务器和本机应用程序之间的界限。
2015年,以ES2015 (仍经常称为ES6)的名称发布了第六版ECMAScript (定义JavaScript语言的规范)。 这个新版本包括对该语言的大量添加,使构建雄心勃勃的Web应用程序变得更加容易和可行。 但是,改进不会随着ES2015的到来而止。 每年都会发布一个新版本。
JavaScript现在有另外两种声明变量的方式: let和const 。
let
是var
的后继者。 尽管var
仍然可用,但let
将变量的范围限制为在其内声明的块(而不是函数),这减少了出错的空间:
// ES5
for (var i = 1; i < 5; i++) {
console.log(i);
}
// <-- logs the numbers 1 to 4
console.log(i);
// <-- 5 (variable i still exists outside the loop)
// ES2015
for (let j = 1; j < 5; j++) {
console.log(j);
}
console.log(j);
// <-- 'Uncaught ReferenceError: j is not defined'
使用const
可以定义不能反弹到新值的变量。 对于诸如字符串和数字之类的原始值,这将导致类似于常量的内容,因为一旦声明了该值便无法更改:
const name = 'Bill';
name = 'Steve';
// <-- 'Uncaught TypeError: Assignment to constant variable.'
// Gotcha
const person = { name: 'Bill' };
person.name = 'Steve';
// person.name is now Steve.
// As we're not changing the object that person is bound to, JavaScript doesn't complain.
箭头函数提供了一种更清晰的语法,用于声明匿名函数(lambda),当主体函数仅具有一个表达式时,将删除function
关键字和return
关键字。 这可以让您以更好的方式编写功能样式代码:
// ES5
var add = function(a, b) {
return a + b;
}
// ES2015
const add = (a, b) => a + b;
箭头函数的另一个重要特征是它们从定义它们的上下文中继承了this
的值:
function Person(){
this.age = 0;
// ES5
setInterval(function() {
this.age++; // |this| refers to the global object
}, 1000);
// ES2015
setInterval(() => {
this.age++; // |this| properly refers to the person object
}, 1000);
}
var p = new Person();
如果您是面向对象编程的狂热者,则可能希望在基于原型的现有机制之上,在语言中添加类 。 尽管它主要只是语法糖,但它为尝试用原型模拟经典面向对象的开发人员提供了一种更简洁的语法。
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
JavaScript的异步特性长期以来一直是一个挑战。 在处理诸如Ajax请求之类的事情时,任何不平凡的应用程序都有陷入回调地狱的风险。
幸运的是,ES2015增加了对promises的本地支持。 承诺表示在计算时尚不存在的值,但以后可能会用到,从而使异步函数调用的管理更易于管理,而无需深入嵌套的回调。
ES2017引入了异步功能 (有时称为异步/等待),在此方面进行了改进,使您可以将异步代码视为同步代码:
async function doAsyncOp () {
var val = await asynchronousOperation();
console.log(val);
return val;
};
ES2015中添加的另一个突出功能是本机模块格式,使模块的定义和用法成为语言的一部分。 加载模块以前仅以第三方库的形式提供。 在下一节中,我们将更深入地介绍模块。
这里没有其他功能要讨论,但是我们已经介绍了一些您在查看现代JavaScript时可能会注意到的主要差异。 您可以在Babel网站的Learn ES2015页面上查看带有示例的完整列表,您可能会发现这些示例对于了解最新的语言很有用。 其中一些功能包括模板字符串,块作用域变量和常量,迭代器,生成器,新数据结构(例如Map和Set)等。
要了解有关ES2015的更多信息,请查看我们的高级课程: 深入ES2015 。
Linters是用于解析您的代码并将其与一组规则进行比较,检查语法错误,格式和良好做法的工具。 尽管每个人都建议使用短绒棉,但如果您入门,它特别有用。 为代码编辑器/ IDE正确配置后,您将获得即时反馈,以确保您在学习新的语言功能时不会因语法错误而卡住。
您可以查看ESLint ,它是最受欢迎的支持ES2015 +版本之一。
现代的Web应用程序可以包含成千上万的代码行。 如果没有一种机制将所有内容组织成较小的组件,编写专用且隔离的代码段,并根据需要以受控方式重用这些代码段,那么以这种大小工作几乎是不可能的。 这是模块的工作。
这些年来,出现了几种模块格式,其中最流行的是CommonJS 。 这是Node.js中的默认模块格式,并且可以在模块捆绑器的帮助下在客户端代码中使用,我们将在稍后讨论。
它利用module
对象从JavaScript文件导出功能,并使用require()
函数将功能导入所需的位置。
// lib/math.js
function sum(x, y) {
return x + y;
}
const pi = 3.141593
module.exports = {
sum: sum,
pi: pi
};
// app.js
const math = require("lib/math");
console.log("2π = " + math.sum(math.pi, math.pi));
ES2015引入了一种在语言中定义和使用组件的方法,以前只有第三方库才有可能。 您可以使用具有所需功能的单独文件,并仅导出某些部分以使它们可用于您的应用程序。
注意:仍在开发对ES2015模块的本机浏览器支持,因此您当前需要一些其他工具才能使用它们。
这是一个例子:
// lib/math.js
export function sum(x, y) {
return x + y;
}
export const pi = 3.141593;
在这里,我们有一个导出函数和变量的模块。 我们可以将该文件包含在另一个文件中,并使用导出的功能:
// app.js
import * as math from "lib/math";
console.log("2π = " + math.sum(math.pi, math.pi));
或者,我们也可以只具体输入我们需要的内容:
// otherApp.js
import {sum, pi} from "lib/math";
console.log("2π = " + sum(pi, pi));
这些示例摘自Babel网站 。 要深入了解,请查看了解ES6模块 。
长期以来,其他语言都有自己的软件包存储库和管理器,以使查找和安装第三方库和组件变得更加容易。 Node.js带有自己的包管理器和存储库npm 。 尽管还有其他软件包管理器可用,但npm已成为事实上的JavaScript软件包管理器,并且据说是世界上最大的软件包注册中心。
在npm存储库中,您可以找到第三方模块,您可以使用单个npm install
命令轻松下载并在项目中使用这些模块。 这些软件包将下载到本地node_modules
目录,其中包含所有软件包及其依赖项。
您下载的包可以与项目或模块的信息(可以本身作为软件包发布在npm上)一起注册为package.json文件中的项目依赖项。
您可以为开发和生产定义单独的依赖项。 虽然需要生产依赖关系才能使程序包正常工作,但开发依赖关系仅对于程序包的开发人员是必需的。
示例package.json文件
{
"name": "demo",
"version": "1.0.0",
"description": "Demo package.json",
"main": "main.js",
"dependencies": {
"mkdirp": "^0.5.1",
"underscore": "^1.8.3"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Sitepoint",
"license": "ISC"
}
我们在开发现代JavaScript Web应用程序时编写的代码几乎永远不会与将要投入生产的代码相同。 我们使用浏览器可能不支持的现代JavaScript版本编写代码,大量使用了node_modules
文件夹中的第三方程序包以及它们自己的依赖项,我们可以使用诸如静态分析工具或简化程序之类的程序,存在构建工具来帮助将所有这些转换为可以有效部署的东西,并且大多数Web浏览器都可以理解。
使用ES2015 / CommonJS模块编写干净,可重用的代码时,我们需要某种方式来加载这些模块(至少直到浏览器支持本机加载ES2015模块)。 在HTML中包含一堆脚本标签并不是一个切实可行的选择,因为对于任何严肃的应用程序来说,它很快就会变得笨拙,而所有这些单独的HTTP请求都将损害性能。
我们可以使用ES2015中的import
语句(或对CommonJS使用require
)将所有需要的模块包括在内,并使用模块捆绑器将所有内容组合在一起,形成一个或多个文件(捆绑)。 我们将要将此捆绑文件上载到服务器并包含在HTML中。 它将包括所有导入的模块及其必需的依赖性。
当前,有两个受欢迎的选项,其中最受欢迎的是Webpack , Browserify和Rollup.js 。 您可以根据需要选择一个或另一个。
如果您想了解有关模块捆绑的更多信息,以及如何将其应用到应用程序开发的大局中,我建议阅读理解JavaScript模块:捆绑和Transpiling 。
尽管在较新的浏览器中对现代JavaScript的支持非常好 ,但您的目标受众可能包括旧版浏览器和部分或完全不支持的设备。
为了使现代JavaScript能够正常工作,我们需要将编写的代码转换为早期版本(通常为ES5)中的等效代码。 此任务的标准工具是Babel-一种编译器,可将您的代码转换为大多数浏览器的兼容代码。 这样,您不必等待供应商实施所有操作。 您可以只使用所有现代JS功能。
除语法翻译外,还有其他一些功能还需要更多。 Babel包含一个Polyfill ,它可以模拟某些复杂功能(如promises)所需的一些机械。
模块捆绑和移植只是我们项目中可能需要的两个构建过程。 其他包括代码缩减(以减小文件大小),分析工具,以及可能与JavaScript无关的任务,例如图像优化或CSS / HTML预处理。
任务管理可能会变得很费力,我们需要一种自动处理任务的方法,能够使用更简单的命令执行所有操作。 最受欢迎的两个工具是Grunt.js和Gulp.js ,它们提供了一种将任务按有序方式组织到组中的方法。
例如,您可以使用诸如gulp build
类的命令,该命令可以运行代码linter,使用Babel进行编译过程以及使用Browserify进行模块捆绑。 不必按顺序记住三个命令及其关联的参数,我们只需执行一个将自动处理整个过程的命令即可。
无论您在哪里手动组织项目的处理步骤,请考虑是否可以使用任务运行器将其自动化。
进一步阅读: Gulp.js简介 。
Web应用程序与网站有不同的要求。 例如,虽然博客可以接受页面重新加载,但对于诸如Google Docs之类的应用程序绝对不是这样。 您的应用程序的行为应与桌面应用程序尽可能接近。 否则,可用性将受到损害。
老式的Web应用程序通常是通过从Web服务器发送多个页面来完成的,并且当需要大量动态处理时,将根据用户操作替换HTML块来通过Ajax加载内容。 尽管这是向更具动态性的Web迈出的一大步,但它确实具有其复杂性。 从用户的角度来看,在每个用户操作上发送HTML片段甚至整个页面都会浪费资源,尤其是时间。 可用性仍然与桌面应用程序的响应能力不匹配。
为了改善性能,我们创建了两种新的方法来构建Web应用程序-从向用户展示Web应用程序的方式到客户端与服务器之间进行通信的方式。 尽管应用程序所需的JavaScript数量也急剧增加,但现在的结果是应用程序的行为与本地应用程序非常接近,而无需每次单击按钮时都重新加载页面或等待大量时间。
Web应用程序最常见的高级体系结构称为SPA ,代表单页应用程序 。 SPA是JavaScript的一大块,其中包含应用程序正常运行所需的一切。 UI完全呈现在客户端,因此不需要重新加载。 唯一更改的是应用程序内部的数据,通常通过Ajax或其他异步通信方法使用远程API处理数据。
这种方法的一个缺点是,应用程序第一次加载会花费更长的时间。 但是,一旦加载,视图(页面)之间的转换通常会快很多,因为它只是客户端和服务器之间发送的纯数据。
尽管SPA可以提供出色的用户体验,但是根据您的需求,它们可能不是最佳的解决方案-特别是在您需要更快的初始响应时间或搜索引擎优化索引的情况下。
有一种相当新的方法可以解决这些问题,称为同构 (或通用)JavaScript应用程序。 在这种类型的体系结构中,大多数代码都可以在服务器和客户端上执行。 您可以选择要在服务器上呈现的内容,以便更快地进行初始页面加载,然后,在用户与应用程序交互时,客户端将接管呈现。 由于页面最初是在服务器上呈现的,因此搜索引擎可以正确地对其进行索引。
在现代JavaScript应用程序中,您编写的代码与为生产而部署的代码不同:您仅部署构建过程的结果。 完成此任务的工作流程可能会有所不同,具体取决于项目的大小,从事该项目的开发人员的数量,有时还取决于您使用的工具/库。
例如,如果您一个人在一个简单的项目上工作,那么每次准备进行部署时,都可以运行构建过程并将生成的文件上传到Web服务器。 请记住,您只需要从构建过程(编译,模块捆绑,压缩等)上载结果文件,这些文件可以只是一个包含整个应用程序和依赖项的.js
文件。
您可以具有以下目录结构:
├── dist
│ ├── app.js
│ └── index.html
├── node_modules
├── src
│ ├── lib
│ │ ├── login.js
│ │ └── user.js
│ ├── app.js
│ └── index.html
├── gulpfile.js
├── package.json
└── README
因此,您将所有应用程序文件保存在用ES2015 +编写的src
目录中,并从lib
目录导入随npm安装的软件包以及您自己的模块。
然后,您可以运行Gulp,它将执行gulpfile.js
的指令来构建您的项目-将所有模块捆绑到一个文件中(包括与npm一起安装的模块),将ES2015 +转换为ES5,将生成的文件最小化,等等。可以配置它以将结果输出到方便的dist
目录中。
注意:如果您有不需要任何处理的文件,则可以将它们从src
复制到dist
目录。 您可以在构建系统中为此配置任务。
现在,您可以将文件从dist
目录上载到Web服务器,而不必担心其余的文件,这些文件仅对开发有用。
如果您正在与其他开发人员一起工作,则可能您也在使用共享代码存储库(例如GitHub)来存储项目。 在这种情况下,您可以在进行提交之前立即运行构建过程,并将结果与其他文件一起存储在Git存储库中,以后再下载到生产服务器上。
但是,如果多个开发人员一起工作,则将生成的文件存储在存储库中容易出错,并且您可能希望保持所有内容远离生成工件。 幸运的是,有一个更好的方法来解决该问题:您可以在流程的中间放置Jenkins , Travis CI , CircleCI等服务,以便在每次提交提交到存储库后自动构建项目。 开发人员仅需担心无需每次都先构建项目就可以推动代码更改。 该存储库还保持自动生成文件的清洁,最后,您仍然可以使用已构建的文件进行部署。
如果您最近几年不从事Web开发,那么从简单网页到现代JavaScript应用程序的转换似乎令人生畏,但是我希望本文可以作为起点。 我已尽可能地链接到有关每个主题的更深入的文章,以便您进一步探索。
请记住,如果在某些时候看完所有可用的选项后,一切似乎变得不堪重负,请记住KISS原则 ,只使用您认为需要的东西,而不使用所有可用的东西。 归根结底,解决问题才是最重要的,而不是使用最新的一切。
您学习现代JavaScript开发有什么经验? 我有没有想在这里涉及到的任何内容,将来您会希望看到? 我希望收到您的来信!
From: https://www.sitepoint.com/anatomy-of-a-modern-javascript-application/