模块化
将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
为什么要有模块化?
- 避免命名冲突,减少命名空间污染
- 降低复杂度
- 提高解耦性,按需加载,部署方便,一个项目中不需要的模块js代码不需要引入
- 更高的复用性
- 高可维护性(理想)
模块化的缺点
- 请求过多(1个文件拆成10个)
- 依赖模糊
- 难以维护(互相依赖,引入js的顺序不能更换,否则会报错)
模块的组成
- 数据--->内部的属性
- 操作数据的行为--->内部的函数
模块化的进化过程
- 全局function模式 :
// module.js
let msg = ''
function foo () {
console.log(msg)
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>01_全局function模式</title>
</head>
<body>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
msg = '1'
foo()
</script>
</body>
</html>
编码: 全局变量/函数,缺点:
- 耦合度高,关联性强,不方便维护
- 功能点不明确
- 污染全局命名空间, 容易引起命名冲突/数据不安全,。
- namespace模式 :
let obj = {
msg:'module2',
foo() {
console.log(this.msg)
}
}
<script type="text/javascript">
obj.foo();
obj.msg = 'NBA'
obj.foo();//NBA
</script>
编码: 将数据/行为封装到对象中,解决了命名冲突(减少了全局变量),缺点:数据不安全(外部可以直接修改模块内部的数据)
- IIFE模式:立即调用函数表达式--->匿名函数自调用
(function(window){
let msg = 'module3'
function foo (){
console.log(msg)
}
window.myModule={foo}
})(window)
<script type="text/javascript">
msg = '';
console.log(msg);//''相当于在window添加一个属性msg=''
foo();//不能直接调用
myModule.foo()
//myModule.otherFun() //myModule.otherFun is not a function
console.log(myModule.data) //undefined 不能访问模块内部数据
myModule.data = 'xxxx' //不是修改的模块内部的data
myModule.foo() //没有改变
</script>
编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口,函数是js唯一的local scope。
- IIFE增强模式:
(function(window,$){
let msg = 'module4'
function foo (){
console.log(msg)
$('body').css('background', 'red')
}
window.module4={foo};
})(window,jQuery)
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module4.js"></script>
<script type="text/javascript">
module4();
</script>
引入依赖: 通过函数形参来引入依赖模块。这就是模块模式,也是现代模块实现的基石。
模块化规范
CommonJS
规范说明
- 每个文件都可以当作一个模块;
- 在服务器端:模块加载时运行时同步加载的;【同步带来的后果是等待造成堵塞】
- 在浏览器端:模块需要提前编译打包处理;运行的是打包生成的js, 运行时不存在需要再从远程引入依赖模块
基本语法
- 暴露模块:
exports.xxx = value(任意数据类型)
module.exports = value
暴露的本质都是exports对象
- 引入模块:
var module = require('模块名/模块相对路径')
// 模块名为第三方模块,模块相对路径为自定义模块
实现
- Node.js : 服务器端
- 下载安装node.js
- 创建项目结构
|-modules
|-module1.js
|-module2.js
|-module3.js
|-app.js
|-package.json // 当前文件夹的终端输入npm init可以自动创建
{
"name": "commonJS-node",
"version": "1.0.0"
}
- 下载第三方模块
npm install uniq --save // 在package添加依赖
- 模块化编码
// 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()')
}
exports.arr = [2,3,1,1,2,3];
// app.js
"use strict";
// 第三方模块引入要放在前面
let uniq = require('uniq')
//引用模块
let module1 = require('./modules/module1')
let module2 = require('./modules/module2')
let module3 = require('./modules/module3')
let fs = require('fs')
//使用模块
module1.foo()
module2()
module3.foo()
module3.bar()
console.log(uniq(module3.arr))
fs.readFile('app.js', function (error, data) {
console.log(data.toString())
})
- 通过node运行app.js 命令: node app.js / 工具: 右键-->运行
- Browserify : 浏览器端,也称为js的打包工具(ES6也用)
- 创建项目结构
|-js
|-dist //打包生成文件的目录
|-src //源码所在的目录
|-module1.js
|-module2.js
|-module3.js
|-app.js //应用主源文件
|-index.html // 不同于服务端是用Node运行app.js,浏览器端必须用html才能运行文件
|-package.json // npm init / 手动创建
{
"name": "browserify-test",
"version": "1.0.0"
}
- 下载browserify 全局: npm install browserify -g and 局部: npm install browserify --save-dev(当前包只在开发环境运行)【browserify要求两个都要执行】
// package.json
{
"name": "browserify-test",
"version": "1.0.0",
"devDependencies":{
"browserify": "^14.5.0" // 开发依赖(开发调试)
}
"dependencies:"{
"uniq":"^1.0.1" // 生产环境依赖(线上)
}
}
- 定义模块代码
// 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 uniq = require('uniq')
//引用模块
let module1 = require('./module1')
let module2 = require('./module2')
let module3 = require('./module3')
//使用模块
module1.foo()
module2()
module3.foo()
module3.bar()
console.log(uniq([1, 3, 1, 4, 3]))
- 打包处理js:
browserify js/src/app.js -o js/dist/build.js
- 页面使用引入:
<script type="text/javascript" src="js/dist/build.js"></script>
AMD
规范说明
专门用于浏览器端,模块加载时异步的
基本语法
- 暴露模块
- 定义没有依赖的模块
define(function(){
return 模块
})
- 定义有依赖的模块
define(['module1','module2'],function(m1,m2){
return 模块
})
- 引入模块
require(['module1','module2'],function(m1,m2){
使用m1/m2
})
实现
- NoAMD
- 创建项目结构
|-js
|-alert.js
|-dataService.js
|-app.js
|-test.html
- 定义模块代码
// dataService.js
(function (window) {
let msg = 'atguigu.com'
function getMsg() {
return msg.toUpperCase()
}
window.dataService = {getMsg}
})(window)
// alert.js
(function (window, dataService) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
window.alerter = {showMsg}
})(window, dataService)
// app.js
(function (alerter) {
alerter.showMsg()
})(alerter)
// test.html
<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>
// 请求多,依赖关系难维护
- AMD
- 下载require.js / 第三方模块, 将require.js / 第三方模块导入项目: js/libs/require.js and js/libs/jquery-1.10.1.js 官网: www.requirejs.cn/
- 创建项目结构
|-js
|-libs
|-jquery-1.10.1.js
|-angular.js
|-require.js
|-modules
|-alerter.js
|-dataService.js
|-main.js
|-index.html
- 定义模块代码
// 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}
})
// main.js
(function () {
//配置
requirejs.config({
//基本路径
baseUrl: "js/",//如果未设置,就是基于main.js
//模块标识名与模块路径映射,这样在模块互相引用时才能找的到
paths: {
"alerter": "modules/alerter", // or "./modules/alerter"
"dataService": "modules/dataService",
//库模块
'jquery': 'libs/jquery-1.10.1',
'angular': 'libs/angular'
}
})
//配置不兼容AMD的模块
shim: {
angular: {
exports: 'angular'
}
}
})
//引入模块使用
require(['alerter', 'angular'], function (alerter, angular) {
alerter.showMsg()
console.log(angular);
})
<script data-main="js/main.js" src="js/libs/require.js"></script>
CMD
规范说明
专门用于浏览器端,模块加载时异步的,模块使用时才会加载
基本语法
- 暴露模块
- 定义没有依赖的模块
define(function(require,exports,module){
exports.xxx = value;
module.exports = value
})
- 定义有依赖的模块
define(function(require,exports,module){
// 引入依赖模块(同步)
var module2 = require('./module2);
// 引入依赖模块(异步)
require.async('./module3',function(m3){
})
exports.xxx = value;
module.exports = value
})
- 引入模块
define(function(require){
var m1 = require('./module1')
var m1 = require('./module4')
m1.show();
m4.show();
})
实现
-
下载sea.js, 并将sea.js导入项目: js/libs/sea.js 官网: seajs.org/,github : github.com/seajs/seajs
-
创建项目结构
|-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
说明
依赖模块需要编译打包处理
语法
- 暴露:export
- 引入:import
实现(浏览器端)
- 使用Babel将ES6转为ES5
- 使用Browserify编译打包JS
ES6-Babel-Browserify使用教程
- 创建项目结构
|-js
|src
|-module1.js
|-module2.js
|-module3.js
|-main.js
|-index.html
|-.babelrc(不要写json)
|-package.json
- 定义package.json文件
{
"name" : "es6-babel-browserify",
"version" : "1.0.0"
}
- 安装babel-cli, babel-preset-es2015和browserify
npm install babel-cli browserify -g
npm install babel-preset-es2015 --save-dev
- 定义.babelrc文件 rc = run control运行时需要控制的文件
{
"presets": ["es2015"] //"presets": ["es2015", "react"] 告诉babel要转换es6和jsx的语法
}
- 编码
- 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);
- 编译
- 使用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>
- 引入第三方模块(jQuery) 1). 下载jQuery模块:
npm install jquery@1 --save 2). 在app.js中引入并使用
import $ from 'jquery'
$('body').css('background', 'red')
模块化规范大总结
| CommonJS | AMD | CMD | ES6 | |||
|---|---|---|---|---|---|---|
| 引用模块 | require | require | require | import | ||
| 暴露接口 | module.exports | exports | define函数返回值 return | exports | export | |
| 加载方式 | 运行时加载,同步加载 | 并行加载,提前执行,异步加载 | 并行加载,按需执行,异步加载 | 编译时加载,异步加载 | ||
| 实现模块规范 | NodeJS | RequireJS | SeaJS | 原生JS | ||
| 适用 | 服务器 | 浏览器 | 浏览器 | 服务器/浏览器 |
| CommonJS | AMD | CMD | ES6 | |||
|---|---|---|---|---|---|---|
| 引用模块 | require | require | require | import | ||
| 暴露接口 | module.exports | exports | define函数返回值 return | exports | export | |
| 加载方式 | 运行时加载,同步加载 | 并行加载,提前执行,异步加载 | 并行加载,按需执行,异步加载 | 编译时加载,异步加载 | ||
| 实现模块规范 | NodeJS | RequireJS | SeaJS | 原生JS | ||
| 适用 | 服务器 | 浏览器 | 浏览器 | 服务器/浏览器 |
CommonJs 和 ES6规范的区别
- 写法上
const fs = require('fs')
exports.fs = fs
module.exports = fs
import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'
export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'
- 输入值
require输入的变量,基本类型数据是赋值,引用类型为浅拷贝,可修改import输入的变量都是只读的,如果输入 a 是一个对象,允许改写对象属性
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作
- 执行顺序
require:不具有提升效果,到底加载哪一个模块,只有运行时才知道
const path = './' + fileName;
const myModual = require(path);
import:具有提升效果,会提升到整个模块的头部,首先执行。import的执行早于foo的调用。本质就是import命令是编译阶段执行的,在代码运行之前。
foo();
import { foo } from 'my_module';
import()函数,支持动态加载模块,其是运行时加载,还有运行到那一块,才会加载模块,可用于按需加载、条件加载、动态的模块路径等。
// 按需加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then({export1, export2} => { // export1和export2都是dialogBox.js的输出接口,解构获得
// do something...
})
.catch(error => {})
});
// 条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
// 动态的模块路径
import(f()).then(...); // 根据函数f的返回结果,加载不同的模块。
- 使用的表达式和变量
require可以使用表达式和变量
let a = require('./a.js')
a.add()
let b = require('./b.js')
b.getSum()
import静态执行,不能使用表达式和变量,因为这些都是只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
本质区别
1、浏览器在不做任何处理时,默认是不支持import和require;
2、babel会将ES6模块规范转化成Commonjs规范;
3、webpack、gulp以及其他构建工具会对Commonjs进行处理,使之支持浏览器环境browserify。有三个重大差异:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。 其中,导致第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
- CommonJS:
运行时加载只能在运行时确定模块的依赖关系,以及输入和输出的变量,一个模块就是一个对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
- ES6 :
编译时加载或者静态加载 ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。可以在编译时就完成模块加载,引用时只加载需要的方法,其他方法不加载。效率要比 CommonJS 模块的加载方式高。
import { stat, exists, readFile } from 'fs';
开发环境依赖 vs 生产环境依赖
- 前端普通项目:package.json里面的dependencies(--save)和devDependencies(--save-dev)里面含有的包没啥区别,即使都放在devDependencies都没有关系。
- 因为实际上部署的时候部署的页面也只是webpack打包生产出来的产品(打包出的产物html css js跟是什么依赖无关的,webpack打包是从入口文件开始分析完代码中所有的依赖后开始打的),所以依赖包放在dependencies和devDependencies没有关系。
- 将所有依赖包放入devDependencies可能会破坏某些在服务器上进行初始build的部署脚本。因此,将所有依赖包放到dependencies中会更容易些。【有些自动构建工程build脚本会加上--production的参数,这个时候不会拉devDependencies的包,导致build流程异常】
-
Node应用服务,因为这时dependencies和devDependencies实际上是作为运行时(runtime)部署的,所以要尽量严格区分dependencies和devDependencies,项目本身就是运行态,如果部署上线不会希望把开发态的一些工具,例如测试框架等,带到线上。
-
开发npm包,若开发A,A中依赖了B,如果B在dependencies的话就会一起被下载到A的node_modules,如果是开发的话,就不会被下载。因此,如果是一些比如webpack等开发依赖,可以放到devDependencies,这样就能够减少他人使用A时下载的包大小。