毫无疑问,JavaScript发展很快,不仅仅体现在各种工具、框架的的不断出现,JavaScript语言本身也发生了巨大的变化。很多开发者不断的抱怨,前端开发变得越来越复杂。

关于Node.js

Node.js是一个JavaScript编写服务端程序的运行时环境。Web全栈应用是可以实现客户端和服务端采用同一种语言开发的。即使只讨论客户端开发,Node.js也扮演着非常重要的角色。

Node.js的出现是对现在JavaScript生态的一次重大冲击,带来了npm模块管理以及推广了CommonJS 规范。一些新的工具和全新的开发模式模糊了浏览器,服务器和本地应用程序之间的界线。

ES2015+

2015年 ECMAScript 第六版发版了,业界称为ES2015(也叫ES6)。这个新版本包含大量增加语言特性,使其更容易的构建复杂的web应用程序。ES2015并不是最终版,每年都会新的版本出现 ES7、ES8、ES9。

变量声明

提供了两种声明变量的方式:let 和const

let作为 var 的补充,尽快var还是可以使用,但是let提供了一种块级作用域的概念在函数内部, 降低了出错的空间。

// ES5
for (var i = 1; i < 5; i++) {
  console.log(i);
}
// <-- 打印出1-4
console.log(i);
// <-- 5 (i在for循环外依旧存在)

// ES2015
for (let j = 1; j < 5; j++) {
  console.log(j);
}
console.log(j);
// <-- 'Uncaught ReferenceError: j is not defined'

使用const允许您定义的变量不能再重新赋值。对于原生类型比如string和number, 相当于常量:

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.

箭头函数

箭头函数提供了一种声明匿名函数的简洁语法:

// 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语法

如果你是一个面向对象编程的粉丝,你可能喜欢的基于原型的面向对象机制。虽然大多只是语法糖,它为开发人员提供了一个更清洁的语法模仿经典的面向对象原型。

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

Promises / Async

在JavaScript中处理异步操作是一种挑战;很容易落入到回调地狱中,尤其在处理Ajax回调的时候。

幸运的是,ES2015添加原生的Promise。Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象

ES2017引入异步函数(有时称为async/await),做出了改进,允许您将异步代码写成同步代码风格:

async function doAsyncOp () {
  var val = await asynchronousOperation();
  console.log(val);
  return val;
};

Modules

ES2015另一个突出的特性就是原生支持模块化开发,而在此之前,都是通过第三方库的方式实现模块加载。最著名就是require.js和sea.js。

ES2015还剩一些特性,我们就不花时间介绍,后期我会整理出一个教程。

其他一些重要特性:template string、块及作用域和 constants, iterators, generators,以及Map 和 Set。

Code linting

Linter是分析代码并将其与一组规则进行比较的工具,用于检查语法错误、格式和良好实践。虽然建议每个人都使用linter,但如果你是初学者,它尤其有用。当您为代码编辑器/IDE正确配置时,您可以获得即时反馈,以确保在学习新语言特性时不会遇到语法错误。

ESLint是最流行的代码检查工具,全面支持ES2015+规范。

模块化开发

现代web应用程序可以有数千(甚至几百上千)行代码。如果没有在较小的组件中组织代码的机制,以这种规模工作几乎是不可能的,因为这种机制需要编写专门的和隔离的代码片段,以便在需要时以可控的方式重用这些代码。这就是模块化的作用。

这些年涌现除了很多的模块化方案。其中最著名的就是CommonJS, Node.js默认采用的就是它。如果想在客户端采用这种方案,必须使用Module bundle。

利用module对象导出对外的功能, 通过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 modules

ES2015引入了一种方式来定义和使用组件,这在以前只能通过第三方库。在一个文件中定义好功能,export导出部分功能供其它组件使用

ES2015模块仍处于开发阶段,所以你现在需要一些额外的工具才能在生产环境使用它们
// lib/math.js

export function sum(x, y) {
  return x + y;
}
export const pi = 3.141593;

我们导出了一个sum函数和pi变量,在另一个文件中引入它们:

// 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 website, 为深入了解:理解ES6 Modules

包管理工具

其他语言早就有了自己的包管理工具,能够方便的使用第三方库或组件。Node.js自带一个包管理工具:npm, 它已经成为npm官方包管理工具,拥有可能世界上最大的仓库。

在你的项目目录下执行npm install <package>,相关依赖库就会下载到本地的node_modules

