文章目录
一、什么是模块化?
- 事实上模块化开发最终的目的是将程序划分成一个个小的结构;
- 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;
- 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用
- 也可以通过某种方式,导入另外结构中的变量、函数、对象等
上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;
无论你多么喜欢 Javascript,以及它现在发展的有多好,我们都需要承认在Brendan Eichi用了10天写出 javascript的时候,它都有很多的缺陷:
- 比如var定义的变量作用域问题;
- 比如 Javascript的面向对象并不能像常规面向对象语言一样使用 class;
- 比如 javascript没有模块化的问题
在网页开发的早期, Brendan Eich开发 javascriptt仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的:
- 这个时候我们只需要讲 Javascript代码写到< script>标签中即可;
- 并没有必要放到多个文件中来编写;甚至流行:通常来说 Javascript程序的长度只有一行。
但是随着前端和 javascriptl的快速发展, Javascript代码变得越来越复杂了
- ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过 javascripti进行前端页面的渲染;
- SPA的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过 Javascript来实现
- 包括Node的实现, Javascript编写复杂的后端程序,没有模块化是致命的硬仿;
所以,模块化已经是 Javascript一个非常迫切的需求
但是 Javascript本身,直到ES6(2015)オ推出了自己的模块化方案
在此之前,为了让 Javascript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、 Commonjs等;
二、CommonJS与Node
我们需要知道 CommonJs是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJs ,后来为了体现它的广泛性,修改为 CommonJs ,平时我们也会筒称为 CJS.
- Node是 CommonJs 在服务器端一个具有代表性的实现;
- Browserify是 CommonJS在浏览器中的一种实现
- webpack打包工具具备 Commonjs的支持和转换;
所以,Node中对 CommonsJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
- 在Node中每一个js文件都是一个单独的模块
- 这个模块中包括 CommonJs规范的核心变量 exports、 module. exports、 require;
我们可以使用这些变量来方便的进行模块化开发;
exports和 module. exports 可以负责对模块中的内容进行导出 ;
require函数可以帮助我们 导入其他模块(自定义模块、系统模块、第三方库模块) 中的内容
2.1 exports 的 导入与导出
bar.js
// Node中每一个Js文件就是一个模块
const name = 'PengSir'
const age = 18
let message = 'My name is peng'
function sayHello(name) {
console.log('hello' + name);
}
// exports 默认是空对象
// console.log(exports); // {}
// 1.导出
exports.name = name
exports.age = age
exports.sayHello = sayHello
exports.message = message
main.js
// 导入:require 是一个函数
// const bar = require('./bar')
const {name,age,sayHello,message} = require('./bar')
console.log(name);// PengSir
图解:导入导出原理
在每个Node应用中都有一个 exports 对象,在其他文件导入某个文件时,其实就是拿到该对象的内存地址。
为了验证这一想法,写个demo来确定一下。
bar.js
// 就是一个模块
let name = 'PengSir'
setTimeout(() => {
exports.name = 'test'
}, 1000);
console.log(name)
exports.name = name
main.js
const bar = require('./bar.js')
console.log(bar.name)
setTimeout(() =>{
console.log(bar.name)
})
输出:
PengSir
test
可以发现结果确实如我们所料。
所以,bar对象是 exports 对象的浅拷贝(引用赋值)
2.2 module.exports 又是什么东西?
但是在Node中我们经常导出东西的时候,又是通过 module. exports 导出的
- module. exports和 exports1有什么关系或者区别呢?
我们追根溯源,通过维基百科中对 Commonjs规范的解析:
- CommonJS中是没有 module. exports的概念的
- 但是为了实现模块的导出,Node中使用的是 Module的类,每一个模块都是 Module的一个实例,也就是new module( 一个JS文件就是一个Module实例 )
- 所以在Node中真正用于导出的其实根本不是 exports,而是 module. exports;
- 因为 module才是导出的真正实现者
一个文件把它当成一个对象的时候,Node底层就会 new module
Node的底层 实际上做了这么一步操作 module.exports = exports
所以咱们上述的 bar = exports = module.exports
验证如下:
bar.js
const name = 'PengSir'
setTimeout(() => {
module.exports.name = 'hahaha'
console.log(exports.name);
}, 1000);
exports.name = name
main.js
const bar = require('./bar')
console.log(bar.name);
setTimeout(() => {
console.log(bar.name);
}, 2000);
output:
PengSir
hahaha
hahaha
由此可见,我们上述的论述完全正确,即 bar = exports = module.exports
图解:
2.3 require的细节
require的加载过程是同步的,意味着必须等到引入的文件(模块)加载完成之后,才会继续执行其他代码,也就是会造成阻塞(因为引入一个文件则该文件内部的所有代码都会被执行一次)。
require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象。
那么,require的查找规则是怎样的呢?
中文文档:require
这里总结一下 require常见的查找规则:
require(X)
情况一:X是一个核心模块,比如path、http
- 直接返回核心模块,并停止查找
情况二:X是以./
或../
或/(根目录)
开头的
- 第一步:将X当做一个文件在对应的目录下查找;
- 如果有后缀名,按照后缀名的格式查找对应的文件
- 如果没有后缀名,按照如下顺序
- 直接查找文件X
- 查找X.js
- 查找X.json
- 查找X.node
- 第二步:没有找到对应的文件,将X作为一个目录
- 查找目录下面的index文件
- 查找X/index.js
- 查找X/index.json
- 查找X/index.node
- 查找目录下面的index文件
情况三:直接是一个X(没有路径),并且X不是一个核心模块
例如我在如下目录的main.js
中编写了require('test’)
/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/0 5_javascript-module/02_commonjs/main.js中编写require('why’)
则它的查找规则如下,会逐级查找上一层目录下的node_modules
:
如果都没找到,那么报错 not found
2.4 模块的加载过程
- 结论一:模块在第一次被引入的时候,模块中的js代码会被运行一次
- 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
- 为什么只会加载运行一次呢?
- 这是因为每个模块对象 module都有一个属性: loaded
- 为 false表示还没有加载,为true表示已经加载
- 结论三:如果有循环引入,那么加载顺序是什么?
顺序为:图结构的深度优先算法
三、ES Module
小知识:ES Module是ES6推出的。 即es 2015
Javascript.没有模块化一直是 它的痛点 ,所以オ会在社区产生许多的规范: Commonjs、AMD、CMD等,所以在ES推出自己的模块化系统时,大家也是兴奋异常
ES Module和 Commonjs的模块化有一些不同之处:
- 一方面它使用了 Import和 export 关键字 ,不是模块也不是函数。
- 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式
ES Module模块采用 export 和 import 关键字来实现模块化:
- export负责将模块内的内容导出
- import负责从其他模块导入内容
ES Module将自动采用严格模式:
use strict
3.1 尝试使用ES Modules
注意在浏览器使用ES Modules时,要在script
标签上加上 type="module"
,且要在服务器上运行,不支持本地运行的file协议(触发CORS),
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./index.js" type="module"></script>
</body>
</html>
index.js
console.log('hello EsModules');
结果:
hello EsModules
3.2 常见的导入与导出方式 export 与 import
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./index.js" type="module"></script>
</body>
</html>
index.js 导入
console.log('hello EsModules');
// 常见的导入方式
// 方式一: import {} from '路径'
// 注意此处的{}不是对象,导入时后边必须要加.js,脚手架里和webpack会自动加
// import { name, age, sayhello } from './modules/foo.js'
// 方式二:导出的变量可以起别名
// import { name as Fname, age as Fage, sayhello as FsayHello } from './modules/foo.js'
// 2.1 导出时已经起了别名的,接收要使用别名接受,可以给别名再起别名
// import {Fname as FooName,Fage as FooAge,FsayHello as FooSayHello} from './modules/foo.js'
// 方式三:import * as foo from '路径'
import * as foo from './modules/foo.js'
console.log(foo.name);
console.log(foo.age);
foo.sayHello('彭先生')
foo.js 导出
const name = 'pengsir'
const age = 18
const sayHello = function (name) {
console.log('姓名' + name);
}
// 1.导出方式
// 方式一:
// export const name = 'pengsir'
// export const age = 18
// export const sayHello = function (name) {
// console.log('姓名' + name);
// }
// 方式二: 常用!!!!!!
// {} 这里不是类 就和 if(){} 的大括号一样
// {放置要导出变量的引用列表}
export {
name,
age,
sayHello
}
// 方式三:{} 导出时,可以给变量起别名
// export {
// name as Fname,
// age as Fage,
// sayHello as FsayHello
// }
输出结果:
hello EsModules
pengsir
18
姓名彭先生
3.3 export default
前面我们学习的导出功能都是有名字的导出( named exports)
- 在导出(exporte) 时指定了名字;
- 在导入(import) 时需要知道具体的名字
某些情况下不是很方便,所以还有另外一种导出方式,叫做 default export
- 默认导出 export时可以不需要指定名字;
- 在导入时十分方便,并且可以自己来指定名字,类如我们导入Vue、Vuex、VueRoter时内部就是使用的默认导出;
bar.js 导出 :
// 方式四:默认导出
export default function format() {
console.log('对某一个东西,进行格式化!');
}
index.js 导入:
// 方式四: 演示 export default如何导入
import utils from './modules/foo.js'
utils() // 实际是调用 format
结果:
对某一个东西,进行格式化!
注意: 一个文件只能有一个默认导出:export default
3.4 import 函数
通过 Import加载的模块,是不可以在其放到逻辑代码中的,比如:
let flag = true
if (flag) {
// 错误用法,语法错误,不能在逻辑在逻辑代码中使用 import 关键字
import format from './modules/foo.js'
}
为什么会出现这个情况呢?
parse => AST => 字节码 =>
- 这是因为 ES Module在被JS引擎(parse)解析时,就必须知道它的依赖关系
- 由于这个时JS代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况
解决办法:
import()
函数 或者 require()
// 方式五:import() 函数
// 注意:上边使用import时是作为关键字使用,现在是作为函数使用,
// 该函数为异步函数,返回值为promise
let flag = true
if (flag) {
import('./modules/foo.js').then(res => {
console.log('then里边的回调');
console.log(res);
}, err => {
console.log(err);
})
}
3.5 异步的 import
使用 type="module"
时,加载该模块时异步加载的,就相当于给script
加了一个async
属性。
示例:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./index.js" type="module"></script>
<script src="./normal.js"></script>
</body>
</html>
index.js
console.log('hello EsModules');
normal.js
console.log('我是普通的js文件');
结果:
我是普通的js文件
hello EsModules
由此可见我们的 ES Module 是异步的。
3.6 ES Module 的加载过程
- 结论一:ES Module 导出的数据是实时变化的
- 如果在
bar.js
中导出一个变量A,在index.js
中导入该变量,如果1秒之后bar.js
中的变量A值被修改,index.js
中的导入的该变量也会修改。 - 但是我们在
index.js
中却不可以修改导入的该变量,除非该变量是一个对象类型(保存的是 该对象的内存地址),因为ES Module在底层实现的时候,每次 导出的变量发生变化,都会在模块环境记录中创建一个最新的该变量,类似:const name = name
发生变化后const name = name; const name = name
,所以能拿到最新的值,且因为底层是使用const 定义的,所以导入后的变量内存地址不能发生变化,但是对象类型的值却可以。
- 如果在
四、工具库的统一出口
在项目中经常有很多的工具函数,分散在不同的文件中,如果要引入的话,需要找到对应的文件来引入,每次都去找对应的文件还是比较麻烦的,我们可以给这些工具库搞一个统一的出口,以后要用的话,直接导入我这个出口文件就可以了。
/**
* 工具的统一出口
*/
// 1.导出方式一:挨个导入再挨个导出
// import { sub, add } from './math.js'
// import { timeFormat } from './format.js'
// export { sub, add, timeFormat }
// 2.导出方式二:直接导出指定的
// export { sub, add } from './math.js'
// export { timeFormat } from './format.js'
// 3.导出方式三: 直接导出所有的
export * from './math.js'
export * from './format.js'