首页 > 代码库 > JavaScript之:模块加载程序的历史与背景
JavaScript之:模块加载程序的历史与背景
- 原文:History and Background of JavaScript Module Loaders
- 作者:Elias Carlston
- 翻译:leotso
介绍
Web 应用程序的应用程序逻辑不断从后端移到浏览器端。但是,由于富客户端 JavaScript 应用程序的规模变得更大,它们遇到了类似于多年来传统应用所面临的挑战:共享代码以便重用,同时保持架构的隔离分层,并且足够灵活以便于轻松扩展。
这些挑战的一个解决方案是开发 JavaScript 模块和模块加载系统。这篇文章将着重于比较和对比过去 5 - 10 年的 JavaScript 模块加载系统。
这是一个综合性的主题,因为它跨越了开发和部署之间的交集。下面请看我们的表演:
- 对导致需要开发模块加载程序的问题的描述
- 快速复习一下模块定义格式
- JavaScript模块加载器综述-比较和对比
- 轻量级加载器(curl, LABjs, almond)
- RequireJS
- Browserify
- Webpack
- SystemJS
- 结论
面临的问题
如果您只有几个 JavaScript 模块,只需在页面中通过 <script> 标记来加载它们,这是一个很好的解决方案。
<head> <title>Wagon</title> <!-- cart requires axle --> <script src=“connectors/axle.js”></script> <script src=“frames/cart.js”></script> <!-- wagon-wheel depends on abstract-rolling-thing --> <script src=“rolling-things/abstract-rolling-thing.js”></script> <script src=“rolling-things/wheels/wagon-wheel.js”></script> <!-- our-wagon-init hooks up completed wheels to axle --> <script src=“vehicles/wagon/our-wagon-init.js”></script></head>
然而, <script> 建立了一个新的HTTP连接,对于小文件来说建立一个连接可能比传输文件本身花费更多的时间。脚本下载的过程会阻塞页面的渲染(某些时候会导致文档样式闪烁[Flash Of Unstyled Content])。而且,直到 IE8/FF3 有并行两个下载数的限制。
下载时间的问题很大程度上可以通过将一组简单的模块连接到一个文件中,以及代码压缩来解决。
<head> <title>Wagon</title> <script src=“build/wagon-bundle.js”></script></head>
然而,这种性能的提升是以牺牲灵活性为代价的。如果你的模块之间有相互依赖关系,那么这种灵活性的缺乏可能是一个值得注意的问题。假设你增加了一个 vehicle 类型:
<head> <title>Skateboard</title> <script src=“connectors/axle.js”></script> <script src=“frames/board.js”></script> <!-- skateboard-wheel and ball-bearing both depend on abstract-rolling-thing --> <script src=“rolling-things/abstract-rolling-thing.js”></script> <script src=“rolling-things/wheels/skateboard-wheel.js”></script> <!-- but if skateboard-wheel also depends on ball-bearing --> <!-- then having this script tag here could cause a problem --> <script src=“rolling-things/ball-bearing.js”></script> <!-- connect wheels to axle and axle to frame --> <script src=“vehicles/skateboard/our-sk8bd-init.js”></script></head>
取决于 skateboard-wheel.js 中构造函数的设计,代码可能会失败,因为 ball-bearing 的<script>标签没有介于 abstract-whell 和 skateboard-whell 之间。 因此,对于中等规模的项目的管理脚本排序变得冗长乏味,并且在一个足够大的项目(50+文件)中,就有可能建立一种依赖关系,对于那些不存在一个合理的顺序来满足所有依赖关系的情况。
模块化编程
模块化编程(Modular programming),我们在以前的文章中探讨过,恰好满足那些管理需求。不过不要高兴得太早,尽管我们拥有组织良好并且解耦的代码库(codebase),我们任然需要将它们传递(/交付)给用户才行。
Web的无状态和异步环境有利于用户体验,而不是程序员的便利。例如,用户可以在Web页面的所有图片下载完成之前就开始阅读。但是引导一个模块程序并不能如此宽容:模块的依赖项必须在加载之前可用。由于 http 无法保证取回文件需要多长时间,因此等待依赖项就会变得很棘手。
JavaScript模块格式以及加载器
amdjs-api Created by amdjsStar Houses the Asynchronous Module Definition API groups.google.com/group/amd-implement |
// myAMDModule.jsdefine([‘myDependencyStringName’, ‘jQuery’], function (myDepObj, $) { //...module code...})
// myCommonJSModule.jsvar myDepObj = require(‘myDependencyStringName’);var $ = require(‘jQuery’);if ($.version <= 1.6) alert(‘old JQ!’);
不适用模块加载程序的理由
轻量级模块加载程序
almond Created by requirejsStar A minimal AMD API implementation for use after optimized builds |
curl Created by cujojsStar curl.js is small, fast, extensible module loader that handles AMD, CommonJS Modules/1.1, CSS, HTML/text, and legacy scripts. github.com/cujojs/curl/wiki |
他们所做的事情是加载文件,按顺序,然后调用一个用户提供的回调函数。下面是来自LABjs github的一个例子:
<script src="http://www.mamicode.com/LAB.js"></script><script>$LAB .script("https://remote.tld/jquery.js").wait() .script("/local/plugin1.jquery.js") .script("/local/plugin2.jquery.js").wait() .script("/local/init.js").wait(function(){ initMyPage(); });</script>
RequireJS
<script src=“tools/require.js” data-main=“myAppInit.js” ></script>
要么在一个普通的JavaScript脚本中调用 require() 方法...
<script src=“tools/require.js”></script><script>require([‘myAppInit’, ‘libs/jQuery’], function (myApp, $) { //...});</script>
但是文档不建议使用这两种方法。后来,它揭示了原因是 data-main 和 require() 都不能保证 require.config 配置在它们执行之前完成。因此,内联 require 调用还是推荐嵌套在 configuration 调用中:
<script src=“tools/require.js”></script><script>require([‘scripts/config‘], function() { require([‘myAppInit’, ‘libs/jQuery’], function (myApp, $) { //... });});</script>
require([‘myAppInit.js’, ‘libs/jQuery’], function (myApp, $) { //...});
尽管它有各种特性,但它的能力和灵活性赢得了广泛的支持,它仍然是今天最受欢迎的加载程序之一。
Borwserify
node-browserify Created by substackStar browser-side require() the node.js way browserify.org |
npm install -g –save-dev browserify
browserify entry-point.js -o bundle-name.js
Browserify 递归地查找 entry-point 中的所有依赖项并将它们组装到单独的文件中:
<script src=”bundle-name.js”></script>
根据服务器端的模式,Browserify 确实需要一些方法的改变。使用AMD,你可能压缩并合并“核心”代码,并且允许加载可选的模块。使用 Browserify,所有的模块必须被捆绑打包;但是,指定一个入口点允许基于相关的功能块来组织包,这对于带宽关注和模块化编程来说都是有意义的。
在2011年推出,Browserify 变得更加强大。
Webpack
webpack Created by webpackStar A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows to load parts for the application on demand. Through "loaders," modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff. webpack.js.org |
Webpack运行在一个名为“loaders”的概念上,它是注册用来处理文件类型的插件。例如, 一个 loader 可以处理 ES6 的转译(Webpack 2.0 handles ES6 natively),或者 SCSS 的编译。
Loaders将数据输入到一个“chunk”,它从一个入口点开始——概念上类似于一个Browserify包。一旦建立了Webpack,当资产发生变化时,chunk会自动重新生成。这个功能非常强大,您不必记着去编辑chunk。
让每个人都很兴奋的特性是热模块替换。一旦Webpack负责你的chunk,当你运行webpack-dev-server时,如果你改变了源代码,它就知道应该要修改浏览器中的代码了。与其他的源代码监控类似,webpack-dev-server不要求浏览器重新加载,因此它属于生产率工具的范畴,可以在开发过程中节约时间。
基本用法很简单,webpack安装方式类似于Browserify:
npm install -g –save-dev webpack
给webpack命令传递一个入口点和一个输出文件:
webpack ./entry-point.js bundle-name.js
如果你限制使用Webpack令人印象深刻的默认设置,不过这种能力总是要付出代价的。在一个项目中,我们组面临几个疑难问题 – 经过Webpack编译的ES6代码不工作,SCSS 能在本地工作但是无法编译到云端。此外,Webpack 的 loader 插件语法重载了require()方法的参数列表, 因此,在没有修改的情况下,它不会在Webpack之外工作(这意味着你无法在客户端和服务器端共享代码)。
Webpack把它的目标放在下一代的网络编译器上,但是可能得等待下一个版本。
Google Trends’ Take - Source
SystemJS
Wikipedia 将 polyfill 定义为 “additional code which provides facilities that are not built into a web browser”,但是 SystemJS 将 ES6 Module Loader Polyfill 拓展到了浏览器之外。 一个很好的例子说明了现代Javascript是如何在环境中运行的,ES6 Module Loader Polyfill 也可以通过 npm 在 Node 环境中使用。
systemjs Created by systemjsStar Dynamic ES module loader |
SystemJS 可以被认为是 ES6 Module Loader Polyfill 的浏览器接口。它的实现方式类似于 RequireJS:通过<script>标签来引入SystemJS,在配饰对象上设置选项,然后调用 System.import()来加载模块:
<script src="system.js"></script><script>// set our baseURL reference pathSystem.config({ baseURL: ‘/app‘});// loads /app/main.jsSystem.import(‘main.js‘);</script>
SystemJS 是 Angular 2 的推荐模块加载程序,所以它已经获得了社区支持。和 Webpack 一样,它通过加载器插件支持非JavaScript文件类型。和 Require 一样,SystemJS 也提供了一个简单工具,systemjs-builder,来打包和优化你的文件。
然而,与SystemJS相关的最强大的组件是JSPM(or JavaScript Package Manager)。其建立在 ES6 Module Loader Polyfill、npm(the Node package manager)之上,JSPM 承诺使同构的Javascript成为现实。对JSPM的完整描述超出了本文的范围,但是在 jspm.io 上有大量的文档,和许多 how-to 文章。
Loader Category | Local module format | Server files | Server module format | Loader code |
Tiny loaders | Vanilla JS | Same structure as local files | Same format as local files | curl(entryPoint.js’) |
RequireJS | AMD | Concatenated and minified | AMD | requirejs(‘entryPoint.js’, function (eP) {// startup code}); |
Browserify | CommonJS | Concatenated and minified | CommonJSinside AMDwrapper | <script src=http://www.mamicode.com/”browserifyBundle.js”></script> |
Webpack | AMD and/or CommonJs (mixed OK) | “Chunked” – Concat and minify into feature groups | Webpack proprietary wrapper | <script src=http://www.mamicode.com/”webpackChunk.js”></script> |
SystemJS | Vanilla, AMD, CommonJS, or ES6 | same as local | SystemJS proprietary wrapper | System.import(‘entryPoint.js’).then(function (eP) {// startup code}); |
总结
与几年前相比,今天过多的模块加载器构成了一种选择过多的尴尬。 希望这篇文章能帮助您理解模块加载器的存在以及主要的不同之处。
在为您的下一个项目选择模块加载程序时,小心落入分析瘫痪的陷阱。 首先尝试最简单的解决方案:完全跳过一个加载程序并坚持使用简单的旧脚本标签是没有问题的。如果你真的需要一种模块加载程序,那么 RequireJS + Almond 是一种可靠的、高性能的、支持良好的选择。如果你需要 CommonJS 支持那么选择 Browersify。只有当你遇到用其他模块加载器完全无法解决的问题时才升级到 SystemJS 或 Webpack。这些前沿系统的文档仍然缺乏。所以,用你使用合适的模块加载程序节省下来的时间提供一些酷炫的特性吧!
JavaScript之:模块加载程序的历史与背景