2019/07/18
posted in
JavaScript
#模块化
2019/07/18
posted in
JavaScript
#模块化
模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。
模块化主要是解决代码分割、作用域隔离、模块之间的依赖管理以及发布到生产环境时的自动化打包与处理等多个方面。
Atwood定律说:"任何可以用JavaScript来开发的应用,最终都将用JavaScript来开发",所以javascript能做的事越来越多,应用也日益复杂,模块化已经成为一个迫切需求。但是作为一个模块化方案,它至少要解决如下问题:
最原始的方式就是,每个文件就是一个模块,然后使用script的方式进行引入。
但是此方式有以下问题:
为了解决作用域污染的问题,就产生了立即执行函数 + 模块对象模式:
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
// 调用:module1.m1()
// 会暴露所有模块成员,内部状态可以被外部改写
var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})();
// 也可以优化一下
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
// app1.js
var app = {};
// app2.js 添加属性
(function(){
app.a = function(a, b) {
// code
}
})();
// app3.js 获取属性
(function(app){
var temp = [ 1, 2];
var a = app.a(temp)
})(app);
具体的可以查阅阮一峰老师的博客Javascript模块化编程(一):模块的写法
在ES6之前,js没有块级作用域,所以采用此方式建立一个函数作用域。但是在ES6之后,可以使用块级作用域。
由于使用了IIFE,所以减少了全局作用域污染,但并不是彻底消除,因为还定义了一个app模块对象呢。
这种方式一度非常流行,就连大名鼎鼎的jquery就是这种模式,但是它并不完美,因为仅仅只是减少了作用域污染,还是会有其他缺点。
在上述过程中,还有两种(全局函数和命名空间模式)方式本文没有提及。
至此,还没有像样的模块化规范,是时候该建立一些规范了。
后来,有人试图将javascript引入服务端,由于服务端编程相对比较复杂,就急需一种模块化的方案,所以就诞生了commonjs,有require + module.exports
实现模块的加载和导出。
CommonJS采用同步的方式加载模块,主要使用场景为服务端编程。因为服务器一般都是本地加载,速度较快。
后来,随着前端业务的日渐复杂,浏览器端也需要模块化,但是commonjs是同步加载的,这意味着加载模块时,浏览器会冻结,什么都干不了,这在浏览器肯定是不行的,这就诞生了AMD和CMD规范,分别以requirejs和seajs为代表。
这两货都采用异步方式加载模块。
AMD(Asynchronous Module Defination)异步模块加载机制。
define(
[module_id,] // 模块名字,如果缺省则为匿名模块
[dependenciesArray,] // 模块依赖
definition function | object // 模块内容,可以为函数或者对象
);
CMD(Common Module Defination)通用模块加载机制
// 方式一
define(function(require, exports, module) {
// 模块代码
var a = require('a')
});
// 方式二
define( 'module', ['module1', 'module2'], function( require, exports, module ){
// 模块代码
} );
尽管以上方案解决了上面说的问题,但是也带来了一些新问题:
由于上述这些原因,有些人想在浏览器使用 CommonJS 规范,但 CommonJS 语法主要是针对服务端且是同步的,所以就产生了Browserify,它是一个 模块打包器(module bundler),可以打包commonjs规范的模块到浏览器中使用。
UMD(Universal Module Definition) 统一模块定义。
AMD 与 CommonJS 虽然师出同源,但还是分道扬镳,关注于代码异步加载与最小化入口模块的开发者将目光投注于 AMD;而随着 Node.js 以及 Browserify 的流行,越来越多的开发者也接受了 CommonJS 规范。令人扼腕叹息的是,符合 AMD 规范的模块并不能直接运行于 CommonJS 模块规范的环境中,符合 CommonJS 规范的模块也不能由 AMD 进行异步加载。
而且有这么多种规范,如果我们要发布一个模块供其他人用,我们不可能为每种规范发布一个版本,就算你闲得蛋疼这样做了,别人使用的时候还得下载对应版本,所以现在需要一种方案来兼容这些规范。
实现的方式就是在代码前面做下判断,根据不同的规范使用对应的加载方式。
// 以vue为例
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Vue = factory());
}(this, function () {
// vue code ...
})
由于目前ES6浏览器支持还不够好,所以很多第三方库都采用了这种方式。
ES6引入了ESModule规范,主要通过export + import
来实现,最终一统江湖。可是现实很骨感,一些浏览器并不支持(IE,说的就是你),所以还不能直接在浏览器中直接使用。
requirejs/seajs/sytemjs
在页面上加载一个AMD/CMD模块格式解释器。这样浏览器就认识了define
, exports
,module
这些东西,也就实现了模块化。
SystemJS 是一个通用的模块加载器,它能在浏览器或者 NodeJS 上动态加载模块,并且支持 CommonJS、AMD、全局模块对象和 ES6 模块。通过使用插件,它不仅可以加载 JavaScript,还可以加载 CoffeeScript 和 TypeScript。配合jspm也是不错的搭配。
browserify/webpack
相比于第一种方案,这个方案更加智能。由于是预编译的,不需要在浏览器中加载解释器。你在本地直接写JS,不管是AMD/CMD/ES6风格的模块化,它都能认识,并且编译成浏览器认识的JS。
注意: browerify只支持Commonjs模块,如需兼容AMD模块,则需要plugin转换
前身为ServerJS。
我们可以理解为代码会被如下内建辅助函数包裹:
(function (exports, require, module, __filename, __dirname) {
// ...
// Your code
// ...
});
通过require加载模块。
const a = require('a')
通过exports
和module.exports
进行模块导出。
exports
:exports
是module.exports
的一个引用,一个模块可以使用多次,但是不能直接对exports
重新赋值,只能通过如下方式使用exports.a = function(){
// code...
}
module.exports
:一个模块只能使用一次module.exports = function(){
// code...
}
在引入requirejs的script标签上添加data-main
属性定义入口文件,该文件会在requirejs加载完后立即执行。
如果baseUrl未单独配置,则默认为引入require的文件的路径。
<script src="./assets/lib/requirejs/require.js" data-main="./assets/lib/requirejs/config"></script>
requirejs.config({
// 为模块加上query参数,解决浏览器缓存,只在开发环境使用
urlArgs: 'yn-course=' + (new Date()).getTime(),
// 配置所有模块加载的初始路径,所有模块都是基于此路径加载
baseUrl: './',
// 映射一些快捷路径,相当于别名
paths: {
'~': 'assets',
'@': 'components',
'vue': 'assets/lib/vue/vue',
'vueRouter': 'assets/lib/vue-router/vue-router',
"jquery" : ["http://libs.baidu.com/jquery/2.0.3/jquery"]
},
// 对于匹配的模块前缀,使用一个不同的模块ID来加载该模块
map: {
'layer': {
'jquery': 'http://libs.baidu.com/jquery/2.0.3/jquery'
}
},
// 从CommonJS包(package)中加载模块
packages:{},
// 加载上下文
context:{},
// 超时,默认7S
waitSeconds: 7,
// 定义应用依赖的模块,在启动后会加载此数组中的模块
deps: [],
// 在deps加载完毕后执行的函数
callback:function(){},
// 用来加载非AMD规范的模块,以浏览器全局变量注入,此处仅作映射,需要在需要时手动载入
shim: {
// 'backbone': {
// deps: ['underscore', 'jquery'], // 模块依赖
// exports: 'Backbone' // 导出的名称
// }
},
// 全局配置信息,可在模块中通过module.config()访问
config:{
color:'red'
},
// 如果设置为true,则当一个脚本不是通过define()定义且不具备可供检查的shim导出字串值时,就会抛出错误
enforceDefine:false,
// 如果设置为true,则使用document.createElementNS()去创建script元素
xhtm: false,
//指定RequireJS将script标签插入document时所用的type=""值
scriptType:'text/javascript'
});
默认requirejs会根据baseUrl+paths
配置去查找模块,但是如下情况例外:
lib/hello.js
、hello.js
“/”
开始"http:"
、"https"
设置baseURl的方式有如下三种:
requirejs.config
指定;data-main
,则baseUrl
为data-main
所对应的js的目录map配置对于大型项目很重要:如有两类模块需要使用不同版本的"foo",但它们之间仍需要一定的协同。
在那些基于上下文的多版本实现中很难做到这一点。而且,paths配置仅用于为模块ID设置root paths
,而不是为了将一个模块ID映射到另一个。
requirejs.config({
map: {
'some/newmodule': {
'foo': 'foo1.2'
},
'some/oldmodule': {
'foo': 'foo1.0'
}
}
});
通过define来定义模块,推荐依赖前置原则,当然也可以使用require动态按需加载。
define(
[module_id,] // 模块名字,如果缺省则为匿名模块
[dependencies,] // 模块依赖
definition function | object // 模块内容,可以为函数或者对象
);
// 如果仅仅返回一个键值对,可以采用如下格式,类似JSONP
define({
color: "black",
size: "unisize"
})
//如果没有依赖
define(function () {
return {
color: "black",
size: "unisize"
}
})
// 有依赖
define(["./a", "./b"], function(a, b) {
})
// 具名模块
define("name",
["c", "d"],
function(cart, inventory) {
//此处定义foo/title object
}
)
如要在define()内部使用诸如require("./a/b")
相对路径,记得将"require"本身作为一个依赖注入到模块中:
define(["require", "./a/b"], function(require) {
var mod = require("./a/b");
});
或者使用如下方式:
define(function(require) {
var mod = require("./a/b");
})
require加载的所有模块都是单例的,每个模块都有一个唯一的标识,这个标识是模块的名字或者模块的相对路径(如匿名模块)。
模块的唯一性与它们的访问路径无关,即使是地址完全相同的一份JS文件,如果引用的方式与模块的配置方式不一致,依旧会产生多个模块。
// User.js
define([], function() {
return {
username : 'yiifaa',
age : 20
};
});
require(['user/User'], function(user) {
// 修改了User模块的内容
user.username = 'yiifee';
// em/User以baseUrl定义的模块进行访问
// 'user/User'以path定义的模块进行访问
require(['em/User', 'user/User'], function(u1, u2) {
// 输出的结果完全不相同,u1为yiifaa,u2为修改后的内容yiifee
console.log(u1, u2);
})
})
requirejs推荐依赖前置,在define或者require模块的时候,可以将需要依赖的模块作为第一个参数,以数组的方式声明,然后在回调函数中,依赖会以参数的形式注入到该函数上,参数列表需要和依赖数组中位置一一对应。
define(["./a", "./b"], function(a, b) {
})
在requirejs中,有3中方式进行模块导出:
define(function(require, exports, module) {
return {
a : 'a'
}
});
define(function(require, exports, module) {
module.exports = {
a : 'a'
}
});
define(function(require, exports, module) {
exports.a = 'a'
});
requirejs提供了两个全局变量require
、requirejs
供我们加载模块,这二者是完全等价的。
// 此处require 和 define 函数仅仅是一个参数(模块标识)的差异,
// 一般require用于没有返回的模块,如应用顶层模块
require(
[dependencies,] // 模块依赖
definition function // 模块内容
);
require是内置模块,不用在配置中定义,直接进行引用即可。
define(['require'], function(require) {
var $ = require('jquery');
})
requirejs支持异步(require([module])
)和同步(require(module)
)两种方式加载,即require参数为数组即为异步加载,反之为同步。
在requirejs中,执行同步加载必须满足两点要求:
define(function(require, exports, module) { })
中可以同步加载模块// 假定这里引用的资源有数十个,回调函数的参数必定非常多
define(['jquery'], function() {
return function(el) {
// 这就是传说中的同步调用
var $ = require('jquery');
$(el).html('Hello, World!');
}
})
prototype
与jquery
的冲突问题define(['jquery', 'prototype'], function() {
var export = {};
export.jquery = function(el) {
// 这就是传说中的同步调用
var $ = require('jquery');
$(el).html('Hello, World!');
}
export.proto = function(el) {
// 这就是传说中的同步调用
var $ = require('prototype');
$(el).html('Hello, World!');
}
return export;
})
define([],function())
:依赖数组中的模块会异步加载,所有模块加载完成后混执行回调函数require([])
:传入数组格式即表示需要异步加载require === requirejs //=> true
require.toUrl("./a.css")
: 获取模块url只要页面不刷新,被requirejs加载的模块只会执行一次,后面会一直缓存在内存中,即时重新引入模块也不会再进行初始化。
我们可以通过undef
卸载已加载的模块。
require.undef("moduleName") // moduleName是模块标识
Module name has not been loaded yet for context: _
:此错误表示执行时模块还未加载成功,一般为异步加载所致,改成同步加载即可。
//C模块
define([],function(){
// 定义一个类
function DemoClass()
{
var count = 0;
this.say = function(){
count++;
return count;
};
}
return function(){
//每次都返回一个新对象
return new DemoClass();
};
});
// A模块
require(['C'],
function(module) {
cosole.log(module().say());//1
});
// B模块
require(['C'],
function(module) {
cosole.log(module().say());//1
});
Sea.js 追求简单、自然的代码书写和组织方式,具有以下核心特性:
通过exports + require
实现模块的加载与导出。
<script src="assets/lib/seajs/sea.js"></script>
<script src="assets/lib/seajs/seajs.config.js"></script>
<script type="text/javascript">
seajs.use('app');
</script>
//seajs配置
seajs.config({
//1.顶级标识始终相对 base 基础路径解析。
//2.绝对路径和根路径始终相对当前页面解析。
//3.require 和 require.async 中的相对路径相对当前模块路径来解析。
//4.seajs.use 中的相对路径始终相对当前页面来解析。
// Sea.js 的基础路径 在解析顶级标识时,会相对 base 路径来解析 base 的默认值为 sea.js 的访问路径的父级
base: './',
// 路径配置 当目录比较深,或需要跨目录调用模块时,可以使用 paths 来简化书写
paths: {
gallery: "https://a.alipayobjects.com/gallery"
/*
var underscore = require('gallery/underscore');
//=> 加载的是 https://a.alipayobjects.com/gallery/underscore.js
*/
},
// 别名配置 当模块标识很长时,可以使用 alias 来简化(相当于 base 设置的目录为基础)
//Sea.js 在解析模块标识时, 除非在路径中有问号(?)或最后一个字符是井号(#),否则都会自动添加 JS 扩展名(.js)。如果不想自动添加扩展名,可以在路径末尾加上井号(#)。
alias: {
'seajs-css': '~/lib/seajs/plugins/seajs-css',
'seajs-text': '~/lib/seajs/plugins/seajs-text',
'$': '~/lib/zepto/zepto'
},
// 变量配置 有些场景下,模块路径在运行时才能确定,这时可以使用 vars 变量来配置
vars: {
//locale: "zh-cn"
/*
var lang = require('./i18n/{locale}.js');
//=> 加载的是 path/to/i18n/zh-cn.js
*/
},
// 映射配置 该配置可对模块路径进行映射修改,可用于路径转换、在线调试等
map: [
//[".js", "-debug.js"]
/*
var a = require('./a');
//=> 加载的是 ./js/a-debug.js
*/
],
// 预加载项 在普通模块加载前,提前加载并初始化好指定模块 preload 中的配置,需要等到 use 时才加载
preload: ['seajs-css','seajs-text'],
// 调试模式 值为 true 时,加载器不会删除动态插入的 script 标签。插件也可以根据 debug 配置,来决策 log 等信息的输出
debug: true,
// 文件编码 获取模块文件时,<script> 或 <link> 标签的 charset 属性。 默认是 utf-8 还可以是一个函数
charset: 'utf-8'
});
<script src="..."></script>
一样,会相对当前页面解析use
用来在页面中加载一个或多个模块。seajs.use
理论上只用于加载启动,不应该出现在 define 中的模块代码里。在模块代码里需要异步加载其他模块时,推荐使用 require.async
方法。
// 加载一个模块
seajs.use('./a');
// 加载一个模块,在加载完成时,执行回调
seajs.use('./a', function(a) {
a.doSomething();
});
// 加载多个模块,在加载完成时,执行回调
seajs.use(['./a', './b'], function(a, b) {
a.doSomething();
b.doSomething();
});
define
// 方式一
define(function(require, exports, module) {
// 模块代码
var a = require('a')
});
// 方式二,此方法严格来说不属于CMD规范
define( 'module', ['module1', 'module2'], function( require, exports, module ){
// 模块代码
});
// 如果模块内容仅是对象或者字符串
define({ "foo": "bar" });
define('I am a template. My name is {{name}}.');
require
require
是一个方法,接受 模块标识作为唯一参数,用来获取其他模块提供的接口。
此方式,require
的参数值 必须 是字符串直接量。
var a = require('./a');
require.async
方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback
参数可选。
此时,参数值可以是动态的,以实现动态加载。
define(function(require, exports, module) {
// 异步加载一个模块,在加载完成时,执行回调
require.async('./b', function(b) {
b.doSomething();
});
// 异步加载多个模块,在加载完成时,执行回调
require.async(['./c', './d'], function(c, d) {
c.doSomething();
d.doSomething();
});
});
require.resolve
使用模块系统内部的路径解析机制来解析并返回模块绝对路径。
define(function(require, exports) {
console.log(require.resolve('./b'));
// ==> http://example.com/path/to/b.js
});
exports
是一个对象,用来向外提供模块接口,也可以使用return
或者module.exports
来进行导出
define(function(require, exports) {
// 对外提供 foo 属性
exports.foo = 'bar';
// return
return {
foo: 'bar',
doSomething: function() {}
};
// module.exports
module.exports = {
foo: 'bar',
doSomething: function() {}
};
});
module
module
是一个对象,上面存储了与当前模块相关联的一些属性和方法。
module.id
模块的唯一标识module.uri
根据模块系统的路径解析规则得到的模块绝对路径module.dependencies
表示当前模块的依赖module.exports
当前模块对外提供的接口文档:官方文档
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
。
严格模式主要有以下限制:
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化arguments.callee
arguments.caller
fn.caller
和fn.arguments
获取函数调用的堆栈export
命令定义模块的对外接口。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量。
以下是几种用法:
//------输出变量------
export var firstName = 'Michael';
export var lastName = 'Jackson';
//等价于
var firstName = 'Michael';
export {firstName}; //推荐,能清除知道输出了哪些变量
//------输出函数或类------
export function multiply(x, y) {
return x * y;
};
//------输出并as重命名------
var v1 = 'Michael';
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2
};
//------输出default------
export default function () { ... }
注意:export default
在一个模块中只能有一个。
import
命令使用export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
以下是几种用法,必须和上面的export
对应:
//------加载变量、函数或类------
import {firstName, lastName} from './profile.js';
//------加载并as重命名------
import { lastName as surname } from './profile.js';
//------加载有default输出的模块------
import v1 from './profile.js';
//------执行所加载的模块------
import 'lodash';
//------加载模块所有输出------
import * as surname from './profile.js';
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起。
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };
参考:前端模块化详解(完整版)