手写commonJs

1,372 阅读3分钟

介绍

commonJs是前端模块化规范的其中一种,主要使用在node.js。

每个文件都是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

特点

  1. 所有模块运行在模块自己的作用域中,不影响全局作用域;
  2. 模块可以被多次加载,但只有第一次会运行一次,后面再加载使用缓存。
  3. 读取模块的路径可以不加后缀,自动找到对应后缀模块。

基本使用

定义一个a.js的文件

const fn = function () {
    console.log('Hello');
}
module.exports = fn;

再定义b.js文件,并在其中引入a.js

const fn = require('./b');
fn(); // Hello

b中调用a中的fn方法,打印出‘Hello’。

手写commonJs

所需调用的node内置模块或第三方模块

  • fs文件系统模块
  • path路径解析模块
  • vm虚拟机

定义一个require方法

const req = function(id) {...} // 取名req与系统默认的require作区分
const fn = req('./a');

定义Module对象

function Module(id) {
    this.id = id;
    this.exports = {};
}

定义不同后缀的对应处理方法

commomJs可以引入.js、.json、.node、.mjs的文件,由于篇幅有限,这里只定义了.js和.json的处理方法。

Module.extensions = {};
Module.extensions['.js'] = function (module) {...};
Module.extensions['.json'] = function (module) {...};

定义方法,给路径自动加上对应后缀

Module.getPath = function(id) {
    const absPath = path.resolve(id); // 获得绝对路径
    if (fs.existsSync(absPath)) { // 若输入的路径已包含后缀,可以直接找到
        return absPath;
    }
    const extensions = Object.keys(Module.extensions);
    for (let i = 0;i < extensions.length; i++) {
        const ext = `${absPath}${extensions[i]}`;
        if (fs.existsSync(ext)) {
            return ext;
        }
    }
    throw new Error('The file do not exist'); // 加上后缀还没找到,抛出错误
}

完善req方法

const req = function(id) {
    const ext = Module.getPath(id); // 获得完整路径
    const myModule = new Module(ext); // 实例化一个模块
    const extName = path.extname(ext); // 获得文件的后缀
    const result = Module.extensions[extName](myModule); // 执行对应后缀方法
    return result;
}

完善js后缀文件处理方法

核心逻辑在于用fs文件系统读取到js文件中的内容,然后再将其封装成方法(为了保证私有作用域,以及将exports的值赋到我们自己定义的module中)。

let script = fs.readFileSync(module.id, 'utf8');
const wrapper = `(function (exports, require, module, __dirname, __filename) {${script}})`;

但此时获得的wrapper依旧是字符串,我们需要将字符串转换成能执行的函数方法,此时一般会想到eval。我们这里使用更高级的vm,具体使用方法参考vm官方文档

let script = fs.readFileSync(module.id, 'utf8');
const wrapper = `(function (exports, require, module, __dirname, __filename) {${script}})`;
const fn = vm.runInThisContext(wrapper); // vm.runInThisContext返回封装的那个方法

完整js处理实现

Module.extensions['.js'] = function (module) {
    let script = fs.readFileSync(module.id, 'utf8');
    const wrapper = `(function (exports, require, module, __dirname, __filename) {${script}})`;
    const fn = vm.runInThisContext(wrapper);
    fn(module.exports, req, module, __dirname, __filename);
    return module.exports;
};

完善json后缀文件处理方法

json的处理会简单很多,把文件读取到后,直接JSON.parse返回对象即可。

Module.extensions['.json'] = function (module) {
    let jsonContent = fs.readFileSync(module.id, 'utf8');
    return JSON.parse(jsonContent);
};

缓存

每次读取文件之后,下一次读取统一文件直接使用缓存即可。定义cache对象:

Module.cache = {};

在req方法内,如果缓存内有数据直接返回缓存数据,并且在拿到新的模块数据后要将数据计入缓存中。完善后的req方法:

const req = (id) => {
    const ext = Module.getPath(id);
    if (Module.cache[ext]) { // 查询缓存
        return Module.cache[ext]; // 查询到缓存,使用缓存
    }
    const myModule = new Module(ext);
    // 对应后缀方法执行
    const result = Module.extensions[path.extname(ext)](myModule);
    Module.cache[ext] = myModule; // 计入缓存
    return result;
};

完整代码

const path = require('path');
const fs = require('fs');
const vm = require('vm');

// Module处理
function Module(id) {
    this.id = id;
    this.exports = {};
};
// 缓存
Module.cache = {};
// 不同后缀类型处理
Module.extensions = {};
Module.extensions['.js'] = function (module) {
    let script = fs.readFileSync(module.id, 'utf8');
    const wrapper = `(function (exports, require, module, __dirname, __filename) {${script}})`;
    const fn = vm.runInThisContext(wrapper);
    fn(module.exports, req, module, __dirname, __filename);
    return module.exports;
};
Module.extensions['.json'] = function (module) {
    let jsonContent = fs.readFileSync(module.id, 'utf8');
    return JSON.parse(jsonContent);
};
Module.getPath = function (id) {
    const absPath = path.resolve(id);
    if (fs.existsSync(absPath)) {
        return absPath;
    }
    const extensions = Object.keys(Module.extensions);
    for (let i = 0; i < extensions.length; i++) {
        const extPath = `${absPath}${extensions[i]}`;
        if (fs.existsSync(extPath)) {
            return extPath;
        }
    }
    throw new Error('The file do not exist');
}

const req = (id) => {
    const ext = Module.getPath(id);
    if (Module.cache[ext]) {
        return Module.cache[ext];
    }
    const myModule = new Module(ext);
    // 对应后缀方法执行
    const result = Module.extensions[path.extname(ext)](myModule);
    Module.cache[ext] = myModule;
    return result;
};

//以下是使用req的代码块
const func = req('./b');
func(); // Hello

招聘!!!

字节跳动互娱基础架构团队招人啦!北京、深圳、杭州都有岗位!

我们是谁

字节成立最早的前端架构团队,目前规模最大,做的最专业,手里直接有大几百人的前端业务团队,产品 DAU 上亿级别,每天不用和 PM、UI 撕逼,有良好的技术氛围,业界大牛云集,团队成员都能获得相对好的技术成长。

平时工作

负责抖音、抖音火山版、直播等业务大规模复杂业务场景的前端架构设计、实现和优化

  1. 负责PC、H5、Hybrid、App Native、BFF、RPC等一种或几种技术场景的架构;
  2. 制定开发规范,工程化体系搭建及优化,提升开发效率、质量和性能,保障业务稳定运行;
  3. 发现现有流程及架构的问题,并持续进行优化;
  4. 解决业务遇到的技术痛点和难点;
  5. 跟进业内前沿技术,保证团队技术的先进性。

职位要求

  1. 本科及以上学历,计算机及相关专业;计算机基础扎实,熟悉数据结构、网络等;
  2. 有一定的架构和方案设计能力及经验,具备一定的方案沟通和推动能力;
  3. 对后端技术有一定了解,熟悉一门后端语言(java/go等);
  4. 对前端工程化(例如构建方面:webpack、rollup等)、Nodejs、渲染框架(例如react或vue等)、中后台搭建系统等至少其一有一定深度的实践经验者优先;
  5. 有大型网站架构经验者优先;有较高的技术热情和积极性者优先。

加分项

  1. 参与或主导过优秀的开源项目;
  2. 有优秀的技术博文、博客。

有意者可以添加我微信说明来意:

image.png