给恐龙们解释一下当今的Javascipt(译文)

恐龙 ,远古时代的动物,指的应该是像我这样,因为Internet的出现,20年前就知道,接触过Javascript,但还好没有”入坑”的程序员。

原文: Modern JavaScript Explained For Dinosaurs

如果你不是从Javascritp最开始一路跟下来,学习Javascript是件多么苦难的事你肯定不清楚。Javascript的生态发展和变化之快,之大,让人搞不懂这些五花八门的工具到底想干啥,想解决什么莫名其妙的问题。我自己从1998年开始学习编程但直到2014年才开始学习Javascript。在那时,我记得我看到Browserfy上有这么一段:

“Browserfy lets you require(‘module’) in the browser by bundling up all of your depednecies.”

我完全看不懂这是啥意思,并试图明白它对Javascript开发者有什么帮助。

这篇博客从提供一个历史的上下文去看Javascript工具是如何演变成了今天(2017年)的样子。我们从构建一个简单的网页例子开始,像恐龙级程序猿那样,不用任何工具,只有HTML和Javascript。从历史上下文着手,能让你更好的学习和适应还在不断变化和发展的Javascript,让我们开始吧。

最原始的方法

让我们用原始的方法 - 手工方式指定文件的下载和连接。下面就是一个简单的 intex.html 文件,里面包含Javascript代码文件的连接:

index.js就是和index.html同网站服务器目录下的另一个文件,内容如下:

<!-- 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:

// index.js
console.log("Hello from JavaScript!");

这两个文件就构成了你所需要的一个简单网页。现在我们加入一个简单的第三方Javascript库(不是我们自己开发的)moment.js,这个库可以帮我们格式化日期,举个例子,我们可以调用moment这个方法:

moment().startOf('day').fromNow(); // 20 hours ago

调用前提是我们必须把moment.js搞到我们的网站上来,从moment.js的网页上可以看到下面的信息:

moment.js

从安装的方法(右上部分)看到安装moment.js包含好几个动作,让我们先忽略之,我们直接下载 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.js是在index.js之前的,所以先被浏览器载入,也意味着我们才可以在index.js里调用moment方法:

// index.js
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());

哈哈,这就是我们使用各种Javascript库的原始方法。好处是它很简单,坏处是,每次我们都要去各个网站上找,然后手工下载这些库,每次库如果升级了,还得重复这个步骤。

包管理器的方法:NPM

从2010年起,各种Javascript包管理器『package manager』开始出现,可以自动下载、升级各种库。在2013年,Bower包管理器无疑是最流行的包管理器,但到了2015年左右,被npm超越 需要指明的是,Yarn作为npm替代品,虽然在2016年yarn从npm上抢走了很多的注意力,但Yarn内部依旧是npm

npm原来是作为node.js的包管理器,node.js是设计在服务器端运行的Javascript Runtime,而不是前端。让npm变成了在浏览器里运行的各种Javascript库的管理器,感觉会有点怪怪的。

包管理器的使用通常涉及Unix/Linux命令行,以前前端开发人员是不要求掌握的,如果你从未使用过Unix/Linux命令行,你可以阅读这个教程。如何使用命令行,对现代Javascript开发人员还是很重要的。

让我们看看如何用npm自动下载moment.js。如果你已经安装了node.js,那么npm已经安装了(包含在node.js里)。然后从命令行进入index.html所在目录,敲入:

$ npm init

你会面对几个问题,统统敲Enter,结束后就会出现一个新文件 package.json 。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"
}

现在可以根据npm用法来安装moment.js这个包,键入下面的命令:

$ npm install moment --save

这个命令会做两件事:下载momen.js包里所有的代码到一个叫 node_modules 的目录里,第二,更新package.json,并记录moment.js成为一个项目依赖:

这个将来分享项目给其它Javascript开发人员时有用,因为不需要分享node_module目录,那样太大了,只要分享package.json就好,然后npm install会根据它来下载和安装所需的Javascript包。

