模块和加载器规范

发布于 2020-12-02 09:33:11 字数 11217 浏览 1284 评论 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目录的目录结构

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

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

0 文章
0 评论
84960 人气
更多

推荐作者

漫雪独思

文章 0 评论 0

垂暮老矣

文章 0 评论 0

鹊巢

文章 0 评论 0

萌酱

文章 0 评论 0

雨说

文章 0 评论 0

冰葑

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文