Magento 2 and RequireJS (翻译)

原文地址

既然我们已经探讨过向 Magento 2 中引入 javaScript 和 CSS 的基本知识了,现在我们要开始探索 Magento 引入的现代前端工具和库了。

Magento 引入现代前端科技处于有点 tricky 的位置。Magento 是一个软件平台,其次才是在该平台上的电子商务系统。有些机构的市场营销项目在启用6个月后就会被抛弃,有些原型工作在产品发布一年后就会贬值,Magento 和他们不同,作为电商平台需要专注于稳定、成熟的技术,要接受时间的考验。

今天,我们将探讨RequireJS—— 他是 Magento 2 系统中几乎所有 javaScript 的基础。

在我们说到 Magento 2 的 RequireJS 实现之前,我们先要快速地了解下 RequireJS 是做什么的。

RequireJS

RequireJS 是一个 javaScript 模块系统。他实现的是 AMD 标准(Asynchronous module definition)(相关的另一个标准叫 CMD)

AMD 标准中,javaScript 模块提供做以下事情的方法:

  1. 运行 javaScript 程序默认不会用全局命名空间。
  2. 模块间共享代码和数据(Share javascript code and data between named modules and programs)

这就是 RequireJS 的全部职责。你可能会使用 RequireJS 的模块实现某个功能,但是这个功能不是 RequireJS 本身提供的。 RequireJS 只是确保你获得那个功能的工具。

RequireJS 的 start page就有不错的例子解释 RequireJS 是怎么工作的。我们直接扣过来,再加点说明。

首先,下载 RequireJS 的源文件把它保存到一个名为scripts的文件夹下。

然后,创建如下文件:

<!-- File: require-example.html -->
<!DOCTYPE html>
<html>
    <head>
        <title>My Sample Project</title>
        <!-- data-main attribute tells require.js to load
             scripts/main.js after require.js loads. -->
        <script data-main="scripts/main" src="scripts/require.js"></script>
    </head>
    <body>
        <h1>My Sample Project</h1>
    </body>
</html>

下面的代码让这个页面加载 RequireJS

<!-- File: require-example.html -->
<script data-main="scripts/main" src="scripts/require.js"></script>

src用法我们是熟悉的,除了它,还有另一个custom data-main属性。这个属性告诉 RequireJS 使用scripts/main模块作为程序的入口。这个scripts/main对应文件scripts/main.js

创建这么个文件:

//File: scripts/main.js
requirejs([], function() {
    alert("Hello World");
});

这个文件创建好后,重新载入你的页面,这时候你应该看到弹出框弹出“Hello World”。恭喜你,你已经创建了第一个 RequireJS 程序。

用jQuery 的document ready也可以做到,RequireJS 没有比它多做什么。

jQuery(function(){
    alert("Hello World");
});

RequireJS 特别在他的模块化。举个例子,假设我们想要用一个叫helper/world的模块,那么修改我们的main.js文件:

requirejs(['helper/world'], function(helper_world) {
    var message = helper_world.getMessage();
    alert(message);
});

这里,我们将要载入的模块组织成一个数组,并将该数组作为第一个参数传递给requirejs方法。然后,RequireJS 将helper/world的输出作为helper_world参数传递给 function。

当然,如果你现在运行的话会得到一个 javascript 错误。这是因为我们还没有定义helper/world模块。要定义这个模块,将模块的名字转化为文件的路径,并创建如下内容:

//File: scripts/helper/world.js
define([], function(){
    var o = {};
    o.getMessage = function()
    {
        return 'Hello Module World';
    }
    return o;
});

模块定义和我们之前的主程序定义差不多,差别在于这里使用的是define而不是requirejsdefine的第一个参数是你模块需要使用的 RequireJS 模块数组。(我们的例子里,这里是空的)第二个参数是javaScript 的函数/闭包,定义你模块的返回值。

RequireJS 不规定模块应该返回或者输出什么。模块可以返回字符串,可以返回仅有一个方法的 javaScrinpt 对象(上面这个例子就是)。他还可以载入一个 javaScript 库(比如PrototypeJS)然后返回一个PrototypeJS 对象。RequireJS 只提供通过模块共享代码的系统,剩下的取决于项目开发者。

在我们开始 Magento 的 RequireJS 实现之前,有两个主题需要先谈谈:Require JS file loading 和 RequireJS module naming

RequireJS File Loading

默认情况下,RequireJS 会将模块名转换成 HTTP(S)路径。比如helper/world转化成:

http://example.com/scripts/helper/world.js
https://example.com/scripts/helper/world.js
//example.com/helper/scripts/world.js

