JavaScript之:模块加载程序的历史与背景

介绍

Web 应用程序的应用程序逻辑不断从后端移到浏览器端。但是,由于富客户端 JavaScript 应用程序的规模变得更大,它们遇到了类似于多年来传统应用所面临的挑战:共享代码以便重用,同时保持架构的隔离分层,并且足够灵活以便于轻松扩展。

这些挑战的一个解决方案是开发 JavaScript 模块和模块加载系统。这篇文章将着重于比较和对比过去 5 - 10 年的 JavaScript 模块加载系统。

这是一个综合性的主题,因为它跨越了开发和部署之间的交集。下面请看我们的表演:

  1. 对导致需要开发模块加载程序的问题的描述
  2. 快速复习一下模块定义格式
  3. JavaScript模块加载器综述-比较和对比
    1. 轻量级加载器(curl, LABjs, almond)
    2. RequireJS
    3. Browserify
    4. Webpack
    5. SystemJS
  4. 结论

面临的问题

如果您只有几个 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模块格式以及加载器

Asynchronous Module Definition(AMD)API 的出现解决了异步问题。 AMD 利用了 JavaScript 处理过程中的两个阶段:解析(parsing/ interpretation,when the code is checked for syntax)和执行(execution,when the interpreted code is run)。


amdjs-api

Created by amdjsStar

Houses the Asynchronous Module Definition API

groups.google.com/group/amd-implement

在语法上,依赖关系通过一组字符串数组声明。由模块加载器来检查是否已经加载了依赖项,如果没有,则执行异步加载(fetch)。只有当所有的依赖项都可用(递归地包括所有依赖项)时,加载程序才会执行有效负载的函数部分, 将已初始化的依赖对象作为参数传递给有效负载函数。

// myAMDModule.js
define([‘myDependencyStringName’, ‘jQuery’], function (myDepObj, $) {
  //...module code...
})

此技术解决了文件排序的问题。你又可以把所有你的模块文件打包到一个大文件中(以任何顺序),加载程序会处理好它们之间的依赖。但它也提供了其他优势:突然间,使用了像jQuery和Bootstrap这样的公共库的公共托管版本,突然之间产生了网络效应。例如,如果用户在他们的机器上已经有了谷歌的CDN版本的jQuery,那么这个加载器就不需要去取它了。

最终,AMD的“杀手级应用”甚至都不是一个产品特性。使用模块加载器的最佳和最意想不到好处是在开发期间引用单个模块文件,然后无缝地过渡到生产中环境中的单个、连接和压缩的文件。

在服务器端,http 获取很少,因为大多数文件已经存在于本地机器上。CommonJS格式使用这种假设来驱动同步模型。与 cjs 兼容的加载程序将使可用的函数称为 require(),它可以从任何普通的 JavaScript 中调用来加载模块。

// myCommonJSModule.js
var myDepObj = require(‘myDependencyStringName’);
var $ = require(‘jQuery’);
if ($.version <= 1.6) alert(‘old JQ!’);

CJS去除了AMD中繁琐的 define()语法,使用Python等语言的后端程序员通常对CJS模式更为适应。CJS也可以更容易地执行静态分析。例如,在上面的例子中,分析器可以推断出从 require(‘jQuery‘) 返回的对象应该有一个名为“version”的属性。IDEs 可以将此用于重构和自动完成等有用的特性。

由于 require() 是一个阻塞函数,它会导致JavaScript解释器暂停当前代码并切换执行上下文到请求目标。在上面的示例中,控制台日志将不会执行直到 myDependencyStringName.js 中的代码完成加载和执行。

在浏览器中,当文件被处理时,连续下载每个依赖关系将导致一个小程序的加载时间无法接受。但这并不意味着没有人可以在浏览器中使用CJS。这个技巧来自于在构建时进行递归分析 —— 当文件必须被压缩和连接的时候,分析器可以遍历抽象语法树(Abstract Syntax Tree)来处理所有的依赖关系,并确保所有的东西都被绑定到最终文件中。

最后,ES6,多年来对JavaScript最重要的更新,增加了新的关键字“module”来提供对模块的内建支持。ES6 Module 包含了许多从 AMD 和 CJS 中获得的经验教训,但更类似于CJS,特别是在加载方面。

不适用模块加载程序的理由

目前,模块化编程和模块加载器已经成为富Web应用程序的同义词。但是使用模块化编程并非必须要使用模块加载器。以我的经验,只有复杂模块的相互依赖问题才能成为绝对需要一个模块加载器的条件,然而许多项目都有许多他们并不需要的复杂的加载基础设施。

在你的技术栈中增加任何新技术都是有开销的:它即增加了可能出错的事情的数量,又增加了你需要理解的东西。加载器的许多好处仅仅是好处而不是需求。要注意那些听起来很容易的好处 —— 你不需要它,犹如这是一种微妙的过早的优化