现在我们不再需要手工从moment.js网站上下载moment.js,而通过npm自动下载和更新。我们会看到 moment.js 其实存到了 node_modules/moment/min 目录里,这意味着在index.html里我们要加入这样的连接 <script src=”node_modules/moment/min/moment.min.js”>

<!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目录里找到包的位置并手工加入到index.html里,下面我们看看能不能把这后面这一步也自动完成呢。

npm for browser

Webpack:模块捆绑器

大部分的编程语言提供import机制把代码从一个文件载入到另一文件里。Javascript最初没有设计这个(没有module的概念),原因是Javascript只是设计在浏览器里跑的,而浏览器是没有读写本地文件系统的权限的(安全原因)。所以在很长时间里,如果你把Javascript代码分散在几个文件里,只能通过全局变量载入这些文件。这正是上面的例子所展示的,整个moment.min.js被载入到HTML里,并定义给全局变量moment,这个变量可以被之后载入任何Javascript文件所调用。

在2009年,开启了一个名为CommonJS的项目,为了在浏览器之外实现这个目标。CommonJS很大一部分是对Javascript的模块『module』给出规范,以便Javascript最终可以像其它编程语言那样可以在不同文件之间import或export,而非通过全局变量。而采用CommonJS最著名的就是node.js:

node.js

如前所说node.js其实是个Javascript服务器,这是早期在node.js里如何使用模块的例子,和上面的区别是在Javascript代码里直接载入而非HTML:

// index.js
var moment = require('moment');
console.log("Hello from JavaScript!");
console.log(moment().startOf('day').fromNow());

这是没有问题的,因为node.js是服务器端的程序,可以直接访问文件系统。node.js也知道npm module的路径,所以不需要这样载入 require(‘./node_modules/moment/min/moment.min.js) 而只需要简单的 require(‘moment’)  即可。

但如果试着在浏览器端这样做,会得到错误信息(require is not defined)。这就是需要模块捆绑器『module bundler』这种工具的原因。模块捆绑器通过一个构建动作产生最终和浏览器兼容的Javascript来解决这个问题。这个例子里,我们可以让模块捆绑器搜索所有的 require 语句(这些语句在浏览器里都是不支持),把它们取代成实际的文件,最终结果是一个大大的Javascript文件(这也是捆绑名字的由来),而且我们也就不再需要这些 require 语句了。

现在让我们来看看如何用Webpack实现前面这个例子。首先我们安装Webpack到这个项目里。Webpack本身也是npm的一个包,所以同样通过npm安装:

$ npm install webpack --save-dev

注意 --save-dev 这个参数表明Webpack是个开发依赖(开发环境依赖的包而非生产环境),这在项目文件 package.json 里可以看到(运行上面的命令后package.jso会被自动更新):

{
"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": "^3.7.1"
}
}

现在Webpack作为一个包也安装在了 node_modules 的目录里,接下来我们可以使用Webpack了:

$ ./node_modules/.bin/webpack index.js bundle.js

这个命令将会运行Webpack,从index.js开始,找出所有的 require,把他们相应的代码(从相应文件读取),最后产生一个文件叫 bundle.js 。这也意味着浏览器里我们将不在使用index.js而是bundle.js,所以index.html要做相应的更改:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript Example</title>
<script src="bundle.js"></script>
</head>
<body>
<h1>Hello from HTML!</h1>
</body>
</html>

如果在浏览器里访问这个html,结果就和开始一样。

如果服务器端每次更改index.js,我们都必须重跑Webpack,这是挺麻烦的,如果还想使用Webpack的高级功能(例如source maps),将会更麻烦。Webpack可以自动读取项目目录下的配置选项文件 webpack.config.js ,针对我们的例子,可以是这样:

// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js'
}
};