模块名被转化为以.js结尾的文件路径。默认情况下,RequireJS 会使用require.js脚本所在的文件夹作为基础路径。(上面的例子中就是/script

但是,RequireJS 允许你设置不同的基础路径。在 RequireJS 程序开始前,添加以下代码:

require.config({
    baseUrl: '/my-javascript-code',
});

添加这段代码后,RequireJS 要加载helper/world模块的时候,就会从下面的路径去找:

http://example.com/my-javascript-code/helper/world.js
https://example.com/my-javascript-code/helper/world.js
//example.com/my-javascript-code/helper/world.js

这个功能让你爱把 js 脚本放哪里就放哪里。

RequireJS:Module Naming

到目前为止,我们的例子中,RequireJS 模块名和他的物理路径是绑定的。换句话说,helper/world模块始终对应路径helper/world.js

RequireJS 允许你通过配置来做点变化。例如,假如你希望你的helper/world模块被称为hello,只要在程序开始前添加如下配置代码:

require.config({
    paths: {
        "hello": "helper/world"
    },
});

path给你的模块重命名或者说是给他一个别名。paths对象的键代表着你想要的别名(hello),键对应的值表示模块的实际名称(helper/world)

上面的代码放好后,

requirejs(['hello'], function(hello) {
    alert("Hello World");
});

这段代码就会从helper/world.js路径来加载hello模块。

还有很多其他的配置指令来控制 RequireJS 从哪里加载模块,不过这超出本篇的范围了,了解更多,请参考“Load Javascript Files”

对日常的 javaScript 开发来说,你本不需要关注模块是如何通过 HTPP 加载的。你只需要在用到某模块时,就能得到那个模块,这就够了。当你需要添加新模块,添加某些不兼容的代码,或者查找某个模块的源代码(为了弄清他做什么的)时,就要关注 RequireJS 是怎么加载文件的了。

Magento 2 and RequireJS

Magento 自带了 RequireJS 库,引入了一些配置,并且提供了让你添加自己的配置的机制。

Magento 2 使用了上面提到的 baseUrl。如果你查看 Magento 的页面源文件,你会看到如下代码:

<script type="text/javascript">
    require.config(
        {"baseUrl":"http://magento.example.com/static/adminhtml/Magento/backend/en_US"}
    );
</script>

这意味着,遇到helper/world模块的时候,他将会从类似下面的URL找文件:

http://magento.example.com/static/adminhtml/Magento/backend/en_US/helper/world.js

如果你阅读过 Alan 这个系列的前几篇文章,你可能认出这个URL是从模块加载前端静态的文件的URL了。这意味着你可以在你的模块中放一个RequireJS模块定义文件,例如在下面这个位置:

app/code/Package/Module/view/base/web/my_module.js

这样你就有了一个名为

Package_Module/my_module

的RequireJS 模块。
可以从以下URL加载他:

http://magento.example.com/static/adminhtml/Magento/backend/en_US/Package_Module/my_module.js

这意味着你可以立刻开始在phtml模板中使用该模块了,就像这样:

<script type="text/javascript">
    requirejs('Package_Module/my_module', function(my_module){
        //...program here...
    });
</script>

或者使用单独的javaScript 文件来使用它。

Congiguring RequireJS via Modules

前面,我们说过RequireJS 的两个配置指令——baseUrlpath。随着你步入高级开发阶段,你将接触并使用到许多 RequireJS 的其他配置指令。

每个 Magento 模块都可以通过一个名为requirejs-config.js的文件来添加 RequireJS 配置指令。

app/code/Package/Module/view/base/requirejs-config.js
app/code/Package/Module/view/frontend/requirejs-config.js
app/code/Package/Module/view/adminhtml/requirejs-config.js

This is a special javascript file that Magento will automatically load on every page load using the area hierarchy. 让我们来试试看。首先我们要创建一个名为Pulsestorm_RequireJsTutorial的Magento 模块。
创建好后启用该模块:

$ php bin/magento module:enable Pulsestorm_RequireJsTutorial
$ php bin/magento setup:upgrade

如果你不会创建Magento 模块的话,请参考Magento 2 简介 —— 不再是 MVC

模块创建好后,请增加如下文件:

//File: app/code/Pulsestorm/RequireJsTutorial/view/base/requirejs-config.js
alert("Hello");

清空缓存,载入 Magento 系统的任意页面(包括后台),你应该会看到alert被调用的弹出框了。恭喜你,你已经成功得给模块增加了一个requirejs-config.js文件。

The Purpose of requirejs-config.js

虽然你可以使用requirejs-config.js运行任意的js,但是他的主要任务是:

  1. 让 end-user-programmers 向 Magento 的 RequireJS 系统中增加require.config选项。
  2. 让 end-user-programmers 对自己的 js 代码进行配置或初始化。

要了解 RequireJS 是怎么做的,我们需要看看 Magento 实际上是怎么处理这些requirejs-config.js文件的。如果你查看 Magento 的任意页面的源代码,你会看到类似下面的代码:

<script  type="text/javascript"  src="http://magento.example.com/static/_requirejs/adminhtml/Magento/backend/en_US/requirejs-config.js"></script>

这是在setup:di:compile(production 模式)过程中,或者运行时(developer 和 default模式)生成的一个特殊的 javaScript 文件。如果你不太了解 Magento 的模式对前端文件加载的影响,你可以查看 Alan 的文章Magento 2: Serving Frontend Files。在接下来的文章中,我们都假设你是developer模式的。

如果你在浏览器中查看下requirejs-config.js文件,你会看到你的alert表达式出现在像下面的代码中

(function() {
    alert("Hello World");
    require.config(config);
})();

虽然它不是100%明显,Magento 2 通过从requirejs-config.js生成上述代码块来让我们给系统添加额外的 RequireJS 初始化。

我们通过一个具体的例子来进一步了解上面的意思。让我们修改requirejs-config.js变成下面这样:

var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

alert("Done");

这里我们定义了一个config变量,并且修改了alert的值。现在你再一次载入页面,看看requirejs-config.js文件,你应该就知道 Magento 到底干了什么。

(function() {
var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

alert("Done");
require.config(config);
})();

对每个requirejs-config.js文件,Magento 都创建了一个类似下面的代码块:

(function() {
    //CONTENTS HERE
    require.config(config);
})();

requirejs-config.js的内容替换了//CONTENTS HERE

var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

alert("Done");

这意味着,如果我们在requirejs-config.js文件中定义一个config变量,Magento 最终会将该变量传递给require.config。这将使得 Magento 的模块开发者可以使用 RequireJS的一些特性,比如:shim,paths,baseUrl,map以及其他RequireJS’s configuration directives

Understanding Lazy Loading

另一个要理解的重点是RequireJS 的模块是延迟加载的(lazy load)。

换句话说,加入我们使用了下面的配置:

var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

默认情况下,Magento 是不会加载Package_Module/my_module.js文件的。Magento 只会在你要用它的时候加载他。

requirejs(['my_module'], function(my_module){

});

requirejs(['Package_Module/my_module'], function(my_module){

});

define(['Package_Module/my_module'], function(my_module){

});

记住,RequireJS 的日常开发时是不需要考虑如何请求源文件的细节的。延迟加载帮助用户节约了带宽,有时候某些页面用不着某些js 文件,这样就不用下载他们了。

但是,在不那么理想的情况下,延迟加载的行为可能和一些比较早的 js 框架或是库配合不好。下面我们在讲到一些jQuery gotchas时,我们会探讨一个例子。

Global jQuery Object

即使你决定不用 RequireJS,你坚持用 plain old jQuery。你还是需要知道 RequireJS 是怎样与 AMD 标准前的 js 库交互的。

在 Magento 2 中,jQuery 以 RequireJS 模块的方式被加载进来。这意味着,如果你尝试使用如下代码:

<script type="text/javascript">
    jQuery(function(){
        //your code here
    });
</script>

你的浏览器会报错说jQuery is undefined。这是因为jQuery 全局对象并不存在,你得以 RequireJS 模块的方式使用jQuery。如果你习惯写上面这种类型的代码,你得这样做:

  1. Replace it with code that kicks off execution of a RequireJS program
  2. Configure that program to use the jquery module as a dependency

换句话说,像下面这样:

requirejs(['jquery'], function(jQuery){
    jQuery(function(){
        //your code here
    });
});

通过requirejs函数调用开始,传递给他所依赖的模块的数组,并且以匿名函数作为程序的main entry point。

requirejs的第一个参数是他所依赖的模块的数组列表。举例来说,下面的代码相当于告诉 RequireJS “我的程序是依赖jQuery模块的”。

requirejs(['jquery'],

requirejs的第二个参数是一个匿名函数,RequireJS 会加载你声明的依赖模块,并将他的返回值传递给匿名函数并调用它。

jQuery 的新版本会检测自己是否被包含在 RequireJS/AMD 的环境中,如果是就会定义一个模块,并且返回全局的 jQuery 对象。

/ File: http://code.jquery.com/jquery-1.12.0.js
// Register as a named AMD module, since jQuery can be concatenated with other
// files that may use define, but not via a proper concatenation script that
// understands anonymous AMD modules. A named AMD is safest and most robust
// way to register. Lowercase jquery is used because AMD module names are
// derived from file names, and jQuery is normally delivered in a lowercase
// file name. Do this after creating the global so that if an AMD module wants
// to call noConflict to hide this version of jQuery, it will work.

// Note that for maximum portability, libraries that are not jQuery should
// declare themselves as anonymous modules, and avoid setting a global if an
// AMD loader is present. jQuery is a special case. For more information, see
// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon

if ( typeof define === "function" && define.amd ) {
    define( "jquery", [], function() {
        return jQuery;
    } );
}

RequireJS and jQuery Plugins

There’s another gotcha to using jQuery and RequireJS together. jQuery 库早于 RequireJS 和 AMD 标准好多年,他形成了自己的庞大的插件系统。那时候还没有模块化,js 默认用的全局变量,这个插件系统也配合得蛮好的,插件开发者通过修改全局的 jQuery 对象来创建他们的插件。

对 RequireJS 来说这样就带来一个问题 —— 就像之前我们提到的,全局的 jQuery 对象是不存在的,你得在requirejs匿名函数中使用jQuery模块。这意味着以前下面这种引入 jQuery 插件的方式会在用到jQuery或者$别名时会出错。

<script src="http://magento.example.com/js/path/to/jquery/plugin/jquery.cookie.js">
//File: http://magento.example.com/js/path/to/jquery/plugin/jquery.cookie.js
var config = $.cookie = function (key, value, options) {

如果你想在 Magento 2 的系统中使用 jQuery 插件,你得通过 RequrieJS。幸运的是,过程还是相对直接的。

首先,你得通过path给该插件一个别名

var config = {
    paths:{
        "jquery.cookie":"Package_Module/path/to/jquery.cookie.min"
    }
};

上面的代码创建了一个名为jquery.cookie的模块,该模块的位于Package_Module模块,是一个jQuery cookie 插件。

现在,你可能认为我们可以用下面的代码来使用它了。

requirejs(['jquery','jquery.cookie'], function(jQuery, jQueryCookie){
    //my code here
});

毕竟,我们列出的依赖模块有jQueryjquery.cookie,这应该会触发对他们的加载。

你是对的——但只是有时候。RequireJS 是异步加载模块源文件的,但它可不保证加载的顺序。这意味着它可能会先加载jQuery 库,但是,有时候页面上的其他脚本或者网络情况可能会导致jQuery cookie 插件反而先被加载了。如果jQuery cookie 先被加载了,他就会因为找不到 jQuery 对象而出错。

这样看来,RequireJS 的设计可不太好。但是你得知道 RequireJS 和 AMD 标准是用来避免全局变量的污染的。RequireJS 和 jQuery 这样的库不能无缝对接,这一点都不奇怪。 Even though jQuery is responsible about its use of global state (one global jQuery object), it still uses global state, and RequireJS isn’t going to get in the business of deciding who does and doesn’t use global state responsibly.

关于加载顺序,RequireJS 提供了shim配置指令来让我们指定依赖模块的加载顺序。
你可以告诉 RequireJS :

Hey RequireJS,当你加载jquery.cookie的时候,请你确保jquery 模块已经完全加载了。

配置就像这样:

var config = {
    paths:{
        "jquery.cookie":"Package_Module/path/to/jquery.cookie.min"
    },
    shim:{
        'jquery.cookie':{
            'deps':['jquery']
        }
    }
};

我们定义了一个名为shim的配置属性,这个属性是一个键值对js对象。键表示模块的名字,值是js另一个对象,他定义该模块的shim配置。

shim还有很多其他配置选项。上面我们用的deps确保 RequireJS 在加载jquery.cookie 前先加载数组中的模块([jquery])。

dep配置选项只显示了shim能够做的一小部分,想了解更多细节,请参阅the shim documentation

有了上面的配置,你现在可以安全地创建依赖 jquery cookie 插件的 RequireJS 程序了。

Require vs RequireJS

在总结前还有一件事要注意。整个 RequireJS 的文档,你可以看到两个函数:

require()
requirejs();

这两个函数有什么不同吗?哦,他们没有什么不同,他们是同一个函数。

AMD 标准命名为 require,但是,RequireJS 意识到一些在用的代码已经定义了 require 函数,为了保证 RequirejS 可以同那些定义了 require 函数的代码一起使用,所以提供了一个 别名叫 requirejs

Wrap Up

本篇已经带你初步了解了 Magento 对 javaScript 以及现代前端库的使用。但是,所有这些都有赖于 RequireJS 。If you start there, you should be able to track back any javascript based feature to its inclusion via RequireJS, and through that figure out what’s going on. As always, knowing what a specific library does is always useful — but knowing how the framework your code lives in works is the key to becoming a more productive and rational programmer.

相关源文件下载

https://github.com/PiscesThankIT/RequireJS-tutorial

https://github.com/PiscesThankIT/RequireJsTutorial

发表评论

电子邮件地址不会被公开。 必填项已用*标注