前端模块化机制

# 模块化的理解
# 模块的定义
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起
- 块的内部数据与实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信
# 模块化的进化过程
全局
function模式:将不同的功能封装成不同的全局函数编码:将不同的功能封装成不同的全局函数
问题:污染全局命名空间,容易引起命名冲突或数据不安全,而且模块成员间看不出直接关系
function m1() { // ... } function m2() { // ... }1
2
3
4
5
6
namespace 模式:简单对象封装
作用:减少了全局变量,解决命名冲突
问题:数据不安全(外部可以直接修改模块内部的数据)
let myModule = { data: 'www.baidu.com', foo() { console.log(`foo() ${this.data}`) } bar() { console.log(`bar() ${this.data}`) } } myModule.data = 'other data' // 能直接修改模块内部的数据 myModule.foo() // foo() other data1
2
3
4
5
6
7
8
9
10
11
IIFE 模式:匿名函数自调用(闭包)
作用:数据是私有的,外部只能通过暴露的方法操作
编码:将数据和行为封装到一个函数内部,通过给 window 添加属性来向外暴露接口
问题: 如果当前这个模块依赖另一模块怎么办
// module.js文件 (function (window) { let data = "www.baidu.com"; //操作数据的函数 function foo() { //用于暴露有函数 console.log(`foo() ${data}`); } function bar() { //用于暴露有函数 console.log(`bar() ${data}`); otherFun(); //内部调用 } function otherFun() { //内部私有的函数 console.log("otherFun()"); } //暴露行为 window.myModule = { foo, bar }; //ES6写法 })(window);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<!-- index.html文件 --> <script type="text/javascript" src="module.js"></script> <script type="text/javascript"> myModule.foo(); myModule.bar(); console.log(myModule.data); // undefined 不能访问模块内部数据 myModule.data = "xxx"; // 不能修改模块内部的data myModule.foo(); // 没有改变 </script>1
2
3
4
5
6
7
8
9
IIFE 模式增强:引入依赖 这就是现代模块实现的基石,保证模块的独立性,还使得模块间的依赖关系变得明显
<!-- index.html文件 --> <!-- 引入的js必须有一定顺序 --> <script type="text/javascript" src="jquery-1.10.1.js"></script> <script type="text/javascript" src="module.js"></script> <script type="text/javascript"> myModule.foo(); </script>1
2
3
4
5
6
7// module.js文件 (function (window, $) { let data = "www.baidu.com"; //操作数据的函数 function foo() { //用于暴露有函数 console.log(`foo() ${data}`); $("body").css("background", "red"); } function bar() { //用于暴露有函数 console.log(`bar() ${data}`); otherFun(); //内部调用 } function otherFun() { //内部私有的函数 console.log("otherFun()"); } //暴露行为 window.myModule = { foo, bar }; })(window, jQuery);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 模块化的好处
- 避免命名冲突(减少命名空间污染)
- 更好的分离,按需加载
- 更高复用性
- 高可维护性
# 引入多个<script>后出现问题
- 请求过多
- 依赖模糊(依赖关系导致加载先后顺序出错)
- 难以维护
# 模块化规范
# CommonJS
# 概述
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里定义的变量、函数、类都是私有的,对其他文件不可见。在服务端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理
# 特点
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块看多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,需清除缓存
- 模块加载的顺序,按照其在代码中出现的顺序
# 基本语法
- 暴露模块:
module.exports = value或exports.xxx = value - 引入模块:
require(xxx),如果是第三方模块,xxx 是模块名;如果为自定义模块,xxx 为模块文件路径
CommonJS 暴露的模块到底是什么?
CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
2
3
4
5
6
7
var example = require("./example.js"); //如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6
2
3
require命令
- 读入并执行一个 js 文件,返回该模块的 exports 对象
- 如果没发现指定模块,会报错
# 模块的加载机制
CommonJS 模块的加载机制,输入的是被输出的值的拷贝。一旦输出一个值,模块内部的变化就影响不到这个值 这点与 ES6 模块化有重大差异
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
2
3
4
5
6
7
8
9
// main.js
var counter = require("./lib").counter;
var incCounter = require("./lib").incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
2
3
4
5
6
7
上面代码说明,counter 输出后,lib.js 模块内部的变化影响不到 counter。这是因为 counter 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值
# 服务端实现
下载安装 node.js
创建项目结构
<!-- 注意:用npm init 自动生成package.json时,package name(包名)不能有中文和大写 --> |-modules |-module1.js |-module2.js |-module3.js |-app.js |-package.json { "name": "commonJS-node", "version": "1.0.0" }1
2
3
4
5
6
7
8
9
10
11下载第三方模块
npm install uniq --save // 用于数组去重定义模块代码
//module1.js module.exports = { msg: "module1", foo() { console.log(this.msg); }, };1
2
3
4
5
6
7//module2.js module.exports = function () { console.log("module2"); };1
2
3
4//module3.js exports.foo = function () { console.log("foo() module3"); }; exports.arr = [1, 2, 3, 3, 2];1
2
3
4
5// app.js文件 // 引入第三方库,应该放置在最前面 let uniq = require("uniq"); let module1 = require("./modules/module1"); let module2 = require("./modules/module2"); let module3 = require("./modules/module3"); module1.foo(); //module1 module2(); //module2 module3.foo(); //foo() module3 console.log(uniq(module3.arr)); //[ 1, 2, 3 ]1
2
3
4
5
6
7
8
9
10通过 node 运行 app.js 命令行输入
node app.js,运行 JS 文件
# 浏览器端实现(借助 Browserify)
创建项目结构
|-js |-dist //打包生成文件的目录 |-src //源码所在的目录 |-module1.js |-module2.js |-module3.js |-app.js //应用主源文件 |-index.html //运行于浏览器上 |-package.json { "name": "browserify-test", "version": "1.0.0" }1
2
3
4
5
6
7
8
9
10
11
12
13下载 browserify
- 全局:
npm install browserify -g - 局部:
npm install browserify --save-dev
- 全局:
定义模块代码(同服务端) 注意:
index.html文件要运行在浏览器上,需借助 browserify 将app.js文件打包编译,如果直接在index.html引入app.js会报错打包处理 js 根目录下运行
browserify js/src/app.js -o js/dist/bundle.js页面使用引入 在
index.html文件中引入<script type="text/javascript" src="js/dist/bundle.js"></script>
# AMD
CommonJS 规范加载模块是同步的,即只有加载完成,才能执行后面的操作。 AMD 规范是非同步加载模块,允许指定回调函数 Node.js 主要用于服务器编程,模块文件一般都已存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,用 CommonJS 比较适用 如果是浏览器环境,要从服务器端加载模块,必须采用非同步模式,因此浏览器端一般采用 AMD AMD 比 CommonJS 规范在浏览器端实现的早
# 基本语法
定义暴露模块
//定义没有依赖的模块 define(function () { return 模块; });1
2
3
4//定义有依赖的模块 define(["module1", "module2"], function (m1, m2) { return 模块; });1
2
3
4引入使用模块
require(["module1", "module2"], function (m1, m2) { 使用m1 / m2; });1
2
3
# 未使用 AMD 规范与使用require.js
- 未使用 AMD 规范
- 缺点:会发送多个请求,引入 js 文件的顺序不能错,否则会报错
// dataService.js文件 (function (window) { let msg = "www.baidu.com"; function getMsg() { return msg.toUpperCase(); } window.dataService = { getMsg }; })(window);1
2
3
4
5
6
7
8// alerter.js文件 (function (window, dataService) { let name = "Tom"; function showMsg() { alert(dataService.getMsg() + ", " + name); } window.alerter = { showMsg }; })(window, dataService);1
2
3
4
5
6
7
8// main.js文件 (function (alerter) { alerter.showMsg(); })(alerter);1
2
3
4// index.html文件 <div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div> <script type="text/javascript" src="js/modules/dataService.js"></script> <script type="text/javascript" src="js/modules/alerter.js"></script> <script type="text/javascript" src="js/main.js"></script>1
2
3
4
5 - 使用
require.js - Require.js 是一个工具库,主要用于客户端的模块管理
- 模块管理遵循 AMD 规范
- 通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载
# AMD 在浏览器实现
下载 require.js,并引入
将
require.js导入项目:js/libs/require.js创建项目结构
|-js |-libs |-require.js |-modules |-alerter.js |-dataService.js |-main.js |-index.html1
2
3
4
5
6
7
8定义
require.js的模块代码// dataService.js文件 // 定义没有依赖的模块 define(function () { let msg = "www.baidu.com"; function getMsg() { return msg.toUpperCase(); } return { getMsg }; // 暴露模块 });1
2
3
4
5
6
7
8
9//alerter.js文件 // 定义有依赖的模块 define(["dataService"], function (dataService) { let name = "Tom"; function showMsg() { alert(dataService.getMsg() + ", " + name); } // 暴露模块 return { showMsg }; });1
2
3
4
5
6
7
8
9
10// main.js文件 (function () { require.config({ baseUrl: "js/", //基本路径 出发点在根目录下 paths: { //映射: 模块标识名: 路径 alerter: "./modules/alerter", //此处不能写成alerter.js,会报错 dataService: "./modules/dataService", }, }); require(["alerter"], function (alerter) { alerter.showMsg(); }); })();1
2
3
4
5
6
7
8
9
10
11
12
13
14// index.html文件 <!DOCTYPE html> <html> <head> <title>Modular Demo</title> </head> <body> <!-- 引入require.js并指定js主文件的入口 --> <script data-main="js/main" src="js/libs/require.js"></script> </body> </html>1
2
3
4
5
6
7
8
9
10
11- 页面引入
require.js模块 在index.html引入<script data-main="js/main" src="js/libs/require.js"></script>// alerter.js文件 define(["dataService", "jquery"], function (dataService, $) { let name = "Tom"; function showMsg() { alert(dataService.getMsg() + ", " + name); } $("body").css("background", "green"); // 暴露模块 return { showMsg }; });1
2
3
4
5
6
7
8
9
10// main.js文件 (function () { require.config({ baseUrl: "js/", //基本路径 出发点在根目录下 paths: { //自定义模块 alerter: "./modules/alerter", //此处不能写成alerter.js,会报错 dataService: "./modules/dataService", // 第三方库模块 jquery: "./libs/jquery-1.10.1", //注意:写成jQuery会报错 }, }); require(["alerter"], function (alerter) { alerter.showMsg(); }); })();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 页面引入
# CMD
CMD 专门用于浏览器,模块的加载是异步的,模块使用时才会执行。 CMD 整合了 CommonJs 和 AMD 的特点 在 Sea.js 中,所有 js 模块都遵循 CMD 模块定义规范
# 基本语法
定义暴露模块
//定义没有依赖的模块 define(function (require, exports, module) { exports.xxx = value; module.exports = value; });1
2
3
4
5//定义有依赖的模块 define(function (require, exports, module) { //引入依赖模块(同步) var module2 = require("./module2"); //引入依赖模块(异步) require.async("./module3", function (m3) {}); //暴露模块 exports.xxx = value; });1
2
3
4
5
6
7
8
9引入使用模块
define(function (require) { var m1 = require("./module1"); var m4 = require("./module4"); m1.show(); m4.show(); });1
2
3
4
5
6
# sea.js 简单使用教程
下载 sea.js,并引入
- 官网 (opens new window)
- 将 sea.js 导入项目:
js/libs/sea.js
创建项目结构
|-js |-libs |-sea.js |-modules |-module1.js |-module2.js |-module3.js |-module4.js |-main.js |-index.html1
2
3
4
5
6
7
8
9
10定义 sea.js 的模块代码
// module1.js文件 define(function (require, exports, module) { //内部变量数据 var data = "atguigu.com"; //内部函数 function show() { console.log("module1 show() " + data); } //向外暴露 exports.show = show; });1
2
3
4
5
6
7
8
9
10
11// module2.js文件 define(function (require, exports, module) { module.exports = { msg: "I Will Back", }; });1
2
3
4
5
6// module3.js文件 define(function (require, exports, module) { const API_KEY = "abc123"; exports.API_KEY = API_KEY; });1
2
3
4
5// 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); }); });1
2
3
4
5
6
7
8
9
10
11
12
13// main.js文件 define(function (require) { var m1 = require("./module1"); var m4 = require("./module4"); m1.show(); m4.show(); });1
2
3
4
5
6
7在 index.html 引入
<script type="text/javascript" src="js/libs/sea.js"></script> <script type="text/javascript"> seajs.use("./js/modules/main"); </script>1
2
3
4结果如下
# ES6 模块化
ES6 模块的设计思想是尽量的静态化,使得编译时就确定模块的依赖关系,已经输入和输出的变量 CommonJS 和 AMD 都只能在运行时确定这些东西,如:CommonJS 模块就是对象,输入时必须查找对象属性
# ES6 模块化语法
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from "./math";
function test(ele) {
ele.textContent = add(99 + basicNum);
}
2
3
4
5
6
7
8
9
10
11
// export-default.js
export default function () {
console.log("foo");
}
2
3
4
// import-default.js
import customName from "./export-default";
customName(); // 'foo'
2
3
模块默认输出,其他模块加载模块时,import命令可为该匿名函数指定任意名字
# ES6 模块与 CommonJS 模块的差异
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS 模块时运行时加载,ES6 模块时编译时输出接口
CommonJS 加载的是一个对象(module.exports 属性),该对象只有在脚本运行完才能生成;ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
ES6 模块时动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块
// lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from "./lib"; console.log(counter); // 3 incCounter(); console.log(counter); // 41
2
3
4
5
6
7
8
9
10
# ES6-Babel-Browserify
使用 Babel 将 ES6 编译为 ES5 代码,使用 Browserify 编译打包 js
定义
package.json文件{ "name" : "es6-babel-browserify", "version" : "1.0.0" }1
2
3
4安装
babel-cli, babel-preset-es2015和browserifynpm install babel-cli browserify -gnpm install babel-preset-es2015 --save-dev- preset 预设(将 es6 转换成 es5 的所有插件打包)
定义
.babelrc文件{ "presets": ["es2015"] }1
2
3定义模块代码
//module1.js文件 // 分别暴露 export function foo() { console.log("foo() module1"); } export function bar() { console.log("bar() module1"); }1
2
3
4
5
6
7
8//module2.js文件 // 统一暴露 function fun1() { console.log("fun1() module2"); } function fun2() { console.log("fun2() module2"); } export { fun1, fun2 };1
2
3
4
5
6
7
8
9//module3.js文件 // 默认暴露 可以暴露任意数据类项,暴露什么数据,接收到就是什么数据 export default () => { console.log("默认暴露"); };1
2
3
4
5// app.js文件 import { foo, bar } from "./module1"; import { fun1, fun2 } from "./module2"; import module3 from "./module3"; foo(); bar(); fun1(); fun2(); module3();1
2
3
4
5
6
7
8
9//app.js文件 import { foo, bar } from "./module1"; import { fun1, fun2 } from "./module2"; import module3 from "./module3"; import $ from "jquery"; foo(); bar(); fun1(); fun2(); module3(); $("body").css("background", "green");1
2
3
4
5
6
7
8
9
10
11
12编译并在
index.html中引入- 使用 Babel 将 ES6 编译为 ES5 代码(但包含 CommonJS 语法) :
babel js/src -d js/lib - 使用 Browserify 编译 js :
browserify js/lib/app.js -o js/lib/bundle.js <script type="text/javascript" src="js/lib/bundle.js"></script>
- 使用 Babel 将 ES6 编译为 ES5 代码(但包含 CommonJS 语法) :
# 总结
- CommonJS 规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了 AMD CMD 解决方案。
- AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD 规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
- CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在 Node.js 中运行。不过,依赖 SPM 打包,模块的加载逻辑偏重
- ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。