Node中的模块化

一、模块化历程

模块就是小而精且利于维护的低耦合代码片段。模块化是前端走向工程化最重要的一环,早期js语言层面没有模块化规范更多的是程序员利用函数、对象、自执行函数去实现分块,后来es6中将模块化规范纳入标准范围,当下常用规范是Commonjs与Esm。

前端开发为什么需要模块化

  • 命名冲突和污染
  • 代码冗余,无效请求多
  • 文件间的依赖关系复杂
  • 项目难以维护不方便使用

常见模块化规范

  • Common js规范
  • AMD规范
  • CMD规范
  • ES modules规范

二、CommonJs规范

CommonJs规范主要应用于Nodejs,由于浏览器所具备的一些特点,例如数据一般通过网路传输、而且还存在单线程阻塞的加载方式而CommonJS规范定义模块的加载是同步完成,因此这就让CommonJs规范不能适用于浏览器平台。CommonJs是一个超级,它是语言层面上的规范类似于ECMAScript,而模块化只是这个规范中的一个部分。

CommonJs的规范主要有以下三点

  • 模块的引用
  • 模块的定义
  • 模块的标识

Nodejs针对以上三点的语法实现

  • 任意一个文件就是一个模块,具有独立作用域
  • 使用require导入其它模块
  • 将模块ID传入require实现目标模块定位
  • 使用module.exports与require实现模块导入与导出
  • module属性及其常见信息获取
  • exports导出数据及其与module.exports区别
  • CommonJS规范下的模块同步加载
模块的导入导出
// 一、模块的导出
const age = 18
const addFn = (x, y) => {
  return x + y
}

module.exports = {
  age: age, 
  addFn: addFn
}
// 一、模块的导入
let obj = require('./m')
console.log(obj)
如何证明common是同步的?
let name = 'lg'
let iTime = new Date()
// 延时4s
while(new Date() -iTime < 4000) {}

module.exports = name
console.log('m.js被加载导入了')
let obj = require('./m')
console.log('01.js代码执行了') // 4秒之后陆续打印 m.js被加载导入了 01.js代码执行了 说明是同步的

Nodejs中的module属性

  • 任意js文件就是一个模块,可以直接使用module属性
  • id
    • 返回模块标识符,一般是一个绝对路径
  • filename
    • 返回文件模块的绝对路径
  • loaded
    • 返回布尔值,表示模块是否完成加载
  • parent
    • 返回对象存放调用当前模块的模块
  • children
    • 返回数组,存放房钱模块调用的其它模块
  • exports
    • 返回当前模块需要暴露的内容
  • paths
    • 返回数组,存放不容目录下的node_modules位置
module属性的打印
module.exports = 1111
console.log(module)

在这里插入图片描述

let obj = require('./m')
console.log(obj); // 1111

module.exports与exports有何区别?

Commonjs中只有一个导出的api就是module.exports,而exports是nodejs为了自己方便操作给每个模块提供的一个变量,这个变量指向module.exports。

在这里插入图片描述

所以我们不能直接给exports赋值,这样会切断和module.exports的联系。

在这里插入图片描述

node中的exports使用案例1
exports.name = 'abc'
let obj = require('./m')
console.log(obj)  // 和common效果一样,打印结果为 { name: 'zce' }
node中的exports使用案例2
exports = {
  name: 'syy',
  age: 18
}
let obj = require('./m')
console.log(obj) // 打印为空对象 {}

require属性

  • 基本功能是读入并且执行一个模块文件
  • resolve: 返回模块文件的绝对路径
  • exrensions: 依据不同后缀名执行解析操作
  • main: 返回主模块对象,也就是入口文件模块
console.log(require.main == module) // false require.main是这个模块的父级
module.exports = 'lg'
let obj = require('./m')
console.log(require.main == module) // true require.main是当前这个模块

小结

  • CommonJS规范起初是为了弥补JS语言模块化缺陷
  • CommonJS是语言层面的规范,当前主要用于Node.js
  • CommonJS规定模块化分为引入、定义、表示符三个部分
  • Moudle在任意模块中可直接使用包含模块信息
  • Require 接收标识符,加载目标模块
  • Exports与module.exports都能导出模块数据
  • CommonJS规范定义模块的加载是同步完成,也是这个特点让他不适用于浏览器平台.

Node中的模块分类及加载流程

模块分类

  • 内置模块
  • 文件模块(第三方包、自定义文件等)

模块加载速度

  • 核心模块
    • Node源码编译时写入到二进制文件中
  • 文件模块
    • 代码运行时,动态加载,加载速度慢一些
      所以加载速度跟加载流程是有关系的

