我们为什么需要Webpack?

现在的网站都在演变成为Web Apps:

  • 页面上的JavaScript越来越多。
  • 在现代浏览器上用户可以做更多的事情了。
  • 整个页面重新加载的情况更少了,与此同时,页面上的代码量更大了。

结果就是:客户端的代码量变得越来越庞大,庞大的代码量意味着我们需要适当地组织代码,而模块系统则提供了把代码分割成不同模块的功能。

模块系统实现的演变

关于怎么定义依赖和导出接口有多种标准:

  • Script标签形式
  • CommonJS
  • AMD和一些变种实现
  • ES6模块
  • 其它

script标签形式

如果不对代码进行模块化,那么通常的做法就是使用标签引入script文件:

    <script src="src=module1.js"></script>
    <script src="src=module2.js"></script>
    <script src="librariesA.js"></script>
    <script src="src=module3.js"></script>

模块把接口导出到全局对象上,比方说window对象上。也就是说模块可以在全局对象上获取到依赖的接口。

这样做的问题是:

  • 全局对象上可能会出现命名冲突
  • 加载的顺序很重要
  • 程序员必须要手动处理模块/库的依赖问题
  • 在大型项目里面,这个列表可能会很长、很难管理

CommonJS:同步的require

CommonJS采用同步的require方法来加载依赖并返回导出的接口。一个模块可以通过往exports对象上添加属性或者设置module.exports的值来确定导出哪些接口。

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

后端的nodejs用的就是这个方法。

优点:

  • 服务端的模块可以被复用
  • 已经有很多模块供使用了(npm)
  • 很容易上手使用

缺点:

  • 阻塞式的调用不能适用于网络请求,因为网络请求是异步的
  • 无法同步require多个模块

使用CommonJS的项目:

  • Nodejs -服务端
  • Browserify
  • Module-webmake -编译成一个bundle
  • Wreq -服务端

AMD:异步require

之前提到的模块系统只能同步require,而AMD则采用异步的实现形式。

    require(["module","../file.js"],function(module,file){/*...*/});
    define("mymodule",["dep1","dep2"],function(d1,d2){
        return someExportValue;
    });

优点:

  • 能满足网络请求的异步需求
  • 能同步加载多个模块

缺点:

  • 代码复杂,更难写也更难读
  • 似乎是一种绕路的笨办法

使用AMD的项目:

  • Require.js -客户端
  • Curl -客户端

ES6模块

EcmaScript6给JavaScript加了一些语言特性,其中就包括了模块系统。

    import "jQuery";
    export function doStuff(){}
    module "localModule"{}

优点:

  • 静态分析变得更简单了
  • 作为ES标准,这个实现未来肯定会成为主流

缺点:

  • 浏览器原生支持还需要时间
  • 目前只有很少的一些模块可用

最合适(unbiased)的解决办法

让开发人员选择合适的模块系统,让代码能跑起来,也能使得添加自定义模块更简单易行。

模块的传输

模块需要在客户端执行,因此它们需要从服务器传输到浏览器上。

有两个极端的方法来传输模块:

  • 每传输一个模块发起一个请求
  • 所有的模块都放在一个请求里

这两种方法都有人在用,但是都不是最优解:

  • 每传输一个模块发起一个请求
    • 优点:只会请求目前需要的模块
    • 缺点:很多请求意味着会有很多额外的消耗
    • 缺点:请求延迟会使得程序启动变慢
  • 所有的模块放在一个请求里
    • 优点:更少的请求意味着更低的延迟
    • 缺点:无论目前需要与否,所有的模块都一次性传输过来了

分块传输

我们需要一个更灵活的传输方式,在大多数情况下,如果能在极端中间找到一个平衡会是最好的选择。比方说,在编译所有的模块的时候可以把模块分割成很多小模块。这样一来,我们就可以把请求分成很多小请求,而分割后的模块只有在需要的时候才会被请求。所以初始的请求不会包含所有的代码,从而减小传输压力。至于代码怎么分割由程序员决定。

这个想法来源于Google's GWT. 更多的信息在code splitting.

为什么只处理JavaScript?

为什么模块系统只能处理JavaScript?还有很多其他的静态资源需要处理:

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

还有:

  • coffeescript ->javascript
  • less 样式表 ->css 样式表
  • jade 模板 ->javascript which generates html
  • i18n files -> something
  • 等等

这些都应该很容易处理才对:

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

这些都可以做到,更多相关的信息在Using loader和loader章节。

静态分析

在编译所有的模块的时候,静态分析会试图去找到依赖。通常来说在没有表达式的情况下只能找到一些简单的东西,但是像

require("./template/" + templateName + ".jade")

这样的表示方法是很常见的形式。 不同的库会用不同的形式表示,有一些看起来很奇怪...

分析策略

一个智能的解析器会使得大多数代码能跑起来。即便程序员写了些很奇怪的代码,它还是能找到最兼容的办法。