首先,在没有模块加载程序的情况下运行您的项目。您将对您的代码有更大的控制和洞察。如果发现你永远都不需要,就这样,以后有需要再加一个并不难。

同样的“你不需要他”(YAGNI)的逻辑适用于你选择的任何模块加载器的特性。我见过许多项目使用AMD的模块,但没有任何好处(而且这方面也有很大的成本)。KISS(Keep It Simple and Stupid)。

轻量级模块加载程序

早期,随着 AMD 成为领先的客户端模块格式,模块加载生态系统纷纷开始支持它。在这波浪潮中包括 LAB.js, curljs 和 Almond。每一个都有不同的方式,但有很多共同之处:它们很小(1-4kb),而且遵循 Unix 哲学——做一件事情,并且把它做好。


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="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>

在这个例子中,LAB 开始获取jQuery,等到加载并执行完成再加载plugin1和plugin2,等到完成再加载init.js。最终,当init.js加载完成,回调函数中调用initMyPage方法。

所有这些加载器都使用相同的技术机制来获取内容:它们在页面的DOM中使用src属性以动态的方式写一个<script>标签。当脚本触发onReadyStateChange事件时,加载程序知道内容已经准备好执行。

LAB和curl不再被积极维护,但是它们非常简单,在今天的浏览器中可能仍然有效。Almond仍然以满足需求简约版本维护者。

RequireJS

RequireJS 出现于2009年,轻量级加载器的后来者,但由于其先进的特性,它获得了最大的吸引力。

在核心层面,RequireJS 与其他轻量级加载器没有本质的不同。它将<script>标签写入DOM,侦听完成事件,然后递归地从结果中加载依赖项。令RequireJS与众不同的是它的扩展性 ———— 一些人可能说令人困惑 ———— 一组配置选项和操作语法糖。例如,有两种方式启动加载过程:要么将加载 RequireJS 脚本的<script>标签的名为data-main的属性指向一个init脚本文件…

<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 是配置选项中的一把瑞士军刀,但是,一种无意识的不确定性笼罩在他们相互影响的多种使用方式上。例如:如果设置了 baseUrl 配置选项,它将提供一个用于搜索文件的位置的前缀。这是明智的,但是,如果 baseUrl 没有指定,默认值将是加载 require.js 的 HTML 页面的位置,除非你使用 data-main,在这种情况下该路径变为 baseUrl!Maps, shims, paths 和 path fallback configs 提供更多解决复杂问题的机会,同时引入不相关的问题。

值得一提的是它的约定中的“gotcha”,即“模块ID”的概念。跟随Node中的约定,Require 期待你去掉依赖声明中的‘.js‘扩展名。如果 Require 发现一个模块ID 以‘.js‘结尾,或者以一个下线或者http协议开头,它切换出模块ID模式,并将字符串值视为文字路径。

如果我们如下修改上面的例子:

require([‘myAppInit.js’, ‘libs/jQuery’], function (myApp, $) {
  //...
});

Require 将几乎肯定无法找到myAppInit,除非它恰好在 baseUrl/data-main 算法会返回的目录中。由于习惯性地输入‘.js‘的肌肉记忆,这个错误会很烦人,除非你有避免它的习惯。

尽管它有各种特性,但它的能力和灵活性赢得了广泛的支持,它仍然是今天最受欢迎的加载程序之一。

Borwserify


node-browserify

Created by substackStar

browser-side require() the node.js way browserify.org

Browserify 允许在浏览器中使用 CommonJS 格式化的模块。因此,Browserify 相比模块加载器更像一个模块打包工具: Browserify完全是一个构建时工具,生成一组可以加载到客户端的代码。

从一台安装了node和npm的构建机器开始,获取如下包:

npm install -g –save-dev browserify

用 CommonJS 格式编写你自己的模块,并且通过如下命令打包捆绑:

browserify entry-point.js -o bundle-name.js

Browserify 递归地查找 entry-point 中的所有依赖项并将它们组装到单独的文件中:

<script src=”bundle-name.js”></script>

根据服务器端的模式,Browserify 确实需要一些方法的改变。使用AMD,你可能压缩并合并“核心”代码,并且允许加载可选的模块。使用 Browserify,所有的模块必须被捆绑打包;但是,指定一个入口点允许基于相关的功能块来组织包,这对于带宽关注和模块化编程来说都是有意义的。

在2011年推出,Browserify 变得更加强大。

Webpack

Webpack 跟随 Browserify 的思想是一个模块打包机, 但是添加了足够的功能来替换您的构建系统。 除了CJS之外,Webpack不仅支持AMD和ES6格式,而且支持非脚本资产,如样式表和HTML模板。


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 path
System.config({
  baseURL: ‘/app‘
});
// loads /app/main.js
System.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 文章。

