上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;
无论你多么喜欢 Javascript,以及它现在发展的有多好,我们都需要承认在B/ endan Eichi用了10天写出 javascript的时候,它都有很多的缺陷:
在网页开发的早期, Brendan Eich开发 javascriptt仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的:
但是随着前端和 javascriptl的快速发展, Javascript代码变得越来越复杂了
所以,模块化已经是 Javascript一个非常迫切的需求
但是 Javascript本身,直到ES6(2015)オ推出了自己的模块化方案
在此之前,为了让 Javascript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、 Commonjs等;
我们需要知道 CommonJs是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJs ,后来为了体现它的广泛性,修改为 CommonJs ,平时我们也会筒称为 CJS.
所以,Node中对 CommonsJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
我们可以使用这些变量来方便的进行模块化开发;
exports和 module. exports 可以负责对模块中的内容进行导出 ;
require函数可以帮助我们 导入其他模块(自定义模块、系统模块、第三方库模块) 中的内容
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 对象的浅拷贝(引用赋值)
但是在Node中我们经常导出东西的时候,又是通过 module. exports 导出的
我们追根溯源,通过维基百科中对 Commonjs规范的解析:
一个文件把它当成一个对象的时候,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
require的加载过程是同步的,意味着必须等到引入的文件(模块)加载完成之后,才会继续执行其他代码,也就是会造成阻塞(因为引入一个文件则该文件内部的所有代码都会被执行一次)。
require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象。
那么,require的查找规则是怎样的呢?
中文文档:require
这里总结一下 require常见的查找规则:
require(X)
情况一:X是一个核心模块,比如path、http
情况二:X是以./
或../
或/(根目录)
开头的
情况三:直接是一个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
顺序为:图结构的深度优先算法
小知识:ES Module是ES6推出的。 即es 2015
Javascript.没有模块化一直是 它的痛点 ,所以オ会在社区产生许多的规范: Commonjs、AMD、CMD等,所以在ES推出自己的模块化系统时,大家也是兴奋异常
ES Module和 Commonjs的模块化有一些不同之处:
ES Module模块采用 export 和 import 关键字来实现模块化:
ES Module将自动采用严格模式:
use strict
注意在浏览器使用ES Modules时,要在script
标签上加上 type="module"
,且要在服务器上运行,不支持本地运行的file协议(触发CORS),
index.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>Documenttitle>
head>
<body>
<script src="./index.js" type="module">script>
body>
html>
index.js
console.log('hello EsModules');
结果:
hello EsModules
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
姓名彭先生
前面我们学习的导出功能都是有名字的导出( named exports)
某些情况下不是很方便,所以还有另外一种导出方式,叫做 default export
bar.js 导出 :
// 方式四:默认导出
export default function format() {
console.log('对某一个东西,进行格式化!');
}
index.js 导入:
// 方式四: 演示 export default如何导入
import utils from './modules/foo.js'
utils() // 实际是调用 format
结果:
对某一个东西,进行格式化!
注意: 一个文件只能有一个默认导出:export default
通过 Import加载的模块,是不可以在其放到逻辑代码中的,比如:
let flag = true
if (flag) {
// 错误用法,语法错误,不能在逻辑在逻辑代码中使用 import 关键字
import format from './modules/foo.js'
}
为什么会出现这个情况呢?
parse => AST => 字节码 =>
解决办法:
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);
})
}
使用 type="module"
时,加载该模块时异步加载的,就相当于给script
加了一个async
属性。
示例:
index.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>Documenttitle>
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 是异步的。
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 定义的,所以导入后的变量内存地址不能发生变化,但是对象类型的值却可以。