这样每次跑Webpack时,可以简化命令行参数,因为index.js和bundle.js已经在 webpack.config.js 里指明了:

$ ./node_modules/.bin/webpack

虽然简化了这个步骤,但每次更改index.js,还是要重复,接下来我们将进一步优化它。

到现在为止的一切看起来没有做很多事情,但整个开发流程而言顺畅不少,增加一个Javascript库,只需增加一条 require 语句而非在HTML里增加一条<script>,同时捆绑成一个Javascript文件,浏览器的载入也比较快。我们现在只在整个开发流程中增加了一个构建步骤,但构建过程里其实我们可以做更多の事,让我们一起继续见证Javascript的牛x或者说奇葩。

webpack

babel:转译代码

转译代码『Tanspiling』就是把一种编程语言转成类似的另一种语言。这对前端开发非常重要,因为浏览器的更新和标准化需要时间,通常比较慢,而新的前端开发语言可以不断试验新的特性,然后再转译成和浏览器兼容的语言。

转译『Tanspiling』好像是在Javascript发展中出现的,“恐龙”以前没有听过,只听过编译『Compiling』,区别是转译用于同质语言,编译用于不同质语言,转译是编译的特例。例如把C“编译”成机器代码,而把Typescript“转译”为Javascript。

对于CSS,这类新的语言有Sass,Less和Stylus等。对于Javascript,曾经最有名的是Coffeescript(出现于2010年),而现在用得最多的则是babel和Typescript。Coffeescript对Javascript的改进甚多 - optional parentheses,significant whitespace,等等。babel不是新的语言,它是一个转译器,把新版本的Javascript(ES2015或更高版本)转成老旧版本(ES5),因为不是所有的浏览器都支持新版本的Javascript。很多人选择babel因为它本身和Javscript很接近。

Javascript语言是个奇葩,其标准一直缺失,比较著名的是ECMAscript

让我们在上面的例子中加入babel。首先,安装babel:

$ npm install babel-core babel-preset-env babel-loader --save-dev

我们为开发环境安装了三个包:babel-core 这个是babel的主体,babel-preset-env 这个用来定义那些新的Javvascript特性需要转译,最后 babel-loader 则是为了让babel和Webpack共同工作。 我们需要在webpack.config.js中加入babel-loader:

// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}
};

这个语法有点让人困惑(好在我们不需要经常做这件事),基本上它是让Webpack找出所有的.js文件(node_modules目录之外)然后用babel-loader载入之。具体的用法需要学习和参考Webpack文档。

好,现在在我们的Javascript可以使用ES2015的特性,下面的例子是在index.js里使用ES2015 template string

// 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 statement而不是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

我们可以刷新浏览器页面来看看babel是否完成它的工作,当然现在许多新版本的浏览器都支持ES2015,但你可以用老的浏览器试试例如IE9 ,或者你在bundle.js里看看有没有转译后的代码:

// bundle.js
// ...
console.log('Hello ' + name + ', how are you ' + time + '?');
// ...

虽然这个例子没有什么好让人激动的,但代码转译是很厉害的功能。例如新的Javascritp特性async/await能让你写出更好的代码。尽管转译有时看起来麻烦,但是对Javascript的改进在过去几年起了巨大的作用,因为程序员可以现在就使用和测试其未来的语言特性。

我们粗略完成想要做的事,整个流程还是有很多可以改进的地方。如果我们关心性能,我们可以压缩最终的捆绑文件,这个不难,只需要往Webpack里加。所以下面我们看看各种方便的工具,用来解决各种各样的问题。

npm脚本:task runner

现在我们只针对Javascript的模块增加了一个构建步骤,对于复杂情况,则应使用task runner,它是属于自动化的构建工具,自动化各种构建过程中要完成的任务,对于前端开发而言,这些任务包括了最小化代码,优化图片,运行测试代码,等等。