模块的加载流程

  • 模块路径分析

    • 依据标识符确定模块位置
    • 文件标识符
      • 路径标识符
      • 非路径标识符(fs、http、path等导入时能直接写名字的模块)
        // 路径标识符
        const a = require('./m')
        
        // 非路径标识符
        const fs = require('fs')
        
  • 模块文件定位

    • 确定目标模块中具体的文件及文件类型(.js、.json等)
      • 项目下存在m1.js模块,导入时使用require(m1)语法,这个时候node是不知道后缀名的
      • node会以一定的顺序补足扩展名依次去查找:m1.js->m1.json->m1.node,
      • 如果以上都没有找到,node就会判定为这是一个目录,将目录当成一个包来处理,就会去查找package.json文件,使用JSON.parse()解析,从而取出描述信息
      • 首先是找到描述文件中的mian属性值,那如果main属性值也是没有扩展名的那么同样也会去进行后缀的补全:main.js->main.json->main.node
      • 如果以上又没找到,那么node会默认将index作为目标模块中具体文件名称,依次去查找index.js->index.json->index.node
      • node首先会在当前层级查找,如果没查找到会按照文件路径一层一层向上查找,如果还没查找到就会抛出错误信息
        console.log(module.paths);
        
        以上会打印一个向上查找文件的路径数组,根据这个路径如果没找到目标文件就会报错 not find **
        在这里插入图片描述
  • 编译执行

    • 采用对应的方式完成文件的编译执行,最终返回一个可用的exports对象供外部使用
    • js文件编译执行
      • 使用fs模块同步读入目标文件的内容
      • 对内容进行语法包装,生成可执行js函数
      • 调用函数时传入exports、module、require等属性值
    • JSON文件编译执行
      • 将读取到的文件内容通过JSON.parse()进行解析
缓存优化
  • 提高模块的加载速度,首先回去缓存中查找对应模块
  • 如果当前模块不存在,则经历一次完整流程
  • 模块加载完成之后,使用路径作为索引进行缓存

vm模块的使用

vm模块为node底层创建独立运行的沙箱环境,在require首次加载模块时,获取到的文件内容是一个字符串,那么如何安全的运行这些字符串代码就是vm的作用了

执行字符串的几种方式

test.txt文件内容为

var age = 18
eval实现
const fs = require('fs')

let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
console.log(content); // var age = 18
console.log(age); // 报错

用eval的话不符合模块化的标准,比如当前模块和eval中同时声明了同一个变量时,就会报错。而模块化中每一个模块的作用域都要求是独立的。

new Function
const fs = require('fs')
let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
console.log(content); // var age = 18
console.log(age) // 33
let fn = new Function('age', content)
console.log(fn(age)) // undefined
console.log(age) // 33

用new Function虽然能解决定义相同变量报错的问题,但是如果当前模块没有定义age而引入的模块定义了age,在当前模块任然取不到age,这同样不符合模块化标准,而且定义函数和传参会比较麻烦,在使用上也没有eval方便

vm.runInThisContext()
const fs = require('fs')
const vm = require('vm')

let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
console.log(content);// var age = 18

vm.runInThisContext(content)
console.log(age) // 33 

用vm.runInThisContext() 就完美解决。如果当前模块没定义age, 那么age打印就是18

手写require模块加载

  1. 先根据filename按顺序找资源,找到之后拿到文件的绝对路径
  2. 绝对路径就是模块的id,根据这个id去缓存中找,如果找的到直接返回module.exports。否则实例化一个新的Module实例
  3. 实例化之后将该模块缓存起来以便下次使用
  4. 编译加载模块内容,不同的文件不同操作
  5. 返回数据module.exports
# v.json文件内容
{
  "age": "18"
}
const fs = require('fs')
const path = require('path')
const vm = require('vm')
// 3 创建空对象加载目标模块
function Module (id) {
  this.id = id
  this.exports = {}
  console.log(1111)
}

// 1 绝对路径
Module._resolveFilename = function (filename) {
  // 利用 Path 将 filename 转为绝对路径
  let absPath = path.resolve(__dirname, filename)
  
  // 判断当前路径对应的内容是否存在()
  if (fs.existsSync(absPath)) {
    // 如果条件成立则说明 absPath 对应的内容是存在的
    return absPath
  } else {
    // 文件定位
    let suffix = Object.keys(Module._extensions)

    for(var i=0; i<suffix.length; i++) {
      let newPath = absPath + suffix[i]
      if (fs.existsSync(newPath)) {
        return newPath
      }
    }
  }
  throw new Error(`${filename} is not exists`)
}

// 文件定位与加载
Module._extensions = {
  '.js'(module) {
    // 读取
    let content = fs.readFileSync(module.id, 'utf-8')

    // 包装
    content = Module.wrapper[0] + content + Module.wrapper[1] 
    
    // VM 
    let compileFn = vm.runInThisContext(content)

    // 准备参数的值
    let exports = module.exports
    let dirname = path.dirname(module.id)
    let filename = module.id

    // 调用
    compileFn.call(exports, exports, myRequire, module, filename, dirname)
  },
  '.json'(module) {
    let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))

    module.exports = content
  }
}

// 包装函数
Module.wrapper = [
  "(function (exports, require, module, __filename, __dirname) {",
  "})"
]

// 4 缓存优先,缓存已加载过的模块
Module._cache = {}

// 5 执行加载(编译执行)
Module.prototype.load = function () {
  let extname = path.extname(this.id)
  
  Module._extensions[extname](this)
}

function myRequire (filename) {
  // 1 绝对路径
  let mPath = Module._resolveFilename(filename)
  
  // 2 缓存优先
  let cacheModule = Module._cache[mPath]
  if (cacheModule) return cacheModule.exports

  // 3 创建空对象加载目标模块
  let module = new Module(mPath)

  // 4 缓存已加载过的模块
  Module._cache[mPath] = module

  // 5 执行加载(编译执行)
  module.load()

  // 6 返回数据
  return module.exports
}
// 初始化模块导入
let obj = myRequire('./v')
// 取缓存
let obj2 = myRequire('./v')
console.log(obj.age)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值