js模块化

333 阅读6分钟

有关于webpack模块化打包工具

  • seajs
  • requirejs
  • Conmon.js
  • esmoudle=>浏览器
  • umd=> 兼容处理

流程基本是定义模块,导出模块 ,使用模块,

我们首先要了解一下模块化

当我们项目特别大的时候,我们会将js代码模块化,

比如同时引进jquery和zepto ,这两者同时引用了 '$' ,此时就会产生命名冲突,这样会造成我们没有办法管理依赖,也没有办法控制依赖加载的顺序,那么我们有以下几种解决方案

  • CMD (这是一种广泛使用的js规范 他的核心思想是通过require方法‘同步’加载依赖其他模块,通过moudle.exports 导出需要暴露的接口 =>CommonJS 是AMD最具代表性的实现, node 内部实现了Common.js

a.js文件

import.exports  = 'Hello world';

b.js文件

let str = require(./a.js);

如果说require方法在node中是同步的,那么他是如何实现的呢?

let fs = require('fs');//读取文件
function require(moduleName){  //moduleName路径
    let content = fs.readFileSync(moduleName,'utf8');
    //最后一个参数是函数的内容体
    let fn = new function(‘exports’,'moudle','require','_dirname','_filename',content + '\n return moudle.response')
    let moudle = {
        exports:{}
    }
    return fn(moudle.exports,moudle,require,_dirname,_dirname)
}
/* 
function(‘exports’,'moudle','require','_dirname','filename',content + '\n return moudle.response'){
    import.exports  = 'Hello world';
    return import.exports 
}
*/
  • 另一种方式是AMD require.js AMD也是一种javascript模块化规范,与Common.js最大的不同在于它采用的异步的方式,,,Common.js仅适用于node 此方法可以在浏览器中使用,,最具代表性的实现是

AMD优点

  1. 可在不转码的情况下直接在浏览器中运行
  2. 可加载多个依赖
  3. 可在node 和 浏览器中运行

通过require声明模块

define('name',[],function(){
    
})
 define('age',[],function(){
    
})
require(['name','age'],function(name,age){
    console.log(name,age)
})

let factories = {};//将name和依赖绑定
function define(moudlName,dependencies,factory){//模块名字,依赖,工厂函数
    factories[moudlName] = factory;
}
let function require(mods,callback){
    let result = mods.map(function(mod){
        let factory = factory[mod];//取出来每个相对应的函数
        let exports;
        exports = factory();
        return exports;//执行结果是一个新的数组
    })
    calback.apllay(null,result) 
}

这样我们就实现了简单的AMD的实现,那么还有更复杂的情况 假设我们加依赖

通过require声明模块

define('name',[],function(){
    
})
 define('age',['name'],function(name){
    return name + 9
})
require(['name','age'],function(name,age){
    console.log(name,age)
})

##那么现在如何管理依赖呢?

let factories = {};//将name和依赖绑定
function define(moudlName,dependencies,factory){

    //模块名字,依赖,工厂函数
    factory.dependencies = dependencies;//将依赖系在函数上
    factories[moudlName] = factory;
    require(dependencies,fucntion(...args)){
        factory.applay(null,arguments);
        exports = factory.apply(null ,arguments)
    };
    return exports;
}
let function require(mods,callback){
    let result = mods.map(function(mod){
        let factory = factory[mod];//取出来每个相对应的函数
        let exports;
        exports = factory();
        return exports;//执行结果是一个新的数组
    })
    calback.apllay(null,result) 
}
require(['age'],function(name,age){
    console.log(name,age)
})

我们重点说CommonJS

Conmon.js 实现

ConmonJS规范

  • 定义了如何导入模块 require
  • 还定义了如何导出模块 module.exports 导出xxx
  • 还定义了一个js就是一个模块
  • 如果第一次加载完成,则会存到缓存里,第二次从缓存中读取

下面我们根据以上三个特点,考虑各种情况,一步步实现一个简单的CommenJS引入功能

  1. 首先我们要引入所用到的模块

    a. 读文件

    b. 获取文件路径

    c. 运行环境

    d. 加载策略=>针对js/json/node文件

    e. Module

    f. 缓存

    g. requier方法

let fs = require('fs');
let path = require('path');
let vm = require('vm');
function Module(p) {
    this.id = p; // 当前模块的标识
    this.exports = {}; // 每个模块都有一个exports属性
    this.loaded = false; // 这个模块默认没有加载完
}
Module._extensions = {
    //js优先级高于json,和node 
    '.js': function (Module) {},
    '.json': function (Module) {},
    '.node': 'xxx'
}
Module._cacheModule = {}// 根据的是绝对路径进行缓存的 
function require(moduleId){//我们将加载模块以参数形式传递过来
    
}

以上是我们读取模块必备的条件,下面我们挨个增加内容, 2. 在require接收到路径的时候,我们首先要对此路径做解析,假设我们给个方法_resolveFileName(moduleId)对路径作出解析

// 解析绝对路径的方法 返回一个绝对路径
Module._resolveFileName = function (moduleId) {
  let p = path.resolve(moduleId);
  // 没有后缀在加上后缀 如果传过来的有后缀就不用加了
  if (!path.extname(moduleId)) {//extname是path内部方法,在这里用到的类似用法,清自行查阅,笔者就先不多做解释了
    //keys将一个对象转成数组
    let arr = Object.keys(Module._extensions);
    //如果没有后蕞名称,因为只有三种情况,我们挨个对比,在这里是有先后顺序的,假设传过来a,我们会现识别a.js
    for (let i = 0; i < arr.length; i++) {
      let file = p + arr[i];
      try {
        fs.accessSync(file);//accessSync 同步判定分拣是否存在
        return file;
      } catch (e) {
        console.log(e)
      }
    }
  } else {
    return p;//如果有后坠就直接查找此文件,无需匹配
  }
}
function req(moduleId) {
  let p = Module._resolveFileName(moduleId);// p是一个绝对路径
}
  1. 当我们解析完绝对路径之后,就需要去查找要加载的文件了,我们之前说如果是第二次加载,就从缓存中查找,所以这里我们首先需要判断有没有缓存
Module._extensions = {
    //js优先级高于json,和node 
    '.js': function (Module) {},//这里的function指的是加载该类型文件的方法
    '.json': function (Module) {},
    '.node': 'xxx'
}
function req(moduleId) {
  let p = Module._resolveFileName(moduleId);// p是一个绝对路径
  if (Module._cacheModule[p]) {
    // 模块存在,如果有直接把对象返回即可稍后补充
  }
  // 表示没有缓存就生成一个模块
  let module = new Module(p);
  // 加载模块
  let content = module.load(p); // 加载模块
  Module._cacheModule[p] = module;
  module.exports = content; //最终以module.export导出
  return module.exports
}

在此过程中 ,我们生成一个模块,new了一个moudle, 将路径传过去,还记得上面的代码,在new的过程中,我们给模块加了id,exports,load,然后我加载此模块,并且将它添加到缓存中 load方法是在实例上调用的,我们将吧这个方法写在Module的原型上

//在new之后就有了这些标识
function Module(p) {
    this.id = p; // 当前模块的标识
    this.exports = {}; // 每个模块都有一个exports属性
    this.loaded = false; // 这个模块默认没有加载完
}
Module.prototype.load = function (filepath) {
    //判断加载的文件是json还是node,还是js
    let ext = path.extname(filepath);
    //根据文件类型添加方法
    let content = Module._extensions[ext](this); //成功读取文件内容
    //this指当前模块的实例  有id  exports loaded
    return content;
}
  1. 最后我们来补充下加载文件的方法,这里只介绍json和js的 如果是json我们直接parse,如果是js,我们说一个js是一个模块,那说明每读取一个js相当于读取一个闭包文件,我们会在js文件内容外包一个闭包,然后导出用moudul.export
//exports,require,module
Module.warpper = ['(function(exports,require,module){', '\n})'];
Module._extensions = {
    //js优先级高于json,和node 
    '.js': function (Module) {
        let script = fs.readFileSync(module.id, 'utf8');
        let fn = Module.warpper[0] + script + Module.warpper[1];
        //我们需要执行此文件,但是eval会在当前作用域向上查找,我们只想在require之后执行此文件,因此这里使用沙漏限制环境
        vm.runInThisContext(fn).call(Module.exports, Module.exports, req, module)
        return module.exports;
    },
    '.json': function (Module) {
        return JSON.parse(fs.readFileSync(module.id, 'utf8')); // 读取那个文件
    },
    '.node': 'xxx'
}