模块和加载器规范 - 文章教程

模块和加载器规范

发布于 2020-12-02 字数 13117 浏览 1072 评论 0

该文档主要的设计目标是定义前端代码的模块规范,便于开发资源的共享和复用。该文档 在 amdjs 规范的基础上,进行了更细粒度的规范化。

要求

在本文档中,使用的关键字会以中文+括号包含的关键字英文表示: 必须(MUST) 。关键字”MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL”被定义在rfc2119中。

模块定义

模块定义 必须(MUST) 采用如下的方式:

define( factory );

推荐采用 define(factory) 的方式进行 模块定义。使用匿名 moduleId,从而保证开发中模块与路径相关联,有利于模块的管理与整体迁移。

SHOULD NOT使用如下的方式:

define( moduleId, deps, factory );

moduleId

moduleId 的格式应该符合 amdjs 中的约束条件。

  1. moduleId的类型应该是string,并且是由/分割的一些term来组成。例如:this/is/a/moduleId
  2. term应该符合[a-zA-Z0-9_]+这个规则。
  3. moduleId不应该有.js后缀。
  4. moduleId应该跟文件的路径保持一致。

moduleId 在实际使用(如require)的时候,又可以分为如下几种类型:

  1. relative moduleId:是以./或者../开头的moduleId。例如:./foo, ../../bar
  2. top-level moduleId:除上面两种之外的moduleId。例如foobar/abar/b

在模块定义的时候,define 的第一个参数如果是 moduleId必须(MUST) 是 top-level moduleId不允许(MUST NOT) 是 relative moduleId

factory

AMD风格与CommonJS风格

模块的 factory 有两种风格,AMD 推荐的风格 和 CommonJS 的风格AMD 推荐的风格 通过返回一个对象做为模块对象,CommonJS 的风格 通过对 module.exports 或 exports 的属性 赋值来达到暴露模块对象的目的。

建议(SHOULD) 使用 AMD推荐的风格,其更符合Web应用的习惯,对模块的数据类型也便于管理。

// AMD推荐的风格
define( function( require ) {
  return {
    method: function () {
      var foo = require("./foo/bar");
      // blabla...
    }
  };
});

// CommonJS的风格
define( function( require, exports, module ) {
  module.exports = {
    method: function () {
      var foo = require("./foo/bar");
      // blabla...
    }
  };
});

参数

模块的 factory 默认有三个参数,分别是require, exports, module

define( function( require, exports, module ) {
  // blabla...
});

使用AMD推荐风格时,exportsmodule参数可以省略。

define( function( require ) {
  // blabla...
});

开发者 不允许(MUST NOT) 修改require, exports, module参数的形参名称。下面就是错误的用法:

define( function( req, exp, mod ) {
  // blablabla...
});

类型

factory可以是任何类型,一般来说常见的就是三种类型function, string, object。当factory不是function时,将直接做为模块对象。

// src/foo.js
define( "hello world. I'm {name}" );

// src/bar.js
define( {"name": "fe"} );

上面这两种写法等价于:

// src/foo.js
define( function(require) {
  return "hello world. I'm {name}";
});

// src/bar.js
define( function(require) {
  return {"name": "fe"};
} );

require

require这个函数的参数是moduleId,通过调用require我们就可以引入其他的模块。require有两种形式:

require( {string} moduleId );
require( {Array} moduleIdList, {Function} callback );

require存在local requireglobal require的区别。

factory内部的requirelocal require,如果require参数中的moduleId的类型是relative moduleId,那么相对的是当前模块id

在全局作用域下面调用的requireglobal requireglobal require不支持relative moduleId

// src/foo.js
define( function( require ) {
  var bar = require("./bar"); // local require
});

// src/main.js
// global require
require( ['foo', 'bar'], function ( foo, bar ) {   
  // blablalbla...
});

exports

exports是使用CommonJS风格定义模块时,用来公开当前模块对外提供的API的。另外也可以忽略exports参数,直接在factory里面返回自己想公开的API。例如下面三种写法功能是一样的:

define( function( require, exports, module ) {
  exports.name = "foo";
});

define( function( require, exports, module ) {
  return { "name" : "foo" };
});

define( function( require, exports, module ) {
  module.exports.name = "foo";
});

module是当前模块的一些信息,一般不会用到。其中module.exports === exports

dependencies

模块和模块的依赖关系需要通过require函数调用来保证。

// src/js/ui/Button.js
define( function( require, exports, module ) {
  require("css!../../css/ui/Button.css");
  require("tpl!../../tpl/ui/Button.tpl.html");

  var Control = require("ui/Control");
  
  /**
   * @constructor
   * @extends {Control}
   */
  function Button() {
    Control.call(this);

    var foo = require("./foo");
    foo.bar();
  }
  baidu.inherits(Button, Control);

  ...

  // exports = Button;
  // return Button;
});

具体实现的时候是通过正则表达式分析factory的函数体来识别出来的。因此为了保证识别的正确率,请尽量 避免在函数体内定义require变量或者require属性。例如不要这么做:

var require = function(){};
var a = {require:function(){}};
a.require("./foo");
require("./bar");

模块加载器配置

AMD Loader应该支持如下的配置,更新配置的时候,写法如下:

<script src="${amdloader.js}"></script>
<script>
require.config({
  ....
});
</script>

baseUrl

类型应该是string。在ID-to-path的阶段,会以baseUrl作为根目录来计算。如果没有配置的话,就默认以当前页面所在的目录为baseUrl。 如果baseUrl的值是relative,那么相对的是当前页面,而不是AMD Loader所在的位置。

paths

