webpack - 前端模块化开发神器

webpack 是新兴的 web 前端开发工具,他让开发者可以模块化的方式开发 web 前端应用,

在模块化的 web 前端开发领域,还有两个知名工具 requirejs 和 browserify,

  • requirejs 是 2011 年发起的老牌项目,曾经风靡一时,目前还有数量可观的使用者,但其使用和调试并不方便
  • browserify 小 requirejs 一岁,完全离线的模块化方案,仅支持 CommonJS 模块规范
  • webpack 是后起之秀,简单的说,他综合了前二者的优点,且弥补了他们的缺点

事实上还有许多三方解决方案可以用于 web 模块化开发,以下文字翻译自 webpack 文档:

Webpack motivation ,详述了 webpack 项目的发起动机:

今天众多网站已进化到 web app,其共同特点有:

  • 单页面上越来越多的 javascript;
  • 在现代浏览器中可以做更多事情;
  • 整页 reload 越来越少 - 单页上的代码越来越多

结果是客户端会有 许多 代码

大量的代码需要组织。模块系统允许你将代码库拆分成模块

模块系统的风格

存在多种标准用于定义依赖项和模块导出值:

  • <script> 标签风格(无模块系统)
  • CommonJs
  • AMD 及其方言
  • ES6 模块
  • 不胜枚举 …

<script> 标签

这是一种不用模块系统处理模块化代码库的方法

1
2
3
4
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="libaryA.js"></script>
<script src="module3.js"></script>

模块导出接口到全局对象 - 例如:window 对象。模块可以通过全局对象访问依赖项的接口。

缺点

  • 在全局对象中出现(定义)冲突
  • loading 顺序很重要
  • 开发者必须决定 模块/库 依赖
  • 在大项目中,依赖列表变得很长,难于管理

CommonJs / 同步

这种风格使用同步的 require 方法加载依赖项并返回其导出的接口。一个模块如果想导出什么,只需往 exports 对象添加属性或者设置 module.exports 的值即可。

1
2
3
4
require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;

它被 node.js 用于服务器端。

优点

  • 服务器端模块可以被重用
  • 有许多模块已经是这种风格 (npm)
  • 使用起来简单方便

缺点

  • 阻塞调用在网络上并不好用。网络请求都是异步的。
  • 多个模块没有并行请求

实现

AMD / 异步

异步模块定义

其他模块系统(浏览器)和同步版本的 require(CommonJs)放在一起时出现了问题,因此引入其异步版本(以及相应的定义模块和导出值的方法)

1
2
3
4
require(["module", "../file"], function(module, file) { /* ... */ });
define("mymodule", ["dep1", "dep2"], function(d1, d2) {
return someExportedValue;
});

优点

  • 适合网络中的异步请求风格
  • 并行加载多个模块

缺点

  • 代码开销。更难阅读和书写
  • 像是某种权宜之计(言下之意:并非良策)

实现

更多关于 CommonJs and AMD 的资料.

ES6 模块

EcmaScript6 为 javascript 添加了一些语言结构, 这形成了另一种模块系统

1
2
3
import "jquery";
export function doStuff() {}
module "localModule" {}

优点

  • 静态分析变容易了
  • 不会过时的 ES 标准

缺点

  • 本地浏览器支持还需要时间
  • 很少有这种风格的(现成)模块

不偏不倚的解决方案

让开发者选择模块风格,让现有代码可以工作。添加自定义模块风格也很容易


传输

模块应在客户端执行,因此他们必须从服务器传输到浏览器

关于如何传输模块,有两种极端:

  • 每个模块一个请求
  • 一个请求传输所有模块

两种方法都使用广泛,但他们都不是最优的:

  • 每个模块一个请求
    • 优点: 只传输被请求的模块
    • 缺点: 许多请求意味着开销
    • 缺点: 拖慢应用的启动, 因为请求会延迟
  • 一个请求传输所有模块
    • 优点: 更少的请求开销, 更少的延迟
    • 缺点: 未请求的模块也被传输了

分块传输

有种更灵活的传输方案可能更好,多数情形下在极端情况之间妥协是最好的选择。

→ 当编译所有模块时:模块集分成更小的批次(块)

我们得到多个更小的请求。模块中的分块初始时不需要,他们只在需要时被请求。初始请求不包含完整代码并且更小。

“拆分点” 由开发者决定并且是可选的

→ 单一的大代码库也可以

注释: 此创意 来自 Google 的 GWT.

更多细节参见 Code Splitting.


为什么只有 JavaScript?

一个模块系统为什么只应帮助 JavaScript ?还有许多其他静态资源需要处理:

  • 样式表
  • 图片
  • web 字体
  • html 模板

并且:

  • coffeescript → javascript
  • less stylesheets → css stylesheets
  • jade templates → javascript which generates html
  • i18n files → something

使用起来应尽量简单:

1
require("./style.css");
1
2
3
require("./style.less");
require("./template.jade");
require("./image.png");

更多细节参见 使用加载器加载器.


静态分析

当编译所有模块时,静态分析算法会尝试查找依赖项。

过去,require 不支持表达式,但是例如:require("./template/" + templateName + ".jade") 是很普通的结构

许多库都各自捣鼓一套风格。好多写法看上去非常怪异 …

策略

一个聪明的解析器会允许更多的现有代码顺畅运行。如果开发者做了些古怪的事,那么他也要为此寻找更多兼容的解决方案。