构建工具

我们写的代码在开发现代JavaScript的web应用程序几乎从来不是相同的代码,将去生产。我们编写代码在一个现代版本可能不支持JavaScript的浏览器,我们大量使用的第三方包在一个node_modules文件夹连同自己的依赖性,我们可以处理像静态分析工具或缩小镜,等构建工具的存在是为了帮助把这一切变成可以有效地部署和理解的大多数web浏览器。

模块打包

在浏览器原生支持模块化规范之前,如果你想写模块化的代码,必须提供实现加载模块的方法。在你的HTML中使用大量的script标签,并不是一个可行的方案,因为它很快就会变得很笨拙,  和所有这些单独的HTTP请求会导致性能问题。

我们需要利用ES2015 import语句(或CommonJS require)和使用模块打包工具将所有模块打包到一个或多个文件中。将打包后的文件上传至服务器,在html页面引入,这里面涵盖了所有的依赖的模块。

目前热门的打包工具:Webpack, BrowserifyRollup.js。可以按需求选择其中的一个。

如果你想深入学习打包原理的话,请看这篇文章 深入理解模块打包以及编译

Transpilation

只有现代浏览器才能支持这些新的ES6特性,如果你的用户还在使用旧版浏览器或移动设备,

我们需要将这些代码转换成上一个版本的Javascript(ES5), 使它们可以很好地运行在现代浏览器上,这种转换通常被称为 Transpilation。

web应用类型

网站和应用的需求不同,网站允许页面重载,应用应该尽可能接近原生应用。

Single Page Applications (SPAs)

web应用程序最常见的高级体系结构称为SPA,它代表单个页面应用程序。

1、html是空框架,js包含应用程序正常工作所需的所有内容。

2、UI完全依赖客户端渲染,无需整页刷新

3、客户端与服务端通过Ajax进行通信

这种方案的一个缺点是,首次应用程序需要更长的时间来加载;然而,一旦被加载视图(页面)之间的转换通常更快,因为它只是纯客户端和服务器之间发送的数据。

Universal / Isomorphic Applications

如果如果业务场景是需要更快的首屏渲染时间和最好的的SEO排名结果, 那么SPA不是一个好的选择,Isomorphic(同构)应用是最好的方案:

SSR + CSR + Isomorphic

  • 1)首次加载是 SSR;
  • 2)交互阶段用 CSR;
  • 3)页面跳转用 Client router,不产生对服务器的页面请求;
  • 4)SSR 和 CSR 共用一套代码,避免重复开发

项目部署

在现代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

src是我们的源码目录,里面的代码采用ES2015编写。lib是我们自己的公共库。

node_modules 是npm第三方库

然后您可以运行Gulp。将所有模块打包到一个文件中(包括安装了npm的模块),将ES2015+转换到ES5,生成压缩后的文件等。然后可以对其进行配置,将结果输出到dist目录中。

如果有文件不需要任何处理, 可以直接复制到dist目录中。您可以在Gulp配置这个任务。

最后您可以将文件从dist目录上传到web服务器,其它文件只在开发阶段有用,没必要上传到服务器。

团队协作开发

如果您与其他开发人员一起工作,那么您可能还在使用一个共享的代码托管库,如GitHub。在这种情况下,您可以在提交之前运行构建过程,并将结果与Git仓库中的其他文件一起提交,以便后续下载到生产服务器上。

但是,如果几个开发人员一起工作,在代码仓库中存储构建文件很容易出错,您可能希望从构建工件中清除一切。幸运的是,有一种更好的方法可以解决这个问题:您可以将Jenkins、Travis CI、CircleCI等服务放在流程中间,这样它就可以在每次提交提交到存储库之后自动构建项目。开发人员只需要push源代码而无需每次都去构建代码。存储库还可以清除自动生成的文件,最后,您仍然可以使用构建的文件进行部署。

后记

如果您近年来一直没有从事web开发,那么从简单的web页面转换到现代JavaScript应用程序,可能会让你望而生畏,但我希望读完这篇文章能打消你的疑虑。后面针对每个小节,我都会补充一些更深入的文章,已帮助您进一步学习。

前端技术栈变化很快,我们应该以什么样的心态面对,这里分享一个KISS原则:只使用您认为需要的东西,而不是您拥有的所有东西。

解决问题是最重要的,而不是使用最新的东西。