模块化开发

模块化开发目录

  • 为什么要模块化开发
  • 模块化演变过程(演示)
    • 文件划分方式
    • 命名空间方式
    • IIFE
    • IIFE 模式增强(引入依赖)
  • 模块化规范
  • Node.js
    • 安装与配置(演示)
    • 语法(演示)
  • common JS
    • common js特点:
    • common JS四个变量:
    • common js举例(演示)
    • 缓存规则
    • 拓展
  • AMD
  • CMD
  • require js
    • 引入多个script文件后出现出现问题
    • 下载
    • 主模块(演示)
  • 提高代码质量

推荐博客
30分钟学会前端模块化开发
common js规范

为什么要模块化开发

不同于java,JavaScript在早期的设计中,就没有模块、包、类的概念,所以需要开发者模拟出类似的功能,来隔离、阻止复杂的JavaScript代码,这个过程被称之为模块化。
模块化开发是当下最重要的前端开发范式之一
模块就是一个实现特定功能的文件,在开发的时候,需要什么功能,便将这个功能引入进来。

模块化开发的四点好处:

  1. 避免变量污染,命名冲突
  2. 提高代码复用率
  3. 提高了可维护性
  4. 方便依赖关系管理

模块化演变过程(演示)

为了实现模块化开发,经历了几个历程:

文件划分方式

又有人叫他全局function模式 ,即将不同的功能封装成不同的全局函数。其实就是函数封装。

将每个功能以及它相关的一些状态数据,单独存放到不同的文件当中,约定每一个文件就是一个独立的模块。使用这个模块,就将这个模块引入到页面中,然后直接调用模块中的成员(变量/ 函数)。
一个script标签就对应一个模块,所有模块都在全局范围内工作。

模块化开发_第1张图片
模块化开发_第2张图片

模块化开发_第3张图片

在以上的例子里,index.html文件同时引用了module1.js和module2.js两个文件,但是函数foo1命名冲突了。于是后引用的module2.js的foo1函数会覆盖掉module1.js里的foo1函数。
模块化开发_第4张图片

控制台里显示如图。

缺点:

污染全局作用域
命名冲突问题
无法管理模块依赖关系
早期模块化完全依靠约定。

命名空间方式

又叫namespace模式,即对象封装。

具体做法: 我们约定每个模块只暴露一个全局的对象,我们所有的模块成员都挂载到这个全局对象下面。
在第一阶段的基础上,通过将每个模块「包裹」为一个全局对象的形式实现,有点类似于为模块内的成员添加了「命名空间」的感觉。

module1.foo1();
module2.foo1();

在module1.js中

var module1 ={
    var1:1,
    foo1:function(){
    console.log('module1.js的foo1函数,var1为'+module1.var1)
}
}

var module2 ={
    var1:100,
    foo1:function(){
    console.log('module2.js的foo1函数,var1为'+module2.var1)
}
}

模块化开发_第5张图片
但是这样的缺点很明显,只要加上一行

module1.var1 = 5000;

控制台里就会变成
模块化开发_第6张图片

缺点:

没有私有空间
模块成员仍然可以在外部被访问/修改
无法管理模块依赖关系

IIFE

即匿名函数自调用(闭包)。

使用立即执行函数的方式,去为我们的模块提供私有空间。将模块中每个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员,我们可以通过挂载到全局对象上的这种方式去实现。确保了私有成员的安全。
有了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问。

我的理解是,写一个立即执行函数,形成一个私有空间,把想要访问的成员添加到全局对象window中暴露,以闭包的形式访问函数。没有暴露的成员,不能访问和修改。

例如在module1.js中写

(function(){
    var var1 = 1;
    function foo1(){
        console.log('module1.js的foo1函数,var1为'+var1)
    }
    window.module1={
        foo1,
    }
})()

在module2.js中写

(function(){
    var var1 = 1;
    function foo1(){
        console.log('module2.js的foo1函数,var1为'+var1)
    }
    window.module2={
        foo1,
    }
})()

此时因为将foo1暴露了,就可以调用了。
在这里插入图片描述
但是因为我没有把var1这个值暴露,所以无法修改,即使加上一行:

module1.var1 = 5000;

结果也没有改变。

IIFE 模式增强(引入依赖)

把jquery库当作参数传入,这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

我的理解就是用jquery。
方式类似:

(function(window, $) {
    function foo1() {
        var var1 = 1;
        console.log('module1.js的foo1函数,var1为'+var1)
    }
    window.module1 = {
        foo1,
    }
  })(window, jQuery)

模块化规范

常见的的JavaScript模块规范有:CommonJS、AMD、CMD、UMD、原生模块化。
在讲解这些之前,还要了解一个软件:Node.js。

Node.js

是一个基于Chrome V8引擎的JavaScript运行环境,使用了一个事件驱动、非阻塞式I/O模型, 让JavaScript 运行在服务端的开发平台,它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。
Node.js对一些特殊用例进行优化,提供替代的API,使得V8在非浏览器环境下运行得更好,V8引擎执行Javascript的速度非常快,性能非常好,基于Chrome JavaScript运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。

node.js
官方介绍

安装与配置(演示)

Node.js安装教程
Node.js配置

Node.jsAPI文档
配置过程经过整理,大致是以下步骤:

  1. npm init //初始化文件夹
  2. npm install -g express //下载express
  3. npm install -g express-generator//express4.x版本中将命令工具分出来了,需要再安装一个命令工具。
  4. express myapp//测试express是否成功,myapp是我自己起的名字
  5. 将依赖模块拷贝到myapp的根目录,可以手动拷贝:将C:\Users\Administrator\AppData\Roaming\npm\node_modules下面的express目录拷贝到应用根目录。也可以执行命令:myapp 文件夹中npm install
    这一步是从网上看到的,实际上我不执行这一步,也能成功
  6. npm install jade//安装模板引擎
  7. npm install mysql
  8. 启动myapp,默认url:http://localhost:3000
    myqpp文件夹中npm start或者node app.js
  9. 在浏览器访问 http://localhost:3000,显示欢迎页面

模块化开发_第7张图片

  1. npm install browserify -g//下载browserify
  2. 编写app.js
  3. browserify myapp/app.js -o myapp/bundle.js//把app.js打包编译。app.js无法直接被网页引用,需要编译成bundle.js才能引用。
  4. 在自制的网页中引用
  5. 再次在myapp中 npm start

npm install browserify -g开发依赖
npm install browserify --save-dev运行依赖
默认监听的端口号是3000,想要更改端口号的话,在myapp/bin文件夹里,将文件WWW重命名为WWW.txt打开,在第十五行修改,将3000改成你喜欢的端口号:
模块化开发_第8张图片
随后再将WWW.txt改回WWW,重启服务器就可以了。

语法(演示)

Node中模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。
我们在app.js里编写的,是文件模块。
nodejs是区别于javascript的,在javascript中的顶层对象是window,而在node中的顶层对象是global
实际上,javascript也存在global对象,只是其并不对外访问,而使用window对象指向global对象而已。
区别演示:

var a = 100;
window.a=200;
console.log(a);
console.log(window.a);

在这里插入图片描述

在node.js中写

var a = 100;
global.a=200;
console.log(a);
console.log(global.a);

在这里插入图片描述
在nodejs中,一个文件就是一个模块,每个模块都有自己的作用域。使用var来声明的一个变量,它并不是全局的,而是属于当前模块下。
所以a≠global.a

common JS

common js通常运行在Node.js上。
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

common js特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

common JS四个变量:

module
exports
require
global
CommonJS规范规定:
每个模块内部,module变量代表当前模块
这个变量是一个对象,它的exports属性(即module.exports)是对外的接口
加载某个模块,其实是加载该模块的module.exports属性。

module.exports.x = 5;

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
一个例子。想要加载相对路径下的lib文件:

var counter = require('./lib');

(require命令会自动省略掉.js的后缀,Node会先查找是否存在没有后缀的该文件,如果没有,再按.js、.json、.node的次序补足扩展名,依次尝试)
根据命令不同,require加载的路径也不同。

  • 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。
  • 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径的模块文件。
  • 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块,或者一个位于各级node_modules目录的已安装模块。
  • 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require(‘example-module/path/to/file’),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
  • 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。
  • 如果想得到require命令加载的确切文件名,使用require.resolve()方法。
    所以如果是当前路径下的文件模块,一定要以./开头,否则nodejs会试图去加载核心模块,或node_modules内的模块

common js举例(演示)

app.js

let module1 = require('./modules/module1')
let module2 = require('./modules/module2')

module1.foo1();
module2();
console.log(module1.msg)

module1.js代码

module.exports={
  msg:"这是暴露的信息",
  foo1(){
    console.log(this.msg);
  }
}

module2.js代码

module.exports = function(){
  console.log("直接暴露一个函数");
}

结果

模块化开发_第9张图片

缓存规则

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

var module1 = require('./modules/module1');
console.log(module1.msg);
//加载一次,并将msg打印出来
module1.msg="hello";
console.log(module1.msg);
//修改msg重新打印一次
require('./modules/module1');
console.log(module1.msg);
//重新加载一次再打印

在这里插入图片描述
结果是,即使最后重新加载了一次模块,msg仍然是缓存中的hello。

所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

拓展

一些没讲到的

  • 目录加载规则
  • require.main
  • 模块的循环加载
  • require的内部处理流程

个人认为这些都是没那么重要、或比较进阶的东西,不详细说了。

AMD

AMD((Asynchromous Module Definition) 异步模块定义。AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出。
它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

define(name, dependencies, factory);
  • name是模块的名字,它是可选的参数。
  • dependencies 指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入 factory 中。如果没有指定 dependencies,那么它的默认值是 [“require”, “exports”, “module”]。
  • factory 包裹了模块的具体实现,它是一个函数或者对象。如果是函数,那么它的返回值就是模块的输出接口或值。
// module1
define('module1', function() {
	// ...
	return {
		add: function() {
			return ...;
		}
		// ...
	}
});
 
// module2
define('module2',['module1'] function(module1) {
	// 使用module1模块中的方法
	module1.add();
	// ...
	return {
		// ...
	}
});

AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。

require(['math'], function (math) {
  math.add(2, 3);
  }

math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。

还可以吧jquery当做模块传过去:

define('myModule', ['jquery'], function($) {
    // $ 是 jquery 模块的输出
    $('body').text('hello world');
});
// 使用
require(['myModule'], function(myModule) {});

AMD允许兼容commonJS的规范。

define(function (require, exports, module) {

var reqModule = require("./someModule");
requModule.test();

exports.asplode = function () {
//someing
}
});

相关博客AMD规范

CMD

CMD和AMD的区别
模块化开发_第10张图片
CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同

CMD推崇依赖就近,所以一般不在define的参数中写依赖

define(function(require,exports,module)
{}
)
//CMD
define(function(require,exports,module){
  var a=require('./a')
  a.doSomethimg()
  var b=require('./b')
  b.doSomething()
  })

//AMD
define(['./a','./b'],function(a,b){
  a.doSomething()
  b.dosomething()
  })

require js

RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范。

引入多个script文件后出现出现问题

开发时经常会遇到这种情况:

 <script src="1.js"></script>
 <script src="2.js"></script>
 <script src="3.js"></script>
 <script src="4.js"></script>
 <script src="5.js"></script>
 <script src="6.js"></script>

引入的js文件过多,会导致很多问题:

  • 请求过多
    首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多。加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长。

  • 依赖模糊
    我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错
    比如jquery的文件必须放在最前面引用。

以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。

require.js的诞生,就是为了解决这两个问题。
模块化开发_第11张图片

  1. 实现js文件的异步加载,避免网页失去响应;

  2. 管理模块之间的依赖性,便于代码的编写和维护。

下载

官网下载链接
下载require.js文件后,在网页中引用。
加载这个文件,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:

<script src="js/require.js" defer async="true" ></script>

async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。
加载require.js以后,下一步就要加载我们自己的代码了。我的代码名为main.js。

<script src="js/require.js" defer async="true" data-main="js/main"></script>

data-main属性的作用是,指定网页程序的主模块。main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。

主模块(演示)

main.js,我把它称为"主模块",意思是整个网页的入口代码。

require(['module1', 'module2'], function (module1, module2) {
        console.log(module1.msg1);
        console.log(module2.msg2);

    });

配置:

require.config({
        paths: {
            module1:"./modules/module1",
            module2:"./modules/module2"
        }
    });

在paths中,为依赖的模块指定路径。(不用加.js后缀)
在module1.js里的写法:

define(function() {
	msg1 = 1;
	return {
		msg1,
	}
});

module2.js

define(function() {
	msg2 = 2;
	return {
		msg2,
	}

requirejs

提高代码质量

怎么提高代码质量?-来自Google的研发经验总结
如何提高JavaScript代码质量(一)
如何提高JavaScript代码质量(二)

你可能感兴趣的:(前端学习,javascript,html,node.js)