模块化开发的意思是如下👇
将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起,
块的内部数据/实现是私有的(在每个模块内部属于私有作用域),只是向外部暴露一些接口(或方法)与外部其它模块通信。
不使用模块化开发的缺点:
1. 如果把所有的代码放在一个js文件当中,如果想要修改代码封装的函数或者某一部分内容的时候不利于查找。
2. 把js代码写在一个文件里,耦合度会变高(也就是代码之间的关联度太强了)不方便后期的维护。
3. 而且如果js代码都写在一个文件里,代码的功能点不明确,维护不利于查找。
4. 所有的js代码全部写在一个文件容易污染全局环境。
我们使用模块化开发主要目的
1. 降低代码的复制度,和耦合性。
2. 避免命名冲突(减少命名空间的污染)【因为每一个模块都是私有作用域】
3. 更好的分离,按需求加载
4. 提高了复用性,和可维护性
模块化三步走:
1. 创建一个模块,并抛出/导出/暴露(module.exports=模块)
2. 引入模块require(“路径”)
3. 使用模块
AMD ,CMD ,COMMINGJS问题
今天接到阿里的电话面试,面试中问到CommonJS AMD CMD这3种JS模块化开发标准的特点和区别。
当时问题回答的不太好,总结了一下,以免大家采坑。
一、CommonJS
CommonJS是NodeJS服务器端的概念 、采用同步加载文件的方式
实例说明:
目前大家项目中使用打包工具webpack就是通过node实现的功能,配置webpack.config.js中使用的module.exports 公布接口这就是CommonJS的标准。
1 const path = require('path');
2
3 module.exports = {
4 entry: './src/index.js',
5 output: {
6 path: path.resolve(__dirname, 'dist'),
7 filename: 'bundle.js'
8 }
9 };
服务器端的Node.js遵循CommonJS规范。核心思想是允许模块通过require 方法来同步加载所要依赖的其他模块,然后通过exports或module.exports来导出需要暴露的接口。
COMMINGJS 的说明或者特点
- 每一个文件都可以当成一个模块
- 在服务器端:模块的加载是运行时同步加载的
(所以会存在等待时间长的问题) - 在浏览器端:因为浏览器端不识 COMMINGJS当中的require语法所以我们要使用的时候模块需要提前编译打包处理
优点:
1. 服务器端便于重用(重复使用,只要暴露了模块,其他地方引入即可使用)
2. NPM中已经将近20w个模块包(模块的量大样式多)
3. 简单并容易使用
缺点:
1. 同步的模块方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
2. 不能非阻塞的并行加载多个模块
3. CommonJS是同步的,意味着你想调用模块里的方法,必须先用require加载模块。这对服务器端的Nodejs来说不是问题,因为模块的JS文件都在本地硬盘上,CPU的读取时间非常快,同步不是问题。但如果是浏览器环境,要从服务器加载模块。模块的加载将取决于网速,如果采用同步,网络情绪不稳定时,页面可能卡住,这就必须采用异步模式。所以,就有了 AMD解决方案。下一篇我们开始介绍模块化规范的AMD规范;
node中使用commonJs规范实现模块记载机制,在这个规范下每个.js文件都是一个模块,他们内部各自使用的变量名函数名互不冲突。
一个模块要想对外暴露变量(函数也是变量),可以用module.exports = variable;
,一个模块要想引用其他模块暴露的变量,用var ref = module.exports("module_name")
;就拿到可引用模块的变量。
深入解析模块原理:
其实要实现“模块”这个功能,并不需要语法层面的支持,node也没有增加任何javascript代码,核心思路就是利用JS的函数式编程(闭包)。
假设模块hello.js的代码为:
var s = 'hello';
var name = 'word';
console.log(s+','+name+'!');
Node.js加载hello模块之后,它可以把代码包装一下,变成这样:
function(){
//读取模块hello的代码
var s = 'hello';
var name = 'word';
console.log(s+','+name+'!');
}()
这样看来,每一个引进来的模块都拿一个匿名函数包裹,做到了模块隔离重用可维护,形成了各自的命名空间。也就是说每一个模块都是一个私有作用域。
★
★
★
CommonJS模块规范(CommonJS同步加载)
CommonJS模块规范主要分为三部分:模块引用、模块定义、模块标识。
模块引用
var http = require('http');
require函数的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。当我们用require()获取module时,Node会根据module.id找到对应的module,并返回module. exports,这样就实现了模块的输出。
require函数使用一个参数,参数值可以带有完整路径的模块的文件名,也可以为模块名。(只能填写绝对路径或者相对路径)
require("./module/b");
//相对路径
require("../node/module/c")
; //绝对路径
模块定义
模块(module)是什么
CommonJS规范规定,一个文件就是一个模块
用module变量代表当前模块。 Node在其内部提供一个Module的构建函数。所有模块都是Module的实例。实例代码如下:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = [];
}
module.exports = {Module};
var module = new Module(filename, parent);
每个模块内部,都有一个module对象,代表当前模块。它的属性如下:
• module.id 模块的识别符,通常是带有绝对路径的模块文件名。
• module.filename 模块的文件名,带有绝对路径。
• module.loaded 返回一个布尔值,表示模块是否已经完成加载。
• module.parent 返回一个对象,表示调用该模块的模块。
• module.children 返回一个数组,表示该模块要用到的其他模块。
• module.exports 初始值为一个空对象{},表示模块对外输出的接口。
exports 属性
exports 属性是module对象的一个属性,它向外提供接口。
如 2.1模块引用 中示例,a.js引入b.js作为一个模块,当在b.js中定义如下方法时:
//加法
function add(num1,num2){
console.log(num1+num2);
}
在a.js中调用却报错
var addMethod = require("./module/b");
//调用模块中的add方法
addMethod.add(1,6);
b.js中的函数要能被其他模块使用,就需要暴露一个对外的接口,exports 属性用于完成这一工作。b.js中的方法定义修改如下:
exports.add = function(num1,num2){
console.log(num1+num2);
}
在a.js中能够调用。
模块标识
模块标识就是传递给require方法的参数,必须符合小驼峰命名的字符串,或者以.、…开头的相对路径,或者绝对路径,默认文件名后缀.js。在Node实现中,正是基于这样一个标识符进行模块查找的,如果没有发现指定模块会报错。
根据参数的不同格式,require命令去不同路径寻找模块文件。加载规则如下:
(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,
require('/home/marco/foo.js')
将加载/home/marco/foo.js。
(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require(’./circle’)将加载当前脚本同一目录的circle.js。
(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
举例来说,脚本/home/user/projects/foo.js执行了require(‘bar.js’)命令,Node会依次搜索以下文件。
/usr/local/lib/node/bar.js
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
(4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如
require('example-module/path/to/file')
,则将先找到example-module的位置,然后再以它为参数,找到后续路径。
(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。
CommonJS是同步的,意味着你想调用模块里的方法,必须先用require加载模块。这对服务器端的Nodejs来说不是问题,因为模块的JS文件都在本地硬盘上,CPU的读取时间非常快,同步不是问题。但如果是浏览器环境,要从服务器加载模块。模块的加载将取决于网速,如果采用同步,网络情绪不稳定时,页面可能卡住,这就必须采用异步模式。所以,就有了 AMD解决方案。下一篇我们开始介绍模块化规范的AMD规范;
CommonJS是同步的,AMD是异步的。
AMD
AMD需要配合require.js模块一起使用(所以有时候别人问你懂不懂require.js就是问AMD的意思)
下載require.js地址https://requirejs.org/
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
我们使用的RequireJS就是AMD的实现框架。
AMD规范只有一个主要接口define(id,dependencies,factory),它要在声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到factory中,对于依赖的模块提前执行,依赖前置。
定义AMD接口规范
AMD当中的定义模块[]内的为可选参数,参数填下载的依赖,必须是数组的形式传递如下的形参,如果你在依赖参数中填写了,那么function的形参就是一一对应的关系,dep1就是d1,dep2就是d2
3 define("module", ["dep1", "dep2"], function(d1, d2) {
4
5 //定义一个模块dep1 dep2是依赖的模块 依赖的模块是前置加载的,
6
7 return someExportedValue;
8
9 });
暴露模块用return
return + 你想暴露的模块
例如
在dataService.js文件下
定义没有依赖的模块
define( function() {
let name="dataService.js";
function getName(){
return name;
}
//暴露模块
return {getName};
})
在alert.js文件下定义一个有依赖的模块
define( ["dataService"], function(dataService) {
let msg="alerter.js"
function showMsg(){
console.log(msg,dataService.getName())
}
//暴露模块
return {showMsg};
});
//暴露模块
return {showMsg};
});
主文件当中引入模块在main.js文件挡住写如下代码
(function(){
requirejs.config({
baseUrl:""//基本的路径用于和下面path对象的属性值拼接的
,
path:{
// 配置模块的路径
// 路径的属性值不可以写文件格式即不能写.js因为默认会给你加,如果我们自己手动加了就会报错
dataService:"./modules/dataService",
alerter:"./modules/alerter"
}
});
requirejs(["alerter"],function(alerter){
alerter.showMsg();
})
})()
Html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AMD</title>
</head>
<style>
.amd{
color: blue;
}
</style>
<body>
<div class="amd">AMD</div>
</body>
<!-- data-main="./main.js"找到主模块 -->
<script data-main="./main.js" src="./libs/require.js"></script>
</html>
//引用模块
2 (function(){
3 require(["dep1", "dep2"], function (d1, d2) {
4 //异步加载d1模块 ,在回调函数中调用
5 d1.add(2, 3);
6
7 });
})()
通常都会把引入模块放到自调用函数当中
requirejs(["dep1", "dep2"], function (d1, d2) {
4 //异步加载d1模块 ,在回调函数中调用
5 d1.add(2, 3);
7 });
6 require和requirejs是一样的引入方式。
CMD
CMD(Common Module Definition)模块定义规范 该规范的代表框架是SeaJS CMD 推崇依赖就近,AMD 推崇依赖前置 ,对比上面AMD的define 就一目了然看到二者的区别了
CMD是结合AMD和CMD规范→说名或者说特点:
- CMD专门用于浏览器端,模块的加载是异步的。
- 模块使用时才会加载执行(也就是我们什么时候用什么时候加载)。
CMD当中的定义模块如下语法👇
定义有依赖的模块
define( function(require, exports,module) {
// 引入依赖模块(同步) require("")参数填地址
var module2=require("")
// 引入依赖模块(异步)参数1填写地址
require.async("",function(){
})
// 暴露模块
module.exports= xxx
});
定义没有依赖的模块
define(function(require, exports,module) {
exports.xxx=value;
module.exports=value;
// 暴露模块
exports.xxx=value
});
引入模块
define(function(require) {
var m1=require("../定义模块的语法/定义没有依赖的模块")
var m4=require("../定义模块的语法/定义有依赖的模块")
m1.show()
m4.show()
});
使用的时候要引入sea.js文件(并用seajs.use(“模块地址的方式”)的方式使用)
最后总结:
ES6模块化规范
1. https://es6.ruanyifeng.com/#docs/module
2.ES6模块化需要编译打包处理。
一:es6语法:如下👇
1.导出/暴露模块:export
2.引入模块:import
**二:浏览器端实现需要操作如下👇
使用Babel将ES6编译为ES5代码
安装babel-cli,babel-preset-es2015和browserify
其中babel-cli当中的cli英文全称为command line interface:命令行接口**
1. npm install babel-cli browserify –g
2. npm install babel-preset –es2015 –save-dev
注意:在下载使用babel的时候要先下载如上的1和2才能使用babel(也就是babel是在browserify库的基础上去使用的)
3.preset预设(将es6转换成es5的所有插件打包)
使用Browserify编译打包js
在和package.json同级下创建文件名为 .babelrc 的文件注意.babelrc前面有个点.不写点会失效
然后再.babel文件内部配置如下代码rc的全称run control表示运行时控制文件
{
"presets": ["es2015",]
}
暴露模块
// 模块暴露之分别暴露
export function foo(){
console.log("foo() module1");
}
export function bar(){
console.log("bar() module1");
}
export let arr=[1,2,3,4,5,6];
// 分别暴露可以暴露多次,多个不同的数据
// 暴露模块之合并统一暴露(就是把你想暴露的内容统一整合为对象然后暴露出去)
function fun(){
console.log("1")
}
function fun1(){
console.log("2")
}
export {fun,fun1}
// 默认暴露
// 默认暴露可以暴露任意数据类型,暴露什么数据我们就接收什么数据
export default ()=>{
console.log("我是默认暴露的箭头函数")
}
//注意: 默认暴露每个模块只能暴露一次
编译操作
编译
使用babel将ES6编译为ES5代码(但是包含CommonJS语法):babel js/src -d js/lib
中间一个-d ,-d的左边为你要编译的文件夹东西,-d的右边为你编译成功后把文件放在那里
但是使用babel编译完之后有require语法浏览器无法识别所以我们要用Browserify在编译一次,但是Browserify编译的时候需要注意的是没有创建新文件夹的功能所以我们需要自己创建文件夹先
如果不创建文件夹就会报错报如下错误
使用Browserify编译js:browserify js/lib/app.js -o js/lib/bundle.js
中间一个-o ,-o的左边为你要编译的文件夹东西,-o的右边为你编译成功后把文件放在那里
编译完之后引入主文件就可以直接使用了。
Node.js的特点如下👇(主要作用就是产接口)
1. 非阻塞式的i/o模型(其中i指的是input输入,o指的是output表示输出)
Node.js当中通常通过异步回调将阻塞变为非阻塞
异步回调就是模块中的回调函数
“node.js当中本来文件创建是一个阻塞的i/o模型,是什么原因可以让其变成非阻塞呢?”
“我们通过写回调函数就会让其变成非阻塞的i/o模型!”
一般情况下i/o操作都是阻塞的。(ajax请求,文件操作,数据库操作…….
(网络请求(分为同步和异步)例如→通常情况下ajax请求为同步阻塞的))
2. 事件驱动
3. Node.js适合处理高迸发操作
模块的分类
1. 内置模块:node中自己携带的模块,可以放心直接使用。
2. 第三方模块:先下载模块在使用的模块。
3. 自定义模块:开发者按自己的需求自己定义的模块。
模块开发三步走
1创建模块,并且暴露/抛出/导出模块,语法module.exports=模块
2引入模块 require(“路径”)
3使用模块
错误的捕获
同步的错误:
try{}catch(e){console.log(e)}
异步的错误:
异步的捕获错误,错误的回调优先,在异步的模块使用时
回调函数中第一个就是错误信息的形参(这就是错误的回调优先的意思)在回调函数中如果打印错误为null则表示异步的模块程序执行时没有错误‘’
fs.mkdir("./fs",function(err){
console.log("创建加创建完毕!")
console.log(err)
})
;