类型应该是Object.<string, string>。它维护的是moduleId前缀到路径的映射规则。这个对象中的key应该是moduleId的前缀,value如果是一个相对路径的话,那么相对的是baseUrl。当然也可以是绝对路径的话,例如:/this/is/a/path//www.google.com/this/is/a/path

{
  baseUrl: '/fe/code/path',
  paths: {
    'ui': 'esui/v1.0/ui',
    'ui/Panel': 'esui/v1.2/ui/Panel',
    'tangram': 'third_party/tangram/v1.0',
    'themes': '//www.baidu.com/css/styles/blue'
  }
}

ID-to-path的阶段,如果模块或者资源是以ui, ui/Panel, tangram开头的话,那么就会去配置指定的地方去加载。例如:

  • ui/Button => /fe/code/path/esui/v1.0/ui/Button.js
  • ui/Panel => /fe/code/path/esui/v1.2/ui/Panel.js
  • js!tangram => /fe/code/path/third_party/tangram/v1.0/tangram.js
  • css!themes/base => //www.baidu.com/css/styles/blue/base.css

另外,需要支持为插件指定不同的的paths,语法如下:

{
  baseUrl: '/fe/code/path',
  paths: {
    'css!': '//www.baidu.com/css/styles/blue',
    'css!foo': 'bar',
    'js!': '//www.google.com/js/gcl',
    'js!foo': 'bar'
  }
}

模块加载器插件

该文档不限定使用何种AMD Loader,但是一个AMD Loader应该支持至少三种插件(css,js,tpl)才能满足我们的业务需求。

插件语法

[Plugin Module ID]![resource ID]

Plugin Module Id是插件的moduleId,例如cssjstpl等等。!是分割符。

resource ID资源Id,可以是top-level或者relative。如果resource IDrelative,那么相对的是当前模块的Id,而不是当前模块Url。例如:

// src/Button.js
define( function( require, exports, module ){
  require( "css!./css/Button.css" );
  require( "css!base.css" );
  require( "tpl!./tpl/Button.tpl.html" );
});

如果当前模块的路径是${root}/src/ui/Button.js,那么该模块依赖的Button.cssButton.tpl.html的路径就应该分别是${root}/src/css/ui/Button.css${root}/src/tpl/Button.tpl.html;该模块依赖的base.css的路径应该是${baseUrl}/base.css

css插件

参考上面的示例。如果resource ID省略后缀名的话,默认是.css;如果有后缀名,以具体的后缀名为准。例如:.less

js插件

用来加载不符合该文档规范的js文件,例如jquerytangram等等。例如:

// src/js/ui/Button.js
define( function( require, exports, module ) {
  require( "js!jquery" );
  require( "js!./tangram" );
});

tpl插件

如果项目需要前端模板,需要通过tpl插件加载。tpl插件由模板引擎提供方实现。插件的语法应该跟上述jscss插件的语法保持一致,例如:

require( "tpl!./foo.tpl.html" );

FAQ

为什么不能采用define(moduleId, deps, factory)来定义模块?

define(moduleId, deps, factory)这种写法,很容易出现很长的deps,影响代码的风格。

define(
  "module/id", 
  [
    "module/a", 
    "module/b", 
    "module/c"
  ], 
  function ( require ) {
    // blabla...
  }
);

构建工具对代码进行处理和编译时,允许将代码编译成这种风格,明确硬依赖。

相对于模块的Id和相对于模块Url有什么区别?

关于id和url的说明

先拿module来看。通常我们define module的时候是使用匿名的,一般情况下,module id和url是对应的,但是有个例外,就是paths配置能够将id映射到非默认对应的url去。所以我们require一个module的时候,不一定是按照默认规则去取module的。由此可以得出:

  1. loader是通过id而不是通过url进行管理的
  2. id -> url是唯一的,但是一个url不一定代表一个module。(define匿名的,不同module可能会映射到同一个文件)

而且,默认规则中,id -> url的结果,是基于baseUrl的url

关于normalize

只有在module内部,使用local require的时候,才有normalize这个行为。global require是不存在normailze这么一说的。其行为包含:

  1. relative id -> toplevel id
  2. id mapping(就是让map配置生效)

对于top level的id,normailze是不会执行step1的。

require resource和require module

在下面require module的过程中:

  1. require(id) -> 2. normalized id -> 3. to url -> 4. download,肯定是在2和3之间判断module是否define,如果有,就直接return

require resource的行为和require module是一样的,也必须是一样的。我想这个没什么好质疑的。

ui/Buttonrequire('css!../../css/button.css')的normalize,无论是什么策略都:

  1. 必须是和baseUrl无关的
  2. 不允许是relative的

都是path style,当然是和默认normalize保持一致最自然。

package开发时的require resource

我为什么一直_强调_在ui/Buttonrequire('css!../../css/button.css')是不合理的,因为:

  1. package开发时是不可能通过require('css!/src/css/button.css')的。这时候不期望提前了解部署细节,不应该。必然是通过相对id
  2. normalize结果不合理,是relative的。requirejs处理的巧妙,esl也是这么处理的,但是不代表我们就不需要去规避这个问题。

回到最本质的问题:package 项目的 src 目录的目录结构

我们在讨论的,其实是package项目的src目录的目录结构

灰大提出的模式,其他同学为什么不能接受?不能接受的具体原因是什么?

如果你对这篇文章有疑问,欢迎到本站 社区 发帖提问或使用手Q扫描下方二维码加群参与讨论,获取更多帮助。

扫码加入群聊

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

目前还没有任何评论,快来抢沙发吧!

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

2583 文章
29 评论
84935 人气
更多

推荐作者

Jay

文章 0 评论 0

guowei007

文章 0 评论 0

2668157715

文章 0 评论 0

HY阳

文章 0 评论 0

想挽留

文章 30 评论 3