在很久之前的前端开发被人们称为切图仔,还完全没有一种程序员工种叫前端工程师,在 2013 年之前我记得我当时打开一些网站,这些网站还是使用的 Adobe Flash Player 这种技术来实现网站动画效果,现在前端经过这么久的发展已经不像是以前了。在 6 年前如果你只会 JQuery + Bootstrap 框架就可以找到一个很好前端工作,但是今天的前端开发已经不是这样子了,需要掌握很多技术。我写前端也很早,在那个时候只需要会有的 CSS 加 HTML 乃至不需要 JS 这种也能做一个网站,例如 PHP 和 JSP 这种技术可以在 HTML 代码中写各自的逻辑代码控制网站显示部分。但今天前端工程师有很多技术栈的支持,例如 Node.js 运行时的支持,可以让前端工程师写服务端程序 (Server Side Render)技术,前端试图层开发也不是以往复制几行别人写好的 div 代码快和 class 样式名就可以实现一个页面了,今天前端试图层主流的是使用 React 运行时解析和渲染式框架和 Svelte 提前编译式框架。不管哪样框架开发,已经不是以往那种在电脑上安装个 Sublime 编辑器和 Chrome 就可以去开发前端项目的时代了,现在前端开发需要掌握很多 JS 工具软件来完成,这篇文章将完整的介绍现在前端必须掌握的工具链软件,例如 Node.js 、NPM 、MVN 、Babel 、Webpack 、JSDoc 这些工具使用教程。


Node.js 运行时

目前最为流行的 JS 运行环境是 Node.js 运行时,当然还有其他的不同类型的 JS 运行时,但是目前工业界使用的最多还是 Node.js 运行时。运行时有两种含义,第一种是能让传统的 JS 代码脱离浏览器端到服务器端运行,这里的服务器端更多的是只的是个人电脑上,使得 JavaScript 语言与操作系统互动比如:读写文件 、新建子进程 、网络操作等,其次提供很多方便 JS 开发的工具。

上图为一个基本的 Node.js 运行时架构,Node.js 运行时中包括了 libuv 和 Google 的 V8 ,JS 代码跑在 V8 引擎上,而 Node.js 内置的 fs 、 http 等核心模块通过 C++ Bindings 调用 libuv 等 C/C++ 类库,从而获取到操作系统提供的平台能力,这些系统软件一般都是使用 C/C++ 进行编写,笔者对于 C/C++ 不太熟悉,所以这里不太叙述。


npm 包管理

这里首先要介绍的是 npm (Node Package Manager) 软件包管理工具,使用它可以构建应用程序的代码、库、模块等和管理项目第三方软件包的功能,可以快速安装 、卸载 、共享发布你自己的包的功能。解决的问题就针对于传统前端开发使用的 zip 包或者 cdn 来管理 js 文件问题。如果电脑上安装了 Node.js 运行时 npm 也会一同安装在你对电脑系统之上;下面是一些最为常用的 npm 命令:

命令描述
npm init初始化一个项目
npm init --yes直接初始化一个项目
npm install package_name安装第三方包
npm install package-name --save-dev安装第三方包到开发环境
npm i -D nodemon安装第三方包到开发环境(简写形式)
npm i [email protected]安装指定版本的第三方包
npm remove jquery移除指定的第三方包
npm uninstall xxxx等效于卸载某个包
npm i -g xxx全局安装某个软件包
npm ls查看当前项目安装的软件包
npm ls -g查看全局安装的软件包

在一个目录中执行了 npm init 命令之后,npm 会自动初始化这个项目的目录,会在目录中自动创建一个 package.json 的文件,接下来篇幅会着重围绕着这个文件做介绍,如下图:

至于 package.json 文件是以 json 的方式进行格式化的,主要的字段保存着本仓库的开发者元数据信息,package.json 包含着开发作者姓名、邮件、主页、开放源代码许可证字段,部分字段看名字也知道是什么意思。

但是每次创建项目的时候都要重复输入此类信息,这时可以在 npm 的配置中配置全局的默认信息,这样就可以不需要每次都是需要手动输入信息,直接从默认的配置文件中读取,存放着写信息的文件会放在用户主目录的 ~/.npmrc 文件中,设置操作命令:

npm set init-author-name 'Leon Ding'
npm set init-author-email '[email protected]'
npm set init-author-url 'https://ibyte.me'
npm set init-license 'MIT'

要重点是介绍一些特殊的字段,这里针对上 scriptsdependenciesdevDependencies 字段进行介绍:

"scripts": {
    "start": "node index.js",
    "test": "mocha tests",
    "build": "webpack",
    "lint": "eslint src"
},

