JS模块化
模块化的理解
- 什么是模块?
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起;
- 块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信;
- 一个模块的组成
- 数据—>内部的属性;
- 操作数据的行为—>内部的函数;
- 模块化是指解决一个复杂的问题时自顶向下把系统划分成若干模块的过程,有多种属性,分别反映其内部特性;
- 模块化编码:编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目;
非模块化的问题
- 页面加载多个js的问题:
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript" src="module3.js"></script>
<script type="text/javascript" src="module4.js"></script>
- 发生问题:
- 难以维护 ;
- 依赖模糊;
- 请求过多;
- 所以,这些问题可以通过现代模块化编码和项目构建来解决;
模块化的优点
1.更好地分离;
- 避免一个页面中放置多个script标签,而只需加载一个需要的整体模块即可,这样对于HTML和JavaScript分离很有好处;
2.更好的代码组织方式;
- 有利于后期更好的维护代码;
3.按需加载
- 提高使用性能,和下载速度,按需求加载需要的模块
4.避免命名冲突
- JavaScript本身是没有命名空间,经常会有命名冲突,模块化就能使模块内的任何形式的命名都不会再和其他模块有冲突。
5.更好的依赖处理
- 使用模块化,只需要在模块内部申明好依赖的就行,增加删除都直接修改模块即可,在调用的时候也不用管该模块依赖了哪些其他模块。
模块化的发展历程
原始写法
- 只是把不同的函数简单地放在一起,就算一个模块;
function fun1(){
//...
}
function fun2(){
//...
}
//上面的函数fun1,fun2组成了一个模块,使用的时候直接调用某个函数就行了。
- 缺点:
- ”污染”了全局变量,无法保证不与其他模块发生变量名冲突;
- 模块成员之间看不出直接关系。
对象写法
- 为了解决污染全局变量的问题,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。
var module1 = new Object({
count : 0,
fun1 : function (){
//...
},
fun2 : function (){
//...
}
});
//这个里面的fun1和fun2都封装在一个赌侠宁里,可以通过对象.方法的形式进行调用;
module1.fun1();
- 优点:
- 减少了全局上的变量数目;
- 缺点:
- 本质是对象,而这个对象会暴露所有模块成员,内部状态可以被外部改写。
立即执行函数(IIFE)
- 避免暴露私有成员,所以使用立即执行函数(自调函数,IIFE);
- 作用: 数据是私有的, 外部只能通过暴露的方法操作
var module1 = (function(){
var count = 0;
var fun1 = function(){
//...
}
var fun2 = function(){
//...
}
//将想要暴露的内容放置到一个对象中,通过return返回到全局作用域。
return{
fun1:fun1,
fun2:fun2
}
})()
//这样的话只能在全局作用域中读到fun1和fun2,但是读不到变量count,也修改不了了。
//问题:当前这个模块依赖另一个模块怎么办?
IIFE的增强(引入依赖)
- 如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用”增强模式”;
- IIFE模式增强:引入依赖;
- 这就是现代模块实现的基石;
var module1 = (function (mod){
mod.fun3 = function () {
//...
};
return mod;
})(module1);
//为module1模块添加了一个新方法fun3(),然后返回新的module1模块。
//引入jquery到项目中;
var Module = (function($){
var _$body = $("body"); // we can use jQuery now!
var foo = function(){
console.log(_$body); // 特权方法
}
// Revelation Pattern
return {
foo: foo
}
})(jQuery)
Module.foo();
js模块化需要解决那些问题:
- 1.如何安全的包装一个模块的代码?(不污染模块外的任何代码)
- 2.如何唯一标识一个模块?
- 3.如何优雅的把模块的API暴漏出去?(不能增加全局变量)
- 4.如何方便的使用所依赖的模块?
模块化规范
CommonJS
- Node.js : 服务器端
Browserify : 浏览器端 也称为js的打包工具
基本语法:
定义暴露模块 : exports
exports.xxx = value module.exports = value
引入模块 : require
var module = require('模块名/模块相对路径')
引入模块发生在什么时候?
- Node : 运行时, 动态同步引入;
- Browserify : 在运行前对模块进行编译/转译/打包的处理(已经将依赖的模块包含进来了), 运行的是打包生成的js, 运行时不存在需要再从远程引入依赖模块;
AMD : 浏览器端
- require.js
- 基本语法
- 定义暴露模块:
define([依赖模块名], function(){return 模块对象})
- 引入模块:
require(['模块1', '模块2', '模块3'], function(m1, m2){//使用模块对象})
- 配置:
- 定义暴露模块:
- 基本语法
require.config({
//基本路径
baseUrl : 'js/',
//标识名称与路径的映射
paths : {
'模块1' : 'modules/模块1',
'模块2' : 'modules/模块2',
'angular' : 'libs/angular',
'angular-messages' : 'libs/angular-messages'
},
//非AMD的模块
shim : {
'angular' : {
exports : 'angular'
},
'angular-messages' : {
exports : 'angular-messages',
deps : ['angular']
}
}
})
CMD : 浏览器端
- sea.js
- 基本语法
- 定义暴露模块:
define(function(require, module, exports){
//通过require引入依赖模块
//通过module/exports来暴露模块
exports.xxx = value
})
- 使用模块seajs.use([‘模块1’, ‘模块2’])
- 定义暴露模块:
- 基本语法
- ES6
- ES6内置了模块化的实现
- 基本语法
- 定义暴露模块 : export
- 暴露一个对象:
- 默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型
export default 对象
- 暴露多个:
- 常规暴露,暴露的本质是对象,接收的时候只能以对象的解构赋值的方式来接收值
export var xxx = value1
export let yyy = value2
//
var xxx = value1
let yyy = value2
export {xxx, yyy}
- 引入使用模块 : import
- default模块:
import xxx from '模块路径/模块名'
- 其它模块
import {xxx, yyy} from '模块路径/模块名'
import * as module1 from '模块路径/模块名'
- default模块:
- 定义暴露模块 : export
- 问题: 所有浏览器还不能直接识别ES6模块化的语法
- 解决:
- 使用Babel将ES6—>ES5(使用了CommonJS) —-浏览器还不能直接执行;
- 使用Browserify—>打包处理—-浏览器可以运行
模块化方案 | 优点 | 缺点 |
---|---|---|
commonJS | 复用性强; 使用简单; 实现简单; |
有不少可以拿来即用的模块,生态不错; 同步加载不适合浏览器,浏览器的请求都是异步加载; 不能并行加载多个模块。 |
AMD | 异步加载适合浏览器 | 可并行加载多个模块; 模块定义方式不优雅,不符合标准模块化 |
ES6 | 可静态分析,提前编译 | 面向未来的标准; 浏览器原生兼容性差,所以一般都编译成ES5; 目前可以拿来即用的模块少,生态差 |
CommonJS通用的模块规范(同步)
- Modules/1.0规范包含内容:
- 1.模块的标识应遵循的规则(书写规范)
- 2.定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴露出来的API;
- 3.如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖;
- 4.如果引入模块失败,那么require函数应该报一个异常;
- 5.模块通过变量exports来向外暴露API,exports只能是一个对象,暴露的API须作为此对象的属性。
nodejs中的commonJS模块化教程
- 1.安装node.js;
2.创建项目结构
//结构如下 |-modules |-module1.js//待引入模块1 |-module2.js//待引入模块2 |-module3.js//待引入模块3 |-app.js//主模块 |-package.json { "name": "commonjsnode", "version": "1.0.0" }
3.下载第三方模块:uniq
npm i uniq --save
4.模块化编码
//module1 使用module.exports = value向外暴露一个对象
module.exports = {
foo(){
console.log('module1 foo()');
}
}
//module2 使用module.exports = value向外暴露一个函数
module.exports = function () {
console.log('module2()');
}
//module3 使用exports.xxx = value向外暴露一个对象
exports.foo = function () {
console.log('module3 foo()');
} ;
exports.bar = function () {
console.log('module3 bar()');
}
//app.js
/*
1. 定义暴露模块:
module.exports = value;
exports.xxx = value;
2. 引入模块:
var module = require(模块名或模块路径);
*/
var uniq = require('uniq');
//引用模块
let module1 = require('./modules/module1');
let module2 = require('./modules/module2');
let module3 = require('./modules/module3');
//使用模块
module1.foo();
module2();
module3.foo();
module3.bar();
- 5.通过node运行app.js
- 命令:node.app.js
- 工具:右键–>运行
浏览器中的commonJS规范
Browserify模块化使用教程
1.创建项目结构
|-js |-dist //打包生成文件的目录 |-src //源码所在的目录 |-module1.js |-module2.js |-module3.js |-app.js //应用主源文件 |-index.html |-package.json { "name": "browserify-test", "version": "1.0.0" }
2.下载browserify
- 全局: npm install browserify -g
- 局部: npm install browserify –save-dev
3.定义模块代码
module1.js
module.exports = { foo() { console.log('moudle1 foo()') } }
module2.js
module.exports = function () { console.log('module2()') }
module3.js
exports.foo = function () { console.log('module3 foo()') } exports.bar = function () { console.log('module3 bar()') }
app.js (应用的主js)
//引用模块 let module1 = require('./module1') let module2 = require('./module2') let module3 = require('./module3') let uniq = require('uniq') //使用模块 module1.foo() module2() module3.foo() module3.bar() console.log(uniq([1, 3, 1, 4, 3]))
打包处理js:
browserify js/src/app.js -o js/dist/bundle.js
- 页面使用引入:
<script type="text/javascript" src="js/dist/bundle.js"></script>
.AMD:异步模块定义规范(预加载)
- AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。
- 它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
- 第一个参数[module],是一个数组,里面的成员就是要加载的模块;
- 第二个参数callback,则是加载成功之后的回调函数。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
require.js介绍
- 优点
- 实现js文件的异步加载,避免网页失去响应;
- 管理模块之间的依赖性,便于代码的编写和维护。
require.js使用教程
- 1.下载require.js, 并引入
- 官网: http://www.requirejs.cn/
- github : https://github.com/requirejs/requirejs
- 将require.js导入项目: js/libs/require.js
- 2.创建项目结构
|-js
|-libs
|-require.js
|-modules
|-alerter.js
|-dataService.js
|-main.js
|-index.html
3.定义require.js的模块代码
- require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。
具体来说,就是模块必须采用特定的define()函数来定义;
如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
define(['myLib'], function(myLib){ function foo(){ myLib.doSomething(); } return {foo : foo}; }); //当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。
如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性;
dataService.js
define(function () { let msg = 'atguigu.com' function getMsg() { return msg.toUpperCase() } return {getMsg} })
alerter.js
define(['dataService', 'jquery'], function (dataService, $) { let name = 'Tom2' function showMsg() { $('body').css('background', 'gray') alert(dataService.getMsg() + ', ' + name) } return {showMsg} })
4.应用主(入口)js: main.js
- 使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。
(function () {
//配置
require.config({
//基本路径
baseUrl: "js/",
//模块标识名与模块路径映射
paths: { "alerter": "modules/alerter", "dataService": "modules/dataService", }
})
//引入使用模块
require( ['alerter'], function(alerter) {
alerter.showMsg()
})
})()
- 5.页面使用模块:
<script data-main="js/main" src="js/libs/require.js"></script>
6.使用第三方基于require.js的框架(jquery)
将jquery的库文件导入到项目:
js/libs/jquery-1.10.1.js
在main.js中配置jquery路径
paths: { 'jquery': 'libs/jquery-1.10.1' }
在alerter.js中使用jquery
define(['dataService', 'jquery'], function (dataService, \$) { var name = 'xfzhang' function showMsg() { $('body').css({background : 'red'}) alert(name + ' '+dataService.getMsg()) } return {showMsg} })
- 使用第三方不基于require.js的框架(angular)
- 将angular.js导入项目
- js/libs/angular.js
- 流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。这样的模块在用require()加载之前,要先用require.config()方法,定义它们的一些特征。
- require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。
- 具体来说,每个模块要定义(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。
- 在main.js中配置
(function () { //配置 require.config({ //基本路径 baseUrl: "js/", //模块标识名与模块路径映射 paths: { //第三方库作为模块 'jquery' : './libs/jquery-1.10.1', 'angular' : './libs/angular', //自定义模块 "alerter": "./modules/alerter", "dataService": "./modules/dataService" }, /* 配置不兼容AMD的模块 exports : 指定与相对应的模块名对应的模块对象 */ shim: { 'angular' : { exports : 'angular' } } }) //引入使用模块 require( ['alerter', 'angular'], function(alerter, angular) { alerter.showMsg() console.log(angular); }) })()
CMD
sea.js简单使用教程
-
- 下载sea.js, 并引入
- 官网: http://seajs.org/
- github : https://github.com/seajs/seajs
- 将sea.js导入项目: js/libs/sea.js
- 下载sea.js, 并引入
-
- 创建项目结构
|-js
|-libs
|-sea.js
|-modules
|-module1.js
|-module2.js
|-module3.js
|-module4.js
|-main.js
|-index.html
定义sea.js的模块代码
module1.js
define(function (require, exports, module) { //内部变量数据 var data = 'atguigu.com' //内部函数 function show() { console.log('module1 show() ' + data) } //向外暴露 exports.show = show })
module2.js
define(function (require, exports, module) { module.exports = { msg: 'I Will Back' } })
module3.js
define(function (require, exports, module) { const API_KEY = 'abc123' exports.API_KEY = API_KEY })
module4.js
define(function (require, exports, module) { //引入依赖模块(同步) var module2 = require('./module2') function show() { console.log('module4 show() ' + module2.msg) } exports.show = show //引入依赖模块(异步) require.async('./module3', function (m3) { console.log('异步引入依赖模块3 ' + m3.API_KEY) }) })
main.js : 主(入口)模块
define(function (require) { var m1 = require('./module1') var m4 = require('./module4') m1.show() m4.show() })
- index.html:
<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./js/modules/main')
</script>
ES6-Babel-Browserify
使用教程
- 1.定义package.json文件
{
"name" : "es6-babel-browserify",
"version" : "1.0.0"
}
- 2.安装babel-cli, babel-preset-es2015和browserify
* npm install babel-cli browserify -g
* npm install babel-preset-es2015 --save-dev
- 3.定义.babelrc文件
{
"presets": ["es2015"]
}
4.编码
js/src/module1.js
export function foo() { console.log('module1 foo()'); }; export let bar = function () { console.log('module1 bar()'); }; export const DATA_ARR = [1, 3, 5, 1];
js/src/module2.js
let data = 'module2 data'; function fun1() { console.log('module2 fun1() ' + data); }; function fun2() { console.log('module2 fun2() ' + data); }; export {fun1, fun2};
js/src/module3.js
export default { name: 'Tom', setName: function (name) { this.name = name } }
js/src/app.js
import {foo, bar} from './module1' import {DATA_ARR} from './module1' import {fun1, fun2} from './module2' import person from './module3' import $ from 'jquery' //引入完毕 $('body').css('background', 'red') foo() bar() console.log(DATA_ARR); fun1() fun2() person.setName('JACK') console.log(person.name);
5.编译
- 使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib
- 使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js
- 6.页面中引入测试
<script type="text/javascript" src="js/lib/bundle.js"></script>
7.引入第三方模块(jQuery)
- 1). 下载jQuery模块:
npm install jquery@1 --save
- 2). 在app.js中引入并使用
import $ from 'jquery' $('body').css('background', 'red')