模块化的理解
什么是模块?
将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起;
块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信;
一个模块的组成
数据--->内部的属性;
操作数据的行为--->内部的函数;
模块化是指解决一个复杂的问题时自顶向下把系统划分成若干模块的过程,有多种属性,分别反映其内部特性;
模块化编码:编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目;
非模块化的问题
页面加载多个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>
发生问题:
难以维护 ;
依赖模糊;
请求过多;
所以,这些问题可以通过现代模块化编码和项目构建来解决;
模块化的优点
更好地分离:避免一个页面中放置多个script标签,而只需加载一个需要的整体模块即可,这样对于HTML和JavaScript分离很有好处;
更好的代码组织方式:有利于后期更好的维护代码;
按需加载:提高使用性能,和下载速度,按需求加载需要的模块
避免命名冲突:JavaScript本身是没有命名空间,经常会有命名冲突,模块化就能使模块内的任何形式的命名都不会再和其他模块有冲突。
更好的依赖处理:使用模块化,只需要在模块内部申明好依赖的就行,增加删除都直接修改模块即可,在调用的时候也不用管该模块依赖了哪些其他模块。
模块化的发展历程
原始写法
只是把不同的函数简单地放在一起,就算一个模块;
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"); var foo = function(){ console.log(_$body); // 特权方法 } // Revelation Pattern return { foo: foo } })(jQuery) Module.foo();
js模块化需要解决那些问题:
1.如何安全的包装一个模块的代码?(不污染模块外的任何代码)
2.如何唯一标识一个模块?
3.如何优雅的把模块的API暴漏出去?(不能增加全局变量)
4.如何方便的使用所依赖的模块?
模块化规范
Node: 服务器端
Browserify : 浏览器端
CommonJS:服务器端
概述
Node 应用由模块组成,采用 CommonJS 模块规范。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
特点
所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
-
基本语法:
exports.xxx = value // 通过module.exports指定暴露的对象value module.exports = value
var module = require('模块相对路径')
引入模块 : require
定义暴露模块 : exports
引入模块发生在什么时候?
Node:运行时, 动态同步引入;
Browserify:在运行前对模块进行编译/转译/打包的处理(已经将依赖的模块包含进来了), 运行的是打包生成的js, 运行时不需要再从远程引入依赖模块;
CommonJS通用的模块规范(同步)
Node内部提供一个Module构建函数。所有模块都是Module的实例。
每个模块内部,都有一个module对象,代表当前模块。
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
Node为每个模块提供一个exports变量,指向module.exports。
如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。
Modules/1.0规范包含内容:
模块的标识应遵循的规则(书写规范)
定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为模块暴露出来的API;
如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖;
如果引入模块失败,那么require函数应该报一个异常;
模块通过变量exports来向外暴露API,exports赋值暴露的只能是一个对象
exports = {Obj}
,暴露的API须作为此对象的属性。exports本质是引入了module.exports的对象。不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。如果暴露的不是变量exports,而是module.exports。module变量代表当前模块,这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
exports=module.exports={Obj}
node中的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.下载第三方模块:举例express
npm i express --save
4.模块化编码
// module1 // 使用module.exports = value向外暴露一个对象 module.exports = { name: 'this is module1', 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()'); }; exports.name = 'this is module3' //app.js文件 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(); module3.name;
5.通过node运行app.js
命令:node.app.js
工具:右键-->运行
浏览器中的commonJS教程
借助Browserify
-
步骤
|-js |-dist //打包生成文件的目录 |-src //源码所在的目录 |-module1.js |-module2.js |-module3.js |-app.js //应用主源文件 |-index.html //浏览器上的页面 |-package.json { "name": "browserify-test", "version": "1.0.0" }
<script type="text/javascript" src="js/dist/bundle.js"></script>
全局: npm install browserify -g
局部: npm install browserify --save-dev
下载browserify
定义模块代码:index.html文件要运行在浏览器上,需要借助browserify将app.js文件打包编译,如果直接在index.html引入app.js就会报错。
打包处理js:根目录下运行
browserify js/src/app.js -o js/dist/bundle.js
页面使用引入:
创建项目结构
AMD : 浏览器端
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
AMD规范则是非同步加载模块,允许指定回调函数,可以实现异步加载依赖模块,并且会提前加载;
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。
如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
语法
AMD规范基本语法
定义暴露模块:
define([依赖模块名], function(){return 模块对象})
引入模块:
require(['模块1', '模块2', '模块3'], function(m1, m2){//使用模块对象})
兼容CommonJS规范的输出模块
define(function (require, exports, module) { var reqModule = require("./someModule"); requModule.test(); exports.asplode = function () { //someing } });
AMD:异步模块定义规范(预加载)
AMD规范:github.com/amdjs/amdjs…
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。
它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
-
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;
第二个参数callback,则是加载成功之后的回调函数。
目前,主要有两个Javascript库实现了AMD规范:RequireJS和curl.js。
RequireJS
优点
实现js文件的异步加载,避免网页失去响应;
管理模块之间的依赖性,便于代码的编写和维护。
require.js使用教程
下载require.js, 并引入
官网: requirejs.org/
github : github.com/requirejs/r…
将require.js导入项目:
js/libs/require.js
创建项目结构
|-js |-libs |-require.js // 引入的require.js |-modules |-alerter.js |-dataService.js |-main.js |-index.html
-
定义require.js的模块代码
define(['myLib'], function(myLib){ function foo(){ myLib.doSomething(); } // 暴露模块 return {foo : foo}; }); //当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。
- 如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性; ``` // dataService.js define(function () { let msg = 'this is dataService' 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} }) ```
如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。
具体来说,就是模块必须采用特定的define()函数来定义;
应用主(入口)js: main.js
(function () { //配置 require.config({ //基本路径 baseUrl: "js/", //模块标识名与模块路径映射 paths: { "alerter": "modules/alerter",//此处不能写成alerter.js,会报错 "dataService": "modules/dataService", } }) //引入使用模块 require( ['alerter'], function(alerter) { alerter.showMsg() }) })()
使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块main.js的头部,参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。
页面使用模块:
<script data-main="js/main" src="js/libs/require.js"></script>
定义模块
require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。
具体来说,每个模块要定义:
1、exports值(输出的变量名),表明这个模块外部调用时的名称;
2、deps数组,表明该模块的依赖性。
支持的配置项:
设置path时起始位置是相对于baseUrl的,除非该path设置以"/"开头或含有URL协议(如http:)。
用于模块名的path不应含有.js后缀,因为一个path有可能映射到一个目录。路径解析机制会自动在映射模块名到path时添加上.js后缀。在文本模版之类的场景中使用require.toUrl()时它也会添加合适的后缀。
在浏览器中运行时,可指定路径的备选(fallbacks),以实现诸如首先指定了从CDN中加载,一旦CDN加载失败则从本地位置中加载这类的机制;
当加载纯.js文件(依赖字串以/开头,或者以.js结尾,或者含有协议),不会使用baseUrl。
如未显式设置baseUrl,则默认值是加载require.js的HTML所处的位置。如果用了data-main属性,则该路径就变成baseUrl。
baseUrl可跟require.js页面处于不同的域下,RequireJS脚本的加载是跨域的。唯一的限制是使用text! plugins加载文本内容时,这些路径应跟页面同域,至少在开发时应这样。优化工具会将text! plugin资源内联,因此在使用优化工具之后你可以使用跨域引用text! plugin资源的那些资源。
baseUrl :所有模块的查找根路径。
paths:path映射那些不直接放置于baseUrl下的模块名。
shim: 为那些没有使用define()来声明依赖关系、设置模块的"浏览器全局变量注入"型脚本做依赖和导出配置。
使用第三方基于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()方法,定义它们的一些特征。
// 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 : 浏览器端
CMD规范:github.com/seajs/seajs…
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。
CMD规范整合了CommonJS和AMD规范的特点。
在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范
基本语法
-
定义暴露模块:
// 没有依赖的模块 define(function(require, module, exports){ let value = 'xxx'; //通过require引入依赖模块 //通过module.exports/exports来暴露模块 exports.xxx = value module.exports = value }) // 有依赖的模块 define(function(require, exports, module){ //引入依赖模块(同步) var module2 = require('./module2') //引入依赖模块(异步) require.async('./module3', function (m3) { ...... }) //暴露模块 exports.xxx = value })
使用模块seajs.use(['模块1', '模块2'])
sea.js简单使用教程
-
下载sea.js, 并引入
define() exports module.exports
如何依赖模块:
require()
如何使用模块:
seajs.use()
官网: seajs.org/
github : github.com/seajs/seajs
将sea.js导入项目:
js/libs/sea.js
如何定义导出模块 :
创建项目结构
|-js |-libs |-sea.js |-modules |-module1.js |-module2.js |-module3.js |-module4.js |-main.js |-index.html
-
定义sea.js的模块代码
define(function (require, exports, module) { //内部变量数据 var data = 'this is module1' //内部函数 function show() { console.log('module1 show() ' + data) } //向外暴露 exports.show = show })
define(function (require, exports, module) { module.exports = { msg: 'I Will Back' } })
define(function (require, exports, module) { const API_KEY = 'abc123' exports.API_KEY = API_KEY })
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) }) })
define(function (require) { var m1 = require('./module1') var m4 = require('./module4') m1.show() m4.show() })
main.js : 主(入口)模块
module4.js
module3.js
module2.js
module1.js
index.html:
<script type="text/javascript" src="js/libs/sea.js"></script> <script type="text/javascript"> seajs.use('./js/modules/main') </script>
ES6模块化
模块化的规范:CommonJS和AMD两种。前者用于服务器,后者用于浏览器。
而ES6 中提供了简单的模块系统,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
基本用法
-
es6 中新增了两个命令 export 和 import ;
// math.js export const add = function (a, b) { return a + b } export const subtract = function (a, b) { return a - b }
// main.js import { add, subtract } from './math.js' add(1, 2) substract(3, 2)
使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。
export 命令用于规定模块的对外接口;
import 命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。
如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
下面是一个JS文件,里面使用export命令输出变量。
定义暴露模块 : export
常规暴露,暴露的本质是对象,接收的时候只能以对象的解构赋值的方式来接收值
默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型
-
暴露一个对象:
export default 对象
-
暴露多个:
export var xxx = value1 export let yyy = value2 // 暴露一个对象 var xxx = value1 let yyy = value2 export {xxx, yyy}
-
引入使用模块 : import
import xxx from '模块路径/模块名'
import {xxx, yyy} from '模块路径/模块名' import * as module1 from '模块路径/模块名'
其它模块
default模块:
export 详细用法
export不止可以导出函数,还可以导出,对象、类、字符串等等;
-
暴露多个:
export const obj = {test1: ''} export const test = '' export class Test { constuctor() { } } // 或者,直接在暴露的地方定义导出函数或者变量 export let foo = ()=>{ console.log('fnFoo');return "foo" },bar="stringBar"
let a=1 let b=2 let c=3 export { a,b,c }
// test.js let a = 1 let b = 2 let c = 3 export { a as test, b, c }; import { test, b, c } from './test.js' // 改变命名后只能写 as 后的命名
// test.js let a = 1 let b = 2 let c = 3 export { a as test, b, c }; // lib.js引入test.js的内容 export * from './test.js' // 引入 import {test,b,c} from './lib.js'
通过通配符暴露其他引入的模块
还可以通过as改变输出名称
一起暴露,推荐使用这种写法,这样可以写在脚本尾部,一眼就看清楚输出了哪些变量。
分别暴露
暴露一个对象,默认暴露
// test.js export default function () { console.log('hello world') } //引入 import say from './test.js' // 这里可以指定任意变量名 say() // hello world
import $ from 'jQuery' // 加载jQuery 库 import _ from 'lodash' // 加载 lodash import moment from 'moment' // 加载 moment
常用的模块
export default指定默认输出,import无需知道变量名就可以直接使用
import详细用法
import 为加载模块的命令,基础使用方式和之前一样
// main.js import { add, subtract } from './test' // 对于export default 导出的 import say from './test'
通过 as 命令修改导入的变量名
import {add as sum, subtract} from './test' sum (1, 2)
加载模块的全部,除了指定输出变量名或者 export.default 定义的导入, 还可以通过 * 号加载模块的全部。
// math.js export const add = function (a, b) { return a + b } export const subtract = function (a, b) { return a - b } //引入 import * as math from './test.js' math.add(1, 2) math.subtract(1, 2)
ES6-Babel-Browserify使用教程
问题: 所有浏览器还不能直接识别ES6模块化的语法
解决:
使用Babel将ES6--->ES5(使用了CommonJS) ----浏览器还不能直接执行;
使用Browserify--->打包处理js----浏览器可以运行
定义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文件,这是一个babel的设置文件
{ "presets": ["es2015"] }
编码
// 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)
npm install jquery@1 --save
import $ from 'jquery' $('body').css('background', 'red')
2). 在app.js中引入并使用
1). 下载jQuery模块:
总结
模块化方案 | 优点 | 缺点 |
---|---|---|
commonJS | 复用性强; 使用简单; 实现简单; |
有不少可以拿来即用的模块,生态不错; 同步加载不适合浏览器,浏览器的请求都是异步加载; 不能并行加载多个模块。 |
AMD | 异步加载适合浏览器 | 可并行加载多个模块; 模块定义方式不优雅,不符合标准模块化 |
ES6 | 可静态分析,提前编译 | 面向未来的标准; 浏览器原生兼容性差,所以一般都编译成ES5; 目前可以拿来即用的模块少,生态差 |
AMD和CMD区别:
对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。
不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
-
CMD 推崇依赖就近,AMD 推崇依赖前置。
// CMD define(function(require, exports, module) { var a = require('./a'); a.doSomething() // 此处略去 100 行 var b = require('./b') // 依赖可以就近书写 b.doSomething() // ... }) // AMD 默认推荐的是 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() ...} )
虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。
AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
还有一些细节差异,具体看这个规范的定义就好,就不多说了。
作者:木头房子
链接:https://juejin.im/post/5cb004da5188251b130c773e