模块和加载器规范
该文档主要的设计目标是定义前端代码的模块规范,便于开发资源的共享和复用。该文档 在 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 中的约束条件。
moduleId
的类型应该是string
,并且是由/
分割的一些term
来组成。例如:this/is/a/moduleId
。term
应该符合[a-zA-Z0-9_]+
这个规则。moduleId
不应该有.js
后缀。moduleId
应该跟文件的路径保持一致。
moduleId
在实际使用(如require
)的时候,又可以分为如下几种类型:
relative moduleId
:是以./
或者../
开头的moduleId
。例如:./foo
,../../bar
。top-level moduleId
:除上面两种之外的moduleId
。例如foo
,bar/a
,bar/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推荐风格
时,exports
和module
参数可以省略。
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 require
和global require
的区别。
在factory
内部的require
是local require
,如果require
参数中的moduleId
的类型是relative moduleId
,那么相对的是当前模块id
。
在全局作用域下面调用的require
是global require
,global 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
,例如css
,js
,tpl
等等。!
是分割符。
resource ID
是资源Id
,可以是top-level
或者relative
。如果resource ID
是relative
,那么相对的是当前模块的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.css
和Button.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文件,例如jquery
,tangram
等等。例如:
// src/js/ui/Button.js
define( function( require, exports, module ) {
require( "js!jquery" );
require( "js!./tangram" );
});
tpl插件
如果项目需要前端模板,需要通过tpl插件加载。tpl插件由模板引擎提供方实现。插件的语法应该跟上述js
,css
插件的语法保持一致,例如:
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的。由此可以得出:
- loader是通过id而不是通过url进行管理的
- id -> url是唯一的,但是一个url不一定代表一个module。(define匿名的,不同module可能会映射到同一个文件)
而且,默认规则中,id -> url的结果,是基于baseUrl的url
。
关于normalize
只有在module内部,使用local require
的时候,才有normalize
这个行为。global require
是不存在normailze
这么一说的。其行为包含:
- relative id -> toplevel id
- id mapping(就是让map配置生效)
对于top level的id,normailze是不会执行step1的。
require resource和require module
在下面require module的过程中:
- require(id) -> 2. normalized id -> 3. to url -> 4. download,肯定是在2和3之间判断module是否define,如果有,就直接return
require resource的行为和require module是一样的,也必须是一样的。我想这个没什么好质疑的。
在ui/Button
中require('css!../../css/button.css')
的normalize,无论是什么策略都:
- 必须是和baseUrl无关的
- 不允许是relative的
都是path style,当然是和默认normalize保持一致最自然。
package开发时的require resource
我为什么一直_强调_在ui/Button
中require('css!../../css/button.css')
是不合理的,因为:
- package开发时是不可能通过
require('css!/src/css/button.css')
的。这时候不期望提前了解部署细节,不应该。必然是通过相对id - normalize结果不合理,是relative的。requirejs处理的巧妙,esl也是这么处理的,但是不代表我们就不需要去
规避
这个问题。
回到最本质的问题:package 项目的 src 目录的目录结构
我们在讨论的,其实是package项目的src目录的目录结构
。
灰大提出的模式,其他同学为什么不能接受?不能接受的具体原因是什么?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论