在2013年,Grunt成为了最流行的前端构建任务自动化工具,后来是Gulp。两者都依赖不同的plugins,这些plugins的背后其实是不同的命令行工具。现在最流行的做法则是直接利用npm包管理器的脚本功能,不需要plugin而是通过脚本直接调用这些工具。

让我们来看看用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 -p",
"watch": "webpack --progress --watch"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"webpack": "^3.7.1"
}
}

我们增加了两段脚本buildwatch。执行下面的命令就可运行build脚本:

$ npm run build

这将触发Webpack运行,并且显示执行的进度(–pregress),同时最小化输出(-p).

执行下面的命令就可运行watch脚本:

$ npm run watch

这将触发Webpack自动重新运行如果Javascript的文件有所改变(–watch),这对开发是很有帮助的。

注意我们不必指明Webpack的路径,因为node.js知道npm模块都在node_modules目录下。我们还可以安装webpack-dev-server这辅助工具,这是个简易实时重载的web服务器:

$ npm install webpack-dev-server --save-dev

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 -p",
"watch": "webpack --progress --watch",
"server": "webpack-dev-server --open"
},
"author": "",
"license": "ISC",
"dependencies": {
"moment": "^2.19.1"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"webpack": "^3.7.1"
}
}

我们可以启动它:

$ npm run server

它会自动在你浏览器里打开index.html(localhost:8080)。任何时候如果你改动了index.js里的Javascript,webpack-dev-server会自动重新构造捆绑Javascript文件并且让你的浏览器自动刷新。这不仅节约你的时间,而且让你专注在你的代码,而不是在代码,浏览器之间不停切换以检测所做的修改。

你还可以做更多的事情,你可以在这里了解更多的Webpack以及webpack-dev-server。当然你也可以类似的用npm脚本把Sass转换成CSS,压缩图像文件,等等,任何命令行可以做的事都可以包含进来。更多npm脚本的高级特性可以观看Kate Hudson的这个讲座

结语

这就是现代Javascript开发最基本的部分。我们从纯HTML和Javascript,到使用包管理器自动下载第三方包,通过模块捆绑打包成单一Javascript文件,使用转译器来提前使用Javascript的新特性,并可以用任务自动化工具来自动化各种构建任务。对初学者来讲,已经经历了不少。对于任何进入编程的新人,前端开发曾经是一个很好的切入点,因为那时前端开发非常容易起步,直接运行,现在则非常吓人,因为有了各种各样复杂的工具,而且还在不断迅速的改变着。

当然现在的情况也没有那么坏,因为随着node.js的生态系统被广泛接受,使用npm作为包管理器,node require 或 import 语句来调用模块,npm脚本来运行完成各种任务,即简化了开发流程,又让工具使用保持一致性,这比前一两年有了太大的转变。

无论是新手还是有经验的开发人员,前端开发框架现今多数都提供了相应的工具让其容易上手:Ember有ember-cli,然后它影响了Angular - angular-cli,接着是React - create-react-app,Vue - vue-cli,等等。这些工具会把一个项目所需的一切准备工作做好,让你可以立马开始写代码。当然工具不是魔法,只是它们使用一致的方法,很多时候,在某个点你需要Webpack,babel等做一些特殊的事情,所以了解本文上面所描述的各个环节就很重要了。

现今的Javascript开发是比较让人沮丧的,因为其一直以很快的速度变化着。虽然很多时候看起来在重复发明轮子,但Javascript的快速演化还是帮助其不断推出创新技术,例如hot reloading,real-time linting,time-travel debugging。成为程序猿还是一个很令人激动的事情,我希望本文能在你成为开发人员的路途上带来指示性帮助。

The End

附录

2017年Javascript的使用报告:the State of Javascript 2017

2018年趋势展望和关注点:The Top JavaScript Trends to Watch in 2018中文翻译

tl;dr 2018年你要干的事:

  • 必备技能:
    必备技能
  • 前端技能:
    前端技能
显示 Gitment 评论
0%