如果你从一开始就没有接触过JavaScript,那么学习现代JavaScript是很困难的。生态系统的增长和变化如此之快,以至于很难理解不同工具试图解决的问题。我1998年开始编程,但直到2014年才开始认真学习JavaScript。我记得当我看到Browserify时,我盯着它的标语:
“Browserify 可以让你在浏览器中使用require('modules') 语句来打包处理所有的依赖”
我几乎不理解这句话中的任何一个词,我努力理解这对作为开发人员的我有什么帮助。
本文的目标是提供一个JavaScript工具链是如何发展到2017年(这篇文章写于2017年)的历史背景。我们将从头开始,建立一个像上图中恐龙做的样例网站-不适用任何工具,只是简单的HTML和JavaScript。然后,我们将逐步介绍不同的工具,以了解它们一次解决一个问题。有了这样的历史背景,您将能够更好地学习和适应不断变化的JavaScript前景。让我们开始吧!
更新:我做了一个这篇文章视频教程版本,它一步一步地讲解每个部分,更清晰,请点击这里查看:
https://firstclass.actualize.co/p/modern-javascript-explained-for-dinosaurs
以“老派”的方式使用JavaScript
让我们从一个使用HTML和JavaScript的“老派”网站的写法开始,这种方式需要到手动下载和链接文件。下面是一个简单的index.html文件,它链接到一个JavaScript文件:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Example</title>
<script src="index.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
这行<script src="index.js"></script>指向的是同一个目录下名为index.js的单独的一个JavaScript文件:
// index.js
console.log("Hello from JavaScript!");
用这种方式,你已经做出了一个网站!但是现在,我们假设您想要添加一个别人编写的库,比如moment.js(一个可以帮助以人类可读的友好方式格式化日期的库)。例如,你可以像下面这样在JavaScript中使用moment函数:
moment().startOf('day').fromNow(); // 20 hours ago
但使用这种方式,你首先必须要在你的网页里加载上moments.js! 在moment.js这个库的主页上,你可以看到以下说明:
呃,在右边的安装教程部分提供了很多种方式。但现在我们先忽略它(因为我们要用古老的前端开发方式)——我们可以下载moment.min.js文件并将其放在index.html文件中,从而将moment.js添加到我们的网站中。
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example</title>
<link rel="stylesheet" href="index.css">
<script src="moment.min.js"></script>
<script src="index.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
注意,moment.min.js需要在index.js之前加载,然后你可以像下面这样在index.js中使用moment函数:
// index.js
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
这就是我们以前用JavaScript的库制作网站的方法!好的地方是它很容易理解。不好的地方是,每次库有更新时都我们要找到并下载新版本的库,替换旧文件,这很烦人。
使用JavaScript的包管理器npm
大约从2010年开始,出现了几个相互竞争的JavaScript包管理器,它们可以帮我们从中央存储库自动下载和升级JavaScript库。Bower可以说是2013年最受欢迎的包管理器,但逐渐在2015年左右被npm超越。(值得注意的是,从2016年年底开始,yarn作为npm方式的替代品获得了很多关注,但它在内部仍然使用npm包。)
注意,npm最初是专门为node.js设计的包管理器,node.js是一个JavaScript运行时环境,运行在服务器,而不是前端。因此,很多人会困惑,为什么要用npm来管理运行在浏览器中的前端JavaScript库。
注意:使用包管理器通常涉及到使用命令行,这在过去的前端开发中是不需要的。如果你从未使用过命令行,你可以阅读这个教程来获得一个大概的认识。无论是好是坏,知道如何使用命令行是现代JavaScript开发的重要组成部分(它也为其他开发领域打开了大门)。
让我们看看如何使用npm来自动安装moment.js包,而不是手动下载它。如果你已经安装了node.js,那么你已经安装了npm,这意味着你可以在命令行中切换到index.html文件所在的文件夹,然后输入:
$ npm init
然后它会给你一些提示问题(默认设置就行,你可以为每个问题直接按下“Enter”),并生成一个名为package.json的新文件。这是npm用来保存所有项目信息的配置文件。默认情况下,package.json的内容应该是类似这样的:
{
"name": "your-project-name",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC"
}
为了安装moment.js这个JavaScript包,我们现在可以按照它的主页上的npm的指令,在命令行中输入以下命令:
$ npm install moment --save
这条命令做两件事——首先,它将moment.js包中的所有代码下载到一个名为node_modules的文件夹中。其次,它会自动修改package.json文件以跟踪moment.js,使其作为项目依赖项。
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.22.2"
}
}
这在以后与他人共享项目时很有用——你不需要共享node_modules文件夹(这个文件夹可能会变得非常大),您只需要共享package.json文件,其他开发人员可以使用npm install命令自动安装所需的包。
所以现在我们不再需要手动从网站下载moments.js,我们可以使用npm自动下载和更新它。在node_modules文件夹中,我们可以看到node_modules/moment/min目录中的moment.min.js文件。这意味着我们可以在index.html文件中链接到npm下载的moment.min.js,如下所示:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Example</title>
<script src="node_modules/moment/min/moment.min.js"></script>
<script src="index.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
所以这样做的好处是,现在我们可以使用npm通过命令行下载和更新我们的包。不好的一点是,我们必须进入node_modules文件夹,找到每个包的位置,并手动将其包含在HTML中。这是非常不方便的,所以接下来我们将看看如何自动化这一过程。
使用JavaScript模块绑定器 (webpack)
大多数编程语言都提供了一种将代码从一个文件导入到另一个文件的方法。JavaScript最初并没有设计这个特性,因为JavaScript被设计为只能在浏览器中运行,不能访问客户端计算机的文件系统(出于安全原因)。因此,在很长一段时间里,将JavaScript代码组织到多个文件中需要使用全局共享的变量来加载每个文件。
这实际上就是我们在上面的moment.js例子中所做的——整个moment.min.js文件被加载在HTML中,它定义了一个全局变量moment,然后它对在moment.min.js之后加载的任何文件都可用(不管这些文件是否需要访问它)。
2009年,一个名为CommonJS的项目开始了,其目标是为浏览器之外的JavaScript指定一个生态系统。CommonJS对JacaScript做了模块规范,这使得JavaScript能像大多数编程语言一样跨文件导入和导出代码,而不需要求助于全局变量。node.js就是CommonJS模块规范最广为人知的实现。
如前所述,node.js是一个用于在服务器端运行的JavaScript运行时环境。前面那个网页的示例如果用node.js模块语句来写就是如下所示的代码。不用使用HTML脚本标签来加载moment.min.js,你可以直接在JavaScript文件中加载它,如下所示:
// index.js
var moment = require('moment');
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
这就是node.js中模块加载的方法,因为node.js是一种服务器端语言,它可以访问计算机的文件系统。Node.js还知道每个npm模块的路径,所以不必写require('./node_modules/moment/min/moment.min.js'),你可以简单地写require('moment'),非常好用吧!
这种机制对于node.js来说很好,但如果你试图在浏览器中使用上面的代码,你会得到一个错误,说require没有定义。浏览器不能访问文件系统,这意味着以这种方式加载模块是非常难的——加载文件必须动态完成,要么同步加载(会减慢执行速度),要么异步加载(会有时间问题)。
这就是模块绑定器的作用所在。JavaScript模块绑定器是一种工具,它通过build(构建)步骤(这一步运行在本地,因此可以访问文件系统)来解决问题,并创建与浏览器兼容的打包好的JavaScript文件(不需要访问文件系统)。在这种情况下,我们就需要一个模块绑定器来查找所有require语句(require语法在浏览器中不支持),并将它们替换为每个所需文件的实际内容。最终的结果是一个捆绑的JavaScript文件(没有require语句)!
最流行的模块绑定器是Browserify,它于2011年发布,最早在前端使用node.js风格的require语句(这使npm成为首选的前端包管理器)。大约在2015年,webpack逐渐成为了更广泛使用的模块绑定器(得益于React前端框架的流行,它充分利用了webpack的各种特性)。
让我们来看看如何使用webpack来让上面的require('moment')示例在能够浏览器中工作。首先,我们需要将webpack安装到项目中。Webpack本身是一个npm包,所以我们可以从命令行安装它:
$ npm install webpack webpack-cli --save-dev
注意,上面这条命令给我们安装了两个包——webpack和webpack-cli(它允许你从命令行使用webpack)。还要注意--save-dev参数——它将webpack保存为开发依赖项,这意味着它是开发环境中需要的包,而不是生产服务器上需要的包(生产环境上只需打包好的JavaScript文件即可,不需要打包工具)。你可以在package.json文件看到,它已经自动自动更新了:
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
},
"devDependencies": {
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0"
}
}
现在我们已经在node_modules文件夹中以包的形式安装了webpack和webpack-cli。你可以在命令行中使用webpack-cli,如下所示:
$ ./node_modules/.bin/webpack index.js --mode=development
该命令将运行安装在node_modules文件夹中的webpack工具,从index.js文件开始,找到所有的require语句,并用适当的代码替换它们,然后创建出单个输出文件(默认为dist/main.js)。--mode=development参数是为了保持JavaScript对开发人员的可读性,而--mode=production则是打包成利于服务器部署环境的最小化输出。
现在我们有了webpack生成的dist/main.js输出,我们将在浏览器中使用这个文件而不是index.js,因为index.js包含无效的require语句。相应的index.html应该修改如下:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Example</title>
<script src="dist/main.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>
如果您刷新一下浏览器,您应该看到一切都像以前一样正确运行!
注意,每次修改index.js时,我们都需要运行webpack命令。这很麻烦 ,而且当我们使用webpack的更高级特性时(比如生成源代码映射以帮助从编译的代码调试原始代码),会变得更加困难。Webpack可以从项目根目录下的名为webpack.config.js的配置文件中读取选项,在我们的例子中是这样的:
// webpack.config.js
module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: 'main.js',
publicPath: 'dist'
}
};
现在,每次修改index.js时,我们都可以使用下面的命令来运行webpack:
$ ./node_modules/.bin/webpack
我们不再需要指定index.js和--mode=development选项,因为webpack会从webpack.config.js文件中加载这些选项。这样做是方便了一点,但仍然很麻烦,因为每次还要运行这条命——我们将使这个过程更便捷一点。
现在看来,我们费这么大劲使用了webpack,感觉工作效率并没有提升太多。但这个工作流程还有一些巨大的优势。我们不再通过全局变量加载外部脚本。任何新的JavaScript库都将使用JavaScript中的require语句添加,而不是在HTML中添加新的<script>标签。使用单个打包好的JavaScript文件通常也更有利于性能。现在我们使用了build步骤,还有一些其他强大的特性可以添加到我们的开发工作流中!
使用Babel将代码转译,引入新的语言特性
转译代码就是把代码从一种语言翻译成另一种相似的语言。这是前端开发的一个重要部分——因为浏览器添加新特性的速度很慢,所以必须要讲太过于新的(带实验性质特点的)语言转义成浏览器都能够兼容的语言。
比如CSS,它可以用Sass、Less和Stylus这些高级一点的语言来替代。对于JavaScript,最流行的转译器是CoffeeScript(2010年左右发布),而现在大多数人使用babel或TypeScript。CoffeeScript是一种专注于通过显著改变语言来改进JavaScript的语言——它引入了可选的括号,显著的空格,等等。Babel不是一种新的语言,而是一种转译器,它可以将尚未在所有浏览器(ES2015及以上)上可用的特性的下一代JavaScript转换为更老的兼容JavaScript (ES5)。Typescript是一种基本上与下一代JavaScript相同的语言,但也添加了可选的静态类型。许多人选择使用babel,因为它最接近原生的JavaScript。
让我们看一个如何在我们现有的webpack构建的例子中加入babel。首先,我们使用命令行安装babel(一个npm包)到项目中:
$ npm install @babel/core @babel/preset-env babel-loader --save-dev
注意,我们安装了3个独立的包作为开发依赖——@babel/core是babel的主要部分,@babel/preset-env是一个预先定义好的如何转义新的JavaScript特性的配置插件,babel-loader是一个能够让babel与webpack一起工作的包。我们可以通过编辑webpack.config.js文件来配置webpack使用babel-loader,如下所示:
// webpack.config.js
module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: 'main.js',
publicPath: 'dist'
},
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
这种配置编写可能会令人困惑(幸运的是,我们不会经常编辑这个文件)。基本上,我们告诉webpack寻找任何.js文件(不包括node_modules文件夹中的文件),并使用babel-loader和@babel/preset-env预设来应用babel转译。你可以在这里阅读更多关于webpack配置语法的内容。
现在一切都设置好了,我们可以开始用JavaScript编写ES2015特性了!下面是index.js文件中的一个ES2015模板字符串示例:
// index.js
var moment = require('moment');
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
var name = "Bob", time = "today";
console.log(`Hello ${name}, how are you ${time}?`);
我们还可以使用ES2015 import语句,而不是require语法来加载模块,这种写法现在更为常见:
// index.js
import moment from 'moment';
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());
var name = "Bob", time = "today";
console.log(`Hello ${name}, how are you ${time}?`);
在本例中,import语法与require语法没有太大区别,但是对于更高级的情况,import具有额外的灵活性。因为我们改变了index.js,所以我们需要在命令行中再次运行webpack:
$ ./node_modules/.bin/webpack
现在我们在浏览器中刷新index.html。在撰写本文时,大多数现代浏览器都支持ES2015的所有特性,所以很难判断babel是否完成了它的工作。你可以在像IE9这样的旧浏览器中测试它,或者你可以在main.js中搜索到转译的代码行:
// main.js
// ...
console.log('Hello ' + name + ', how are you ' + time + '?');
// ...
这里你可以看到babel将ES2015模板字符串转换成普通的JavaScript字符串连接,以保持浏览器兼容性。虽然这个示例可能不太令人兴奋,但是它的代码转换能力是非常强大的。JavaScript中有一些令人兴奋的语言特性,比如async/await,让您可以用它们来编写更好的代码。虽然转译有时看起来很乏味和痛苦,但在过去的几年里,它已经使得语言有了巨大进步,因为人们可以在当前来测试未来的功能。
到这一步,我们几乎完成了,但在我们的工作流程中还有一些未抛光的边缘。如果我们关心性能,我们应该最小化bundle文件,这应该很容易,因为我们已经加入了build构建步骤。我们还需要在每次修改JavaScript时重新运行webpack命令,我们马上来解决这个问题。所以接下来我们还需找到一些方便的工具来帮忙。
使用任务运行程序(npm脚本)
既然我们已经了解了如何使用build构建步骤来处理JavaScript模块,那么使用一个任务运行器就很有意义了。任务运行器是一种自动完成构建过程不同部分的工具。对于前端开发,任务包括了最小化代码、优化图像、运行测试等。
2013年,Grunt是最受欢迎的前端任务运行器,Gulp紧随其后。两者都是依赖于封装其他命令行工具的插件。现在最流行的选择是使用npm包管理器本身内置的脚本功能,它不使用插件,而是直接与其他命令行工具一起工作。
让我们写一些npm脚本,让webpack的使用更容易。只需要简单地更改package.json文件如下:
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack --progress --mode=production",
"watch": "webpack --progress --watch"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.22.2"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.2",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0"
}
}
在这里,我们添加了两个新脚本,build和watch。要运行构建脚本,你可以在命令行中输入:
$ npm run build
这将运行webpack(使用我们之前做的webpack.config.js中的配置),使用--progress选项来显示进度百分比,使用--mode=production选项来最小化生产环境的代码。运行watch脚本,输入:
$ npm run watch
它使用--watch选项,现在每次修改JavaScript文件后,webpack将会自动重新运行,这对开发非常有用。
请注意package.json中的脚本可以运行webpack并且不需要指定完整路径./node_modules/.bin/webpack,因为node.js知道每个npm模块的路径。真是太好了!我们还可以安装webpack-dev-server,这是一个独立的工具,它提供了一个简单的web服务器,可以实时重新加载。要将它作为开发依赖项安装,输入命令:
$ npm install webpack-dev-server --save-dev
然后修改一下package.json加入npm脚本:
{
"name": "modern-javascript-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack --progress -p",
"watch": "webpack --progress --watch",
"server": "webpack-dev-server --open"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.2",
"webpack": "^3.7.1",
"webpack-dev-server": "^3.1.6"
}
}
然后你可以使用以下脚本来运行你的开发环境代码:
$ npm run server
它会自动在浏览器中以localhost:8080(默认)的地址打开index.html网页。当你在index.js中更改JavaScript代码时,webpack-dev-server将重新构建打包好的JavaScript代码并自动刷新浏览器。这将节省您很多时间,而且它让您将注意力集中在代码上,而不必不断地在代码和浏览器之间切换来查看新的更改。
我们以上的这些操作仅仅只是触及了表面,webpack和webpack-dev-server还有很多选项(你可以在这里阅读)。当然,你也可以制作npm脚本来运行其他任务,比如将Sass转换为CSS,压缩图像,运行测试——任何支持命令行工具的都是可行的。npm脚本本身也有一些很棒的高级选项和技巧——Kate Hudson的这次演讲可以作为一个入门教程:
总结
简而言之,这就是现代JavaScript开发的工作流程!。我们从普通的HTML和JavaScript过渡到使用包管理器来自动下载第三方包,使用模块绑定器来创建单个脚本文件,使用转译器来使用未来的JavaScript特性,以及使用任务运行器来自动化构建过程。对初学者来说这是一种很大的转变。Web开发曾经是编程新手的一个很好的入门点,因为它非常容易启动和运行。但如今,它可能有点令人生畏,特别是因为各种工具往往会快速变化。
不过,这并不像看上去的那么糟糕。这些问题都在一步一步得以解决,特别是采用node.js生态系统作为前端工作开发这种方法。使用npm作为包管理器、node的require或import这些模块语句以及用于运行任务运行的npm脚本非常棒,它带来了一致性。与前几年前相比,这是一个大大简化了的工作流程!
对于初学者和有经验的开发人员来说,现在的框架通常会附带工具,能让开发过程更容易,这点更好!Ember有Ember-cli,它引领出Angular的Angular-cli、React的create-react-app、Vue的vue-cli等。所有这些工具都可以将您所需要的提前设置好-您所需要做的就是开始编写代码。然而,这些工具其实并没有什么神奇之处,它们只是把一切都统一化了——不过你可能经常需要对webpack、babel等做一些额外的配置。因此,正如我们在本文中所介绍的那样,理解每一部分的作用仍然非常重要。
使用现代JavaScript开发流程一开始肯定会令人沮丧,因为它不停的在快速变化。仅管有时好像是在重复造轮子,JavaScript的快速发展已经推动了诸如热加载、实时检测和时间旅行调试(time-travel debugging)等创新。对于开发人员来说,这是一个激动人心的时刻!我希望这些信息可以作为路线图,帮助您踏上征程!