[JS] 最佳实践之部署
摘记<Javascript高级程序设计> -- 马特 2020版 第28章 28-3
部署
任何 JavaScript 解决方案最重要的部分可能就是把网站或 Web 应用程序部署到线上环境了。在此之前我们已完成了很多工作,包括架构方面和优化方面的。现在到了把代码移出开发环境,发布到网上,让用户去使用它的时候了。不过,在发布之前,还需要解决一些问题。
构建流程
准备发布 JavaScript 代码时最重要一环是准备构建流程。开发软件的典型模式是编码、编译和测试。换句话说,首先要写代码,然后编译,之后运行并确保它能够正常工作。但因为 JavaScript 不是编译型语言,所以这个流程经常会变成编码、测试。你写的代码跟在浏览器中测试的代码一样。这种方式的问题在于代码并不是最优的。你写的代码不应该不做任何处理就直接交给浏览器,原因如下。
- 知识产权问题:
如果把满是注释的代码放到网上,其他人就很容易了解你在做什么,重用它,并可能发现安全漏洞。
- 文件大小:
你写的代码可读性很好,容易维护,但性能不好。浏览器不会因为代码中多余的空格、缩进、冗余的函数和变量名而受益。
- 代码组织:
为保证可维护性而组织的代码不一定适合直接交付给浏览器。
为此,需要为 JavaScript 文件建立构建流程。
1. 文件结构
构建流程首先定义在源代码控制中存储文件的逻辑结构。注意,把代码分散到多个文件是从可维护性而不是部署角度出发的。对于部署,应该把所有源文件合并为一个或多个汇总文件。Web 应用程序使用的 JavaScript 文件越少越好,因为 HTTP 请求对某些 Web应用程序而言是主要的性能瓶颈。而且,使用<script>
标签包含 JavaScript 是阻塞性操作,这导致代码下载和执行期间停止所有其他下载任务。因此,要尽量以符合逻辑的方式把 JavaScript 代码组织到部署文件中。
2. 任务运行器
如果要把大量文件组合成一个应用程序,很可能需要任务运行器自动完成一些任务。任务运行器可以完成代码检查、打包、转译、启动本地服务器、部署,以及其他可以脚本化的任务。很多时候,任务运行器要通过命令行界面来执行操作。因此你的任务运行器可能仅仅是一个辅助组织和排序复杂命令行调用的工具。从这个意义上说,任务运行器在很多方面非常像.bashrc 文件。其他情况下,要在自动化任务中使用的工具可能是一个兼容的插件。
如果你使用 Node.js 和 npm 打印 JavaScript 资源,Grunt 和 Gulp 是两个主流的任务运行器。它们非常稳健,其任务和指令都是通过配置文件,以纯 JavaScript 形式指定的。使用 Grunt 和 Gulp 的好处是它们分别有各自的插件生态,因此可以直接使用 npm 包。关于这两个工具插件的详细信息可以参考本书附录。
3. 摇树优化
摇树优化(tree shaking)是非常常见且极为有效的减少冗余代码的策略。使用静态模块声明风格意味着构建工具可以确定代码各部分之间的依赖关系。更重要的是,摇树优化还能确定代码中的哪些内容是完全不需要的。
实现了摇树优化策略的构建工具能够分析出选择性导入的代码,其余模块文件中的代码可以在最终打包得到的文件中完全省略。假设下面是个示例应用程序:
import { foo } from './utils.js';
console.log(foo);
export const foo = 'foo';
export const bar = 'bar'; // unused
这里导出的 bar 就没有被用上,而构建工具可以很容易发现这种情况。在执行摇树优化时,构建工具会将 bar 导出完全排除在打包文件之外。静态分析也意味着构建工具可以确定未使用的依赖,同样也会排除掉。通过摇树优化,最终打包得到的文件可以瘦身很多
4. 模块打包器
以模块形式编写代码,并不意味着必须以模块形式交付代码。通常,由大量模块组成的 JavaScript代码在构建时需要打包到一起,然后只交付一个或少数几个 JavaScript 文件。模块打包器的工作是识别应用程序中涉及的 JavaScript 依赖关系,将它们组合成一个大文件,完成对模块的串行组织和拼接,然后生成最终提供给浏览器的输出文件。能够实现模块打包的工具非常多。Webpack、Rollupt 和 Browserify 只是其中的几个,可以将基于模块的代码转换为普遍兼容的网页脚本。
验证
即使已出现了能够理解和支持 JavaScript 的 IDE,大多数开发者仍通过在浏览器中运行代码来验证自己的语法。这种方式有很多问题。首先,如此验证不容易自动化,也不方便从一个系统移植到另一个系统。其次,除了语法错误,只有运行的代码才可能报错,没有运行到的代码则无法验证。有一些工具可以帮我们发现 JavaScript 代码中潜在的问题,最流行的是 Douglas Crockford 的 JSLint 和 ESLint。
压缩
谈到 JavaScript 文件压缩,实际上主要是两件事:代码大小(code size)
和传输负载(wire weight)
代码大小指的是浏览器需要解析的字节数,而传输负载是服务器实际发送给浏览器的字节数。在 Web开发的早期阶段,这两个数值几乎相等,服务器发送给浏览器的是未经修改的源文件。而今天,这两个数值不可能相等,实际上也不应该相等。
1. 代码压缩
JavaScript 不是编译成字节码,而是作为源代码传输的,所以源代码文件通常包含对浏览器的JavaScript 解释器没有用的额外信息和格式。JavaScript 压缩工具可以把源代码文件中的这些信息删除,并在保证程序逻辑不变的前提下缩小文件大小。
注释、额外的空格、长变量或函数名都能提升开发者的可读性,但对浏览器而言这些都是多余的字节。压缩工具可以通过如下操作减少代码大小:
- 删除空格(包括换行);
- 删除注释;
- 缩短变量名、函数名和其他标识符。
所有 JavaScript 文件都应该在部署到线上环境前进行压缩。在构建流程中加入这个环节压缩JavaScript 文件是很容易的。
2. JavaScript 编译
类似于最小化,JavaScript 代码编译通常指的是把源代码转换为一种逻辑相同但字节更少的形式。与最小化的不同之处在于,编译后代码的结构可能不同,但仍然具备与原始代码相同的行为。编译器通过输入全部 JavaScript 代码可以对程序流执行稳健的分析。
编译可能会执行如下操作:
- 删除未使用的代码;
- 将某些代码转换为更简洁的语法;
- 全局函数调用、常量和变量行内化。
3. JavaScript 转译
我们提交到项目仓库中的代码与浏览器中运行的代码不一样。ES6、ES7 和 ES8 都为 ECMAScript规范扩充增加了更好用的特性,但不同浏览器支持这些规范的步调并不一致。
通过 JavaScript 转译,可以在开发时使用最新的语法特性而不用担心浏览器的兼容性问题。转译可以将现代的代码转换成更早的 ECMAScript 版本,通常是 ES3 或 ES5,具体取决于你的需求。这样可以确保代码能够跨浏览器兼容。
4. HTTP 压缩
传输负载是从服务器发送给浏览器的实际字节数。这个字节数不一定与代码大小相同,因为服务器和浏览器都具有压缩能力。所有当前主流的浏览器(IE/Edge、Firefox、Safari、Chrome 和 Opera)都支持客户端解压缩收到的资源。服务器则可以根据浏览器通过请求头部(Accept-Encoding)标明自己支持的格式,选择一种用来压缩 JavaScript 文件。在传输压缩后的文件时,服务器响应的头部会有字段(Content-Encoding)标明使用了哪种压缩格式。浏览器看到这个头部字段后,就会根据这个压缩格式进行解压缩。结果是通过网络传输的字节数明显小于原始代码大小。
例如,使用 Apache 服务器上的两个模块(mod_gzip 和 mod_deflate)可以减少原始 JavaScript文件的约 70%。这很大程度上是因为 JavaScript 的代码是纯文件,所以压缩率非常高。减少通过网络传输的数据量意味着浏览器能更快收到数据。注意,服务器压缩和浏览器解压缩都需要时间。不过相比于通过传入更少的字节数而节省的时间,整体时间应该是减少的。