Comparison Table


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=”browserifyBundle.js”></script>

Webpack

AMD and/or CommonJs (mixed OK)

“Chunked” – Concat and minify into feature groups

Webpack proprietary wrapper

<script src=”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。这些前沿系统的文档仍然缺乏。所以,用你使用合适的模块加载程序节省下来的时间提供一些酷炫的特性吧!

时间: 2024-10-25 16:44:34

JavaScript之:模块加载程序的历史与背景的相关文章

JavaScript AMD 模块加载器原理与实现

关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者遵循 CMD规范.前者的规范产出比较适合于浏览器异步环境的习惯,后者的规范产出对于写过 nodejs 的同学来说是比较爽的.关于两者的比较,有兴趣的同学请参看玉伯在知乎的回答 AMD和CMD的区别有哪些.本文希望能按照 AMD 规范来简单实现自己的一个模块加载器,以此来搞清楚模块加载器的工作原理.

javascript 异步模块加载 简易实现

在javascript是没有类似java或其他语言的模块概念的,因此也不可能通过import或using等关键字来引用模块,这样造成了复杂项目中前端代码混乱,变量互相影响等. 因此在复杂项目中引入AMD的概念,AMD:全称是Asynchronous Module Definition,即异步模块加载机制.通过AMD可以不需要在页面中手动添加<script>来引用脚本,而通过定义依赖自动加载模块脚本,接下来的代码将讲解如何实现建议的AMD模块,如果需要查看比较详细的实现可以下载requirejs

关于javascript模块加载技术的一些思考

前不久有个网友问我在前端使用requireJs和seajs的问题,我当时问他你们公司以前有没有自己编写的javascript库,或者javascript框架,他的回答是什么都没有,他只是听说像requirejs和seajs是新东西新技术,很有价值所以想用它. 这位网友的问题引起了我对javascript模块加载技术的思考,上篇文章我给出了自己写的一个javascript库的基本结构,其实写这篇文章的一个起因就是因为我想使用requirejs或者seajs这样的技术来重新设计我写javascrip

JavaScript模块加载框架sea.js 学习一

简单总结sea.js 学习 文件目录结构 /sea/sea.js      下载地址  http://seajs.org/docs/#downloads /sea/jquery-sea.js   下载地址 http://jquery.com/download/ /sea/sea_config.js /sea/home.jsdata.js /sea/data.js 1.html页面代码文件 <style> .ch{height:200px;width:200px;background:#ccc;

seajs实现JavaScript 的 模块开发及按模块加载

seajs实现了JavaScript 的 模块开发及按模块加载.用来解决繁琐的js命名冲突,文件依赖等问题,其主要目的是令JavaScript开发模块化并可以轻松愉悦进行加载. 官方文档:http://seajs.org/docs/#docs 首先看看seajs是怎么进行模块开发的.使用seajs基本上只有一个函数"define" fn.define = function(id, deps, factory) { //code of function- } 使用define函数来进行定

解密javascript模块加载器require.js

require.config require.config设置require.js模板加载选项 // 定义config req.config = function (config) { return req(config); }; // 加载config配置项 req = requirejs = function (deps, callback, errback, optional) { var context, config, contextName = defContextName; //

【模块化编程】理解requireJS-实现一个简单的模块加载器

在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ...... 为了深入了解加载器,中间阅读过一点requireJS的源码,但对于很多同学来说,对加载器的实现依旧不太清楚 事实上不通过代码实现,单单凭阅读想理解一个库或者框架只能达到一知半解的地步,所以今天便来实现一个简单的加载器 加载器原理分析 分与合 事实上,一个程序运行需要完整的模块,以下代码为例: 1

felayman---nodejs的几种模块加载方式

nodejs的几种模块加载方式 一.直接在exports对象中添加方法 1.首先创建一个模块(module.js)module.js exports.One = function(){ console.log('first module'); }; 2.load.js var module =require('./module'); module.One(); 这样我们就可以在引入了该模块后,返回一个exports对象,这里是指module对象,其实都只是两个引用或者句柄,只是都指向了同一个资源

Node.js【6】Web开发、进阶(模块加载、控制流、部署、弊端)

笔记来自<Node.js开发指南>BYVoid编著 实现过程:https://github.com/ichenxiaodao/express-example 第5章 使用Node.js进行Web开发 从零开始用Node.js实现一个微博系统,功能包括路由控制.页面模板.数据库访问.用户注册.登录.用户会话等内容. 会介绍Express框架.MVC设计模式.ejs模板引擎以及MongoDB数据库的操作. 5.1.准备工作 Express(http://expressjs.com/)除了为http