例如有一个 scripts 的字段包含着上面键值对信息,左边双引号包含的是命令行别名参数信息,而右边则是对应的真实命令参数,可以使用通过别名运行对应的命令。例如上面的 "start": "node index.js" 可以通过简短的 npm run start 的命令快速启动 node.js 运行时来执行 index.js 程序入口文件,其他 scripts 以此类推进行。

另外一个 package.json 中的 dependencies 字段结构内容会保护当前项目中安装的第三方包依赖信息,当项目被打成 zip 分享出去时,其他人只需要在自己电脑上执行 npm install 命令自动安装,这样就达到项目源代码和项目依赖信息保持一致的目的,默认情况下 npm 命令会主动去 npmjs.com 下载对应的软件包,这些包会被存储在项目的根目录中的 node_modules 中,例如下面内容:

{
  "name": "my-nodejs-app",
  "version": "1.0.0",
  "description": "A simple Node.js application",
  "dependencies": {
    "express": "^4.17.1",
  }
}

其中的 express 就为通过 npm 安装的第三方软件包,express 可以用于构建 Web 服务器和 Web 应用程序,后面双引号 ^4.17.1 代表着允许安装续"4.17.1" 以后的更高向后兼容的新版本例如 "4.18.0"、"4.19.0" 等版本的 Express 但不包括 "5.0.0" 及其以上的版本,dependencies 表示的是运行本项目所需要的依赖,如果项目的 node_modules 中没有包含对于的依赖软件包源代码,就会导致不能正常运行项目,常用的命令如下:

# 本地安装一个软件包
npm install <package-name>
# 简写成为 i
npm i <package-name>@<version>
# 也可以从远程 git 仓库安装
npm install git://github.com/<name>/<package-name>.git#<version>
# 全局安装
sudo npm install -g <package-name>
# 强制安装某个库
npm insatll -force <package-name>
# 更新软件包
npm update <package-name>
# 卸载软件包
npm uninsatll <package-name>

剩下的就为 devDependencies 字段,该字段保存着开发环境所需要依赖的信息,该字段软件包依赖只能在开发阶段所使用的,例如 js 的 eslint 工具,就为开发环境所使用的,当执行这条命令:

# 安装 eslint 到项目开发依赖
npm i eslint --save-dev
# 等价于 -D 表示开发环境依赖
npm i -D eslint

当这条命令执行完成之后 ESLint 的软件包会被下载到 node_modules 目录中,另外 ESLint 本就为一个可执行命令,它还会在生成一个node_modules/.bin/eslint 可执行脚本,该 .bin 从名称就可以看出对于到可执行文件存放的目录。

.bin 不仅可以存放普通的 js 文件,还可以存放 Linux 的下的 bash 文件,这些文件可以在 package.json 中的 scripts 字段属性所引用,例如下面有一个文件较 build.sh 文件内容如下:

#!/bin/bash
echo "execute linux bash script."

给文件添加上可执行权限,就可以在 scripts 进行起别命进行使用,例如的使用如下:

"build-js": "bin/build.sh"

至此就可以使用 npm run build-js 运行引用的 bash 脚本文件中的内容。


nvm 版本管理

对于一个长期从事使用 JS 作为主力开发语言的工程师,如何选择对应版本的的 Node.js 运行时?在不同项目使用着不同运行时,开发环境和线上环境版本如何选择?更为关键的是一个团队人员的运行时怎么统一?不同的版本之间可能存在一些兼容性的问题,如何快速切换 Node.js 版本?这里要介绍的是一款名为 NVM (Node Version Manager) 的工具,使用它可以快速在电脑上安装不同版本的 Node.js 运行时,它的项目地址:

https://github.com/nvm-sh/nvm

使用 nvm 命令来管理依赖版本,要比直接通过官网的方式安装的 Node.js 要方便很多,nvm 可以列出目前 Node.js 官方已经发布共用户选择的版本信息,常用的命令也就那么几个:

命令描述
nvm use stable安装当前可用的稳定版本
nvm ls-remote列出远程版本信息
nvm install x.x.x安装一个特定版本(TLS)
nvm alias default x.x.x设置默认版本
nvm use default使用默认版本
nvm use node快速切换到当前可靠的版本
nvm use x.x.x切换到某个特定的版本

使用 nvm ls 命令可以快速查看到当前系统 nvm 的软件包管理状态信息,如下如:

有 nvm 来帮助管理基础的 Node.js 环境,使得在开发一些对于运行时环境有要求的项目来说更为方便的切换 Node.js 版本,而不需要手动去设置环境变量和开发环境。


Babel 编译器

写过静态编译型语言的工程师应该知道 Compiler 的作用,可以将我们编写的源代码中人类可读翻译成为机器电脑可读的机器码,在 JS 中也一样有编译器的存在,什么?这里可能有人会问了 JS 不是解释的动态语言吗?怎么还需要进行编译呢?这里要归咎于 JS 发展历史原因,JS 的发展也伴随着它语言规范的发展 ECMAScript 标准,而 JS 除了 Node.js 这样的运行时之外,它的宿主环境为各个浏览器里面的执行引擎。我们工程师使用 JS 编写的程序最后大部分都是运行在浏览器中,导致一个问题用户电脑上安装的浏览器都是不同的版本,而我们编写的源代码采用 ES 版本可能在运行它的浏览器中新的特性语法得不到支持,浏览器 ES 版本可能只支持 ES5 ,而编写程序则使用的 ES6 版本,那么浏览器就不认识高版本 ES6 所编写的代码?怎么解决这个问题?这里就可以使用 Babel 来解决这个问题,按它官网上介绍 Babel 是一个 JavaScript 编译器 Compiler ?对没错就是 Compiler ,和 TypeScript 的 TSC 一样,可以将高版本的 ES6 语法编写的代码通过 Babel 编译器,转换为 ES5 版本的代码,从而使得低版本的浏览器也能运行 ES6 所编写的程序代码。

准确来说 Babel 是一个 ES 语法树的转码器,如图上所示, Babel 将代码转换成 AST 后,最后对新版本的 AST 进行编辑,这个如果读者了解编译原理的话叫:语法解析树,整个流程 解析 (parse)-> 转换 (transform)-> 生成 (generate) ,最后得到 AST 为旧版本的 ES 语法树。在 Babel 的官方网站上就给出实际用例,这是一个将 ES6 多 map 函数 arrow function 通过 Babel 转换为 ES5 的语法例子:

// Babel 输入:ES2015 箭头函数
[1, 2, 3].map(n => n + 1);

// Babel 输出:ES5 等价语法
[1, 2, 3].map(function(n) {
  return n + 1;
});

要在前端工程中使用 Babel 必须要在项目的目录中创建对于的规则配置文件,它的配置文件分为很多种,这里博文我只会介绍关于 babel.config.json 的方式配置,更多配置可以去查看它们官方文档,官方文档阅读起来有点复杂。推荐从我这篇博文入手,在项目根目录创建一个名为 babel.config.json 的文件,其中包含以下内容:

{
  "presets": [...],
  "plugins": [...]
}

Babel 提供了多种配置规则来满足不同项目的需求,因为现在前端项目有很多框架和不同 JS 变种例如 React 的 JSX 和 TypeScript 这种语法,所以 Babel 也提供对应的转换规则,配置文件中主要的有 2 个字断,分别是 PresetsPlugins ,它们的值是数组类型,可以是多个值组成的。Babel 提供了一些常用的预设,如: @babel/preset-env(用于根据目标环境自动确定需要的转译插件)、@babel/preset-react(用于处理 React JSX 语法)、@babel/preset-typescript(用于处理 TypeScript 语法)等,而 Plugins 是插件单元,用于处理特定的语法或功能,这里日常使用都是有别人已经提供好的插件,也可以自定义插件来使用。最后可以将多个预设和插件组合起来形成一个配置集,以满足特定的转译需求,例如下面例子:

# 安装了 Babel 插件和预设
sudo npm install @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-arrow-functions --save-dev

上面这行命令会安装用于处理 ES6 版本的 babel/preset-env 用于根据目标环境自动确定需要的转译插件,和处理 arrow fucntions 新特性的插件,配置文件则编写:

{
  "presets": ["@babel/preset-env"],
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

最后为了测试 Babel 的功能,使用 ES6 版本的语法编写一段代码,使其通过 Babel 进行编译为低版本的 ES 语法的代码,代码内容如下:

const numbers = [1, 2, 3, 4, 5];

// 测试 arrow function
const doubled = numbers.map((number) => number * 2);

console.log('Doubled numbers:', doubled);

const sum = numbers.reduce((total, number) => total + number, 0);

console.log('Sum of numbers:', sum);

const person = {
  firstName: 'John',
  lastName: 'Doe',
};

// 测试解构
const { firstName, lastName } = person;

console.log('First Name:', firstName);
console.log('Last Name:', lastName);

使用 Babel 对其进行编译,执行 Babel 的命令方式有很多种,第一种采用的是 npx 执行,或者使用 npm 全局安装 Babel 来执行对应的命令操作,如下:

# 执行翻译将结果输出到 dist 目录中
npx babel src --out-dir dist

上面这条命令执行的结果会保存在 dist 命令中,而原来采用的 ES6 特性编写的代码存储在 src 目录中,这样就使得开发过程中采用新的 ES6 新特性编写代码,而运行的代码是 ES5 版本,Babel 编译的结果文件会兼容更多浏览器版本,输出结果如下:

"use strict";

var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map(function (number) {
  return number * 2;
});
console.log('Doubled numbers:', doubled);
var sum = numbers.reduce(function (total, number) {
  return total + number;
}, 0);
console.log('Sum of numbers:', sum);
var person = {
  firstName: 'John',
  lastName: 'Doe'
};
var firstName = person.firstName,
  lastName = person.lastName;
console.log('First Name:', firstName);
console.log('Last Name:', lastName);

导致 Babel 这个项目诞生原因是因为我们电脑和手机上使用的浏览器是客户端软件,很多人在浏览器发布新的版本之后,也不一定会更新到最新的软件版本,这就会导致一个问题,部分用户的浏览器支持的 ES 版本较低,所以必须得有一个方法来解决这个问题。而 Babel 就是这样一款工具,上面 babel.config.json 配置文件中使用两个字段 presets 和 plugins ,但是 persets 对象还支持一个 targets 属性选项可以指定被编译器后的代码宿主环境浏览器版本环境要求信息,此属性转译输出针对特定的浏览器版本或环境进行优化的代码,如下配置:

{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "browsers": ["last 2 versions", "safari >= 7"]
      }
    }]
  ],
  "plugins": [
    "@babel/plugin-transform-arrow-functions",
  ]
}

上面这段配置要求浏览器最近的发布 2 个版本,并且 Safari 的版本要大或者等于 7 版本,这里的 browsers 属性的值是一个数组,可以是多个条件表达式,下面为具体支持表达式列表。

表达式描述
last n versions最近的 n 个版本
x%占有率超过 x% 的浏览器
not dead未停止维护的浏览器
IE x指定 IE 版本
last n major versions最近的 n 个主要版本

也可以精确指定浏览器的发行版本,要求被编译输出的代码必须能够兼容浏览器的版本,如下配置:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "58",
          "firefox": "52",
          "ie": "11"
        }
      }
    ]
  ]
}

在上面的例子使用的是 npx 的方式进行,在使用 npm 安装 babel 之后,babel 会有一个可执行程序被安装在 node_modules/.bin 目录中,采用 npx babel 命令相当于直接运行了 node_modules/.bin/babel 程序。另外一种是 babel 提供的 cli 工具 babel-cli 可以方便在系统全局命令行环境中执行 babel 程序,babel-cli 是 Babel 的命令行工具,可以通过全局安装 npm install -g babel-cli ,常用的命令如下:

# 将 src 下的源代码输出到 lib 目录中,也可以是 -d 参数
babel src -out-dir lib
# 转码结果输出到标准输出
babel example.js
# --out-file 或 -o 参数指定输出文件
babel example.js -out-file compiled.js
# 直接运行 ES6+ 代码,无需预先将代码转译为 ES5
babel-node

除此之外,Babel 还提供了基于元编程的方式进行使用,在 @babel/core 包提供了很多的 API 来协助开发人员来进行 AST 解析,使用这些 API 可以达到上面的命令行相同的功能,其实命令行的功能也是基于 @babel/core 包的 API 实现的。

问题在于 js 是一个动态依赖运行时语言,ts 解决了类型系统问题,但是最终还是得编译输出为 js 文件,即使使用 Babel 也一样,Babel 解决的是浏览器兼容问题,所以写 js 这类语言,是编译再编译,编译时间成本就在哪里。而且开发阶段的编译,最终输出还是 js 代码,只有运行时才 jit ,没有什么好的折中方案,初次版本架构设计带来的历史性问题,关于这些其实 JS 社区大佬也讨论过 “根本不需要TypeScript,JS+JSDoc够了”,大佬说我想多了 。目前 Node.js 原创始人正在开发一款新的项目 Deno 运行时,Deno 可以用于执行 JavaScript 和 TypeScript 代码,它使用的 JavaScript 引擎是 V8,与 Node.js 使用的引擎相同。然而 Deno 不仅限于 JavaScript 并且还支持 TypeScript,那么上面我所提出编译耗时问题可以放到 Deno 运行时在 JIT 阶段进行,意味着无需将 TypeScript 在编译为 JavaScript 文件,允许直接运行 TypeScript 代码文件,而无需事先将其编译成 JavaScript,这是 Deno 与 Node.js 的一个显著不同之处。


其他资料

便宜 VPS vultr
最后修改:2023 年 10 月 31 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !