Web/11-Node.js/04-Node.js模块化规范:CommonJS.md

710 lines
17 KiB
JavaScript
Raw Normal View History

---
title: 04-Node.js模块化规范CommonJS
publish: true
---
<ArticleTopAd></ArticleTopAd>
## 前言
网站越来越复杂js代码js文件也越来越多会遇到**一些问题**
- 文件依赖
- 全局污染命名冲突
程序模块化包括
- 日期模块
- 数学计算模块
- 日志模块
- 登陆认证模块
- 报表展示模块等
所有这些模块共同组成了程序软件系统
一次编写多次使用才是提高效率的核心
## 模块化的理解
### 什么是模块化
**概念**将一个复杂的程序依据一定的规则规范封装成几个块文件并组合在一起
模块的内部数据实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
最早的时候我们会把所有的代码都写在一个js文件里那么耦合性会很高关联性强不利于维护而且会造成全局污染很容易命名冲突
### 模块化的好处
- 避免命名冲突减少命名空间污染
- 降低耦合性更好地分离按需加载
- **高复用性**代码方便重用别人开发的模块直接拿过来就可以使用不需要重复开发类似的功能
- **高可维护性**软件的声明周期中最长的阶段其实并不是开发阶段而是维护阶段需求变更比较频繁使用模块化的开发方式更容易维护
- 部署方便
## 模块化规范
### 模块化规范的引入
假设我们引入模块化首先可能会想到的思路是在一个文件中引入多个js文件如下
```html
<body>
<script src="zepto.js"></script>
<script src="fastClick.js"></script>
<script src="util/login.js"></script>
<script src="util/base.js"></script>
<script src="util/city.js"></script>
</body>
```
但是这样做会带来很多问题
- 请求过多引入十个js文件就有十次http请求
- 依赖模糊不同的js文件可能会相互依赖如果改其中的一个文件另外一个文件可能会报错
以上两点最终导致**难以维护**
于是这就引入了模块化规范
### 模块化的概念解读
模块化起源于 Node.jsNode.js 中把很多 js 打包成 package需要的时候直接通过 require 的方式进行调用CommonJS这就是模块化的方式
那如何把这种模块化思维应用到前端来呢这就产生了两种伟大的 jsRequireJS SeaJS
### 模块化规范
服务器端规范
- [**CommonJS规范**](http://www.commonjs.org/):是 Node.js 使用的模块化规范。
CommonJS 就是一套约定标准不是技术用于约定我们的代码应该是怎样的一种结构
浏览器端规范
- [**AMD规范**](https://github.com/amdjs/amdjs-api):是 **[RequireJS](http://requirejs.org/)** 在推广过程中对模块化定义的规范化产出。
```
- 异步加载模块
- 依赖前置提前执行require([`foo`,`bar`],function(foo,bar){}); //也就是说把所有的包都 require 成功,再继续执行代码。
- define 定义模块define([`require`,`foo`],function(){return});
```
- **[CMD规范]()** **[SeaJS](http://seajs.org/)** 在推广过程中对模块化定义的规范化产出。淘宝团队开发。
```
同步加载模块
依赖就近延迟执行require(./a) 直接引入或者Require.async 异步引入 //依赖就近:执行到这一部分的时候,再去加载对应的文件。
define 定义模块 export 导出define(function(require, export, module){});
```
PS面试时经常会问AMD CMD 的区别
另外还有ES6规范import & export
这篇文章我们来讲一下`CommonJS`它是 Node.js 使用的模块化规范
## CommonJS 的基本语法
### CommonJS 的介绍
[CommonJS](http://www.commonjs.org/):是 Node.js 使用的模块化规范。也就是说Node.js 就是基于 CommonJS 这种模块化规范来编写的。
CommonJS 规范规定每个模块内部module 变量代表当前模块这个变量是一个对象它的 exports 属性 module.exports是对外的接口对象加载某个模块其实是加载该模块的 module.exports 对象
CommonJS 每个文件都可以当作一个模块
- 在服务器端模块的加载是运行时同步加载的
- 在浏览器端: 模块需要提前编译打包处理首先既然同步的很容易引起阻塞其次浏览器不认识`require`语法因此需要提前编译打包
### 模块的暴露和引入
Node.js 中只有模块级作用域两个模块之间的变量方法默认是互不冲突互不影响这样就导致一个问题模块 A 要怎样使用模块B中的变量&方法呢这就需要通过 `exports` 关键字来实现
Node.js中每个模块都有一个 exports 接口对象我们可以把公共的变量方法挂载到这个接口对象中其他的模块才可以使用
接下来详细讲一讲模块的暴露模块的引入
### 暴露模块的方式一 exports
`exports`对象用来导出当前模块的公共方法或属性别的模块通过 require 函数调用当前模块时得到的就是当前模块的 exports 对象
**语法格式**
```js
// 相当于是:给 exports 对象添加属性
exports.xxx = value
```
这个 value 可以是任意的数据类型
**注意**暴露的关键词是`exports`不是`export`其实这里的 exports 类似于 ES6 中的 export 的用法都是用来导出一个指定名字的对象
**代码举例**
```js
const name = 'qianguyihao';
const foo = function (value) {
return value * 2;
};
exports.name = name;
exports.foo = foo;
```
### 暴露模块的方式二 module.exports
`module.exports`用来导出一个默认对象没有指定对象名
语法格式
```javascript
// 方式一:导出整个 exports 对象
module.exports = value;
// 方式二:给 exports 对象添加属性
module.exports.xxx = value;
```
这个 value 可以是任意的数据类型
代码举例
```js
// 方式1
module.exports = {
name: '我是 module1',
foo(){
console.log(this.name);
}
}
// 我们不能再继续写 module.exports = value2。因为重新赋值会把 exports 对象 之前的赋值覆盖掉。
// 方式2
const age = 28;
module.exports.age = age;
```
`module.exports` 还可以修改模块的原始导出对象比如当前模块原本导出的是一个对象我们可以通过 module.exports 修改为导出一个函数如下
```js
module.exports = function () {
console.log('hello world')
}
```
### exports module.exports 的区别
最重要的区别
- 使用exports时只能单个设置属性 `exports.a = a;`
- 使用module.exports时既单个设置属性 `module.exports.a`也可以整个赋值 `module.exports = obj`
其他要点
- Node中每个模块的最后都会执行 `return: module.exports`
- Node中每个模块都会把 `module.exports`指向的对象赋值给一个变量 `exports`也就是说 `exports = module.exports`
- `module.exports = XXX`表示当前模块导出一个单一成员结果就是XXX
- 如果需要导出多个成员则必须使用 `exports.add = XXX; exports.foo = XXX`或者使用 `module.exports.add = XXX; module.export.foo = XXX`
### 问题: 暴露的模块到底是谁
**答案**暴露的本质是`exports`对象重要
比如方式一的 `exports.a = a` 可以理解成是** exports 对象添加属性**方式二的 `module.exports = a`可以理解成是给整个 exports 对象赋值方式二的 `module.exports.c = c`可以理解成是给 exports 对象添加属性
Node.js 中每个模块都有一个 module 对象module 对象中的有一个 exports 属性称之为**接口对象**我们需要把模块之间公共的方法或属性挂载在这个接口对象中方便其他的模块使用
### 引入模块的方式require
require函数用来在一个模块中引入另外一个模块传入模块名返回模块导出对象
**语法格式**
```js
const module1 = require('模块名');
```
解释
- 内置模块require的是**包名**
- 下载的第三方模块require的是**包名**
- 自定义模块require的是**文件路径**文件路径既可以用绝对路径也可以用相对路径后缀名`.js`可以省略
**代码举例**
```js
const module1 = require('./main.js');
const module2 = require('./main');
const module3 = require('Demo/src/main.js');
```
**require()函数的两个作用**
- 执行导入的模块中的代码
- 返回导入模块中的接口对象
### 主模块
主模块是整个程序执行的入口可以调度其他模块
```bash
# 运行main.js启动程序此时main.js就是主模块
$ node main.js
```
### 模块的初始化
一个模块中的 JS 代码仅在模块**第一次被使用时**执行一次并且在使用的过程中进行初始化然后会被缓存起来便于后续继续使用
代码举例
1calModule.js:
```js
var a = 1;
function add () {
return ++a;
}
exports.add = add;
```
2main.js main.js 中引入 hello.js 模块
```js
var addModule1 = require('./calModule')
var addModule2 = require('./calModule')
console.log(addModule1.add());
console.log(addModule2.add());
```
在命令行执行 `node main.js` 运行程序打印结果
```bash
2
3
```
从打印结果中可以看出`calModule.js`这个模块虽然被引用了两次但只初始化了一次
## CommonJS 在服务器端的实现举例
### 1初始化项目
在工程文件中新建如下目录和文件
```
modules
| module1.js
| module2.js
| module3.js
app.js
```
然后在根目录下新建如下命令
```
npm init
```
然后根据提示依次输入如下内容
- **包名**可以自己起包名也可以用默认的包名注意包名里不能有中文不能有大写
- **版本**可以用默认的版本 1.0.0也可以自己修改包名
其他的参数一路回车即可效果如下
![](http://img.smyhvae.com/20180410_1425.png)
于是根目录下会自动生成`package.json`这个文件点进去看一下
```json
{
"name": "commonjs_node",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "smyhvae",
"license": "ISC"
}
```
### 2导入第三方包
`uniq`这个第三方包的作用是保证唯一性我们拿它来举例我们在当前工程目录下输入如下命令进行安装
```
npm install uniq
```
安装成功后根目录下会自动生成相应的文件
![](http://img.smyhvae.com/20180410_1450.png)
需要说明的是我的node版本是 v8.10.0v8以上对应的 npm 版本是 v5.6.0版本比较高因此当我输入完`npm install uniq`之后`package.json`中就会自动添加`uniq`包的依赖
![](http://img.smyhvae.com/20180410_1855.png)
如果有些童鞋的npm版本较低就需要手动去添加依赖另一种方式是可以使用`npm install uniq --save`命令这个多出来的`--save`就可以自动添加依赖
我们去[官网](https://www.npmjs.com/package/uniq)看一下`uniq`的用法:
```javascript
let uniq = require('uniq');
let arr = [1, 1, 2, 2, 3, 5];
uniq(arr);
console.log(arr); //输出结果:[ 1, 2, 3, 5 ]
```
可以看出这个包可以起到数组去重的作用
### 3自定义模块
1module1.js
```javascript
//暴露方式一module.exports = value
//暴露一个对象出去
module.exports = {
name: '我是 module1',
foo(){
console.log(this.name);
}
}
//我们不能再继续写 module.exports = xxx。因为重新赋值会把之前的赋值覆盖掉。
```
2module2.js
```javascript
//暴露方式一module.exports = value
//暴露一个函数出去
module.exports = function(){
console.log('我是 module2');
}
```
注意此时暴露出去的 exports 对象 等价于整个函数
3module3.js
```javascript
//暴露方式二exports.xxx = value
//可以往 export 对象中不断地添加属性,进行暴露
exports.foo1 = function(){
console.log('module3 中的 foo1 方法');
}
exports.foo2 = function(){
console.log('module3 中的 foo2 方法');
}
exports.arr = [1,1,2,2,3,5,11];
```
4app.js将其他模块汇集到主模块
```javascript
//将其他模块汇集到主模块
let uniq = require('uniq'); //引入时,第三方模块要放在自定义模块的上面
let module1 = require('./modules/module1');
let module2 = require('./modules/module2');
let module3 = require('./modules/module3');
//调用module1对象的方法
module1.foo();
//调用module2的函数
module2(); //注意在定义时module2对象等价于整个函数function。所以module2()的意思是,直接调用了函数。
//调用module3中的属性
module3.foo1();
module3.foo2();
uniq(module3.arr); //将module3中的数组进行去重操作
console.log(module3.arr); //打印数组去重后的结果
```
这样的话我们的代码就写完了
我们在命令行中输入`node app.js`就可以把代码跑起来了打印结果如下
```bash
我是 module1
我是 module2
module3 中的 foo1 方法
module3 中的 foo2 方法
[ 1, 11, 2, 3, 5 ]
```
## CommonJS 基于浏览器端的实现举例
### 1初始化项目
在工程文件中新建如下目录和文件
```
js
dist //打包生成文件的目录
src //源码所在的目录
| module1.js
| module2.js
| module3.js
| app.js //应用主源文件
index.html //因为CommonJS是基于浏览器端js文件要跑在浏览器的页面上所以要有这个html页面
```
然后在根目录下新建如下命令
```
npm init
```
然后根据提示依次输入如下内容
- **包名**可以自己起包名也可以用默认的包名注意包名里不能有中文不能有大写
- **版本**可以用默认的版本 1.0.0也可以自己修改包名
其他的参数一路回车即可
于是根目录下会自动生成`package.json`这个文件点进去看一下
```json
{
"name": "commonjs_browser",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
```
### 2下载第三方包Browserify
这里需要用到[Browserify](http://browserify.org/)这个工具进行编译打包。Browserify 称为 CommonJS 的浏览器端的打包工具。
输入如下命令进行安装两个命令都要输入
```javascript
npm install browserify -g //全局
npm install browserify --save-dev //局部。
```
上面的代码中`-dev`表示开发依赖这里解释一下相关概念
- 开发依赖当前这个包只在开发环境下使用
- 运行依赖当前这个包是在生产环境下使用
### 3自定义模块 & 代码运行
1module1.js
```javascript
//暴露方式一module.exports = value
//暴露一个对象出去
module.exports = {
name: '我是 module1',
foo(){
console.log(this.name);
}
}
//我们不能再继续写 module.exports = xxx。因为重新赋值会把之前的赋值覆盖掉。
```
2module2.js
```javascript
//暴露方式一module.exports = value
//暴露一个函数出去
module.exports = function(){
console.log('我是 module2');
}
```
注意此时暴露出去的 exports 对象 等价于整个函数
3module3.js
```javascript
//暴露方式二exports.xxx = value
//可以往export对象中不断地添加属性进行暴露
exports.foo1 = function(){
console.log('module3 中的 foo1 方法');
}
exports.foo2 = function(){
console.log('module3 中的 foo2 方法');
}
```
4app.js将其他模块汇集到主模块
```javascript
let module1 = require('./module1'); // ./ 指的是当前路径
let module2 = require('./module2');
let module3 = require('./module3');
module1.foo();
module2();
module3.foo1();
module3.foo2();
```
引入的路径解释
- `./`是相对路径指的是当前路径app.js的当前路径是src
到此我们的主要代码就写完了
但是如果我们直接在index.html中像下面这样写是不行的因为浏览器不认识 require 关键字
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="./js/src/app.js"></script>
</body>
</html>
```
为了能够让index.html引入app.js我们需要输入如下命令
打包处理js:
```
browserify js/src/app.js -o js/dist/bundle.js
```
然后在index.html中引入打包后的文件
```html
<script type="text/javascript" src="js/dist/bundle.js"></script>
```