0

0

浅析node怎样链接多个JS模块

青灯夜游

青灯夜游

发布时间:2023-02-07 17:52:37

|

2483人浏览过

|

来源于掘金社区

转载

有时候是不是会有这样的疑问:纷繁的功能文件,到最后是怎么组合成起来并且在浏览器中展示的?为什么需要 node 环境?下面本篇文章给大家介绍一下node是怎样把多个js模块链接在一起的?希望对大家有所帮助!

浅析node怎样链接多个JS模块

一、个人理解

浏览器本身只能做一些展示及用户交互的功能,对于系统操作的能力很有限,那么,浏览器内置的运行环境显然不满足一些更为人性化的开发模式,比如:更好的区分功能模块、实现文件的操作。那么,带来的缺陷就很明显,比如:各个 JS 文件比较分散,需要在 html 页面里面单独引入,如果某个 JS 文件需要其他的 JS 库,那么很可能会因为 html 页面未引入而报错,在功能庞大的项目里,手动的管理这些功能文件确实让人有点捉襟见肘。

那么, node 到底是怎么更友好的提供开发的呢?其实,上面也说了,人为的管理文件依赖不但会消耗大量的精力,还会存在疏漏,那么,是不是以用自动化的方式进行管理就会好很多?是的,在 node 的运行环境里拓宽了对系统的操作能力,也就是说,或许以前开发者也想通过一些代码来完成那些机械琐碎的工作,但是,只有想法没有操作权限,最后只能望洋兴叹。现在,可以以 node 的一些扩展功能对文件进行先前加工与整理,再添加一些自动化的代码,最后转换为一个浏览器可识别的、完整的 JS 文件,这样一来,多个文件的内容,便可以汇集到一个文件。【相关教程推荐:nodejs视频教程编程教学

二、创建文件

先创建一些 JS 文件,如下图所示:

image.png

这些文件都是手动创建,babel-core 这个文件是从全局的 node_modules 里面复制出来的,如下图所示:

image.png

为什么要复制出来呢?这是因为,任何脚手架干的事其实都是为了快速搭建,但是,怎么能理解它干的什么事呢?那干脆就直接复制吧,本身,node 除了一些内置的模块,其他的都需要通过指明 require 路径的方式来找到相关模块,如下图所示:

image.png

通过 require('./babel-core') 方法,解析一个功能模块下的方法。

1、编写入口文件,转换ES6代码

entrance.js 作为入口文件,作用就是设定工作从哪开始?怎么开始?那么,这里的工作指的就是转换ES6代码,以提供浏览器使用。

//文件管理模块
const fs = require('fs');
//解析文件为AST模块
const babylon = require('babylon');
//AST转换模块
const { transformFromAst } = require('./babel-core');
//获取JS文件内容
let content = fs.readFileSync('./person.js','utf-8')
//转换为AST结构,设定解析的文件为 module 类型
let ast = babylon.parse(content,{
    sourceType:'module'
})
//将ES6转换为ES5浏览器可识别代码
le t { code } = transformFromAst(ast, null, {
    presets: ['es2015']
});
//输出内容
console.log('code:\n' + `${code}`)

上面的代码很简单,最终的目的就是将 module 类型的 person.js 文件转换为 ES5

let person = {name:'wsl'}
export default person

终端运行入口文件,如下所示:

node entrance.js

打印一下代码,如下图所示:

"use strict";
//声明了一个 __esModule 为 true 的属性
Object.defineProperty(exports, "__esModule", {
  value: true
});
var person = { name: 'wsl' };
exports.default = person;

可以,看到打印的代码,里面都是浏览器能识别的代码,按照常理,看看能不能直接运行一下?

下面将这段代码通过 fs 功能写入一个 js 文件并让一个页面引用,来看看效果:

fs.mkdir('cache',(err)=>{
    if(!err){
        fs.writeFile('cache/main.js',code,(err)=>{
            if(!err){
                console.log('文件创建完成')
            }
        })
    }
})

再次运行命令,如图所示:

image.png

浏览器运行结构,如图所示:

image.png

其实代码生成完就有很明显的错误,未声明变量,怎么会不报错呢?这时候,在入口文件输入之前就需要添加一些自定义辅助代码,来解决一下这个报错。

解决的方式也很简单,将原 code 的未声明的 exports 变量通过自执行函数的方式包裹一下,再返回给指定对象。

//完善不严谨的code代码
function perfectCode(code){
    let exportsCode = `
    var exports = (function(exports){
    ${code}
    return exports
    })({})
    console.log(exports.default)`
    return exportsCode
}
//重新定义code
code = perfectCode(code)

看一下输出完善后的 main.js 文件

var exports = (function(exports){
    "use strict";
    Object.defineProperty(exports, "__esModule", {
    value: true
    });
    var person = { name: 'wsl' };
    exports.default = person;
    return exports
})({})
console.log(exports.default)

浏览器运行,如图所示:

image.png

现在浏览器运行正常了。

2、处理 import 逻辑

VisualizeAI
VisualizeAI

用AI把你的想法变成现实

下载

既然是模块,肯定会存在一个模块依赖另一个或其他很多个模块的情况。这里先不着急,先看看person 模块引入单一 animal 模块后的代码是怎样的?

animal 模块很简单,仅仅是一个对象导出

let animal = {name:'dog'}
export default animal

person 模块引入

import animal from './animal'
let person = {name:'wsl',pet:animal}
export default person

看下转换后的代码

"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
var _animal = require("./animal");
var _animal2 = _interopRequireDefault(_animal);
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : { default: obj };
}
var person = { name: 'wsl', pet: _animal2.default };
exports.default = person;

可以看到,转换后会多一个未声明的 require 方法,内部声明的 _interopRequireDefault 方法已声明,是对 animal 导出部分进行了一个包裹,让其后续的代码取值 default 的时候保证其属性存在!

下面就需要对 require 方法进行相关的处理,让其转为返回一个可识别、可解析、完整的对象。

是不是可以将之前的逻辑对 animal 模块重新执行一遍获取到 animal 的代码转换后的对象就行了?

但是,这里先要解决一个问题,就是对于 animal 模块的路径需要提前获取并进行代码转换,这时候给予可以利用 babel-traverse 工具对 AST 进行处理。

说到这里,先看一下 JS 转换为 AST 是什么内容?

这里简单放一张截图,其实是一个 JSON 对象,存储着相关的代码信息,有代码位置的、指令内容的、变量的等等。

image.png

拿到它的目的其实就是找到import 对应节点下的引入其他模块的路径

image.png

通过 babel-traverse 找到 AST 里面 import 对应的信息

const traverse = require('babel-traverse').default;
//遍历找到 import 节点
traverse(ast,{
    ImportDeclaration:({ node })=>{
        console.log(node)
    }
})

输出看下节点打印的结构

Node {
  type: 'ImportDeclaration',
  start: 0,
  end: 29,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 29 }
  },
  specifiers: [
    Node {
      type: 'ImportDefaultSpecifier',
      start: 7,
      end: 13,
      loc: [SourceLocation],
      local: [Node]
    }
  ],
  source: Node {
    type: 'StringLiteral',
    start: 19,
    end: 29,
    loc: SourceLocation { start: [Position], end: [Position] },
    extra: { rawValue: './animal', raw: "'./animal'" },
    value: './animal'
  }
}

可以看到 node.source.value 就是 animal 模块的路径,需要的就是它。

扩展入口文件功能,解析 import 下的 JS 模块,

添加 require 方法

//完善代码
function perfectCode(code){
    let exportsCode = `
        //添加require方法
        let require = function(path){
            return {}
        }

        let exports = (function(exports,require){
            ${code}
            return exports
        })({},require)
    `
    return exportsCode
}

这样转换完的 main.js 给不会报错了,但是,这里需要解决怎么让 require 方法返回 animal 对象

let require = function(path){
    return {}
}

let exports = (function(exports,require){
    "use strict";
    Object.defineProperty(exports, "__esModule", {
        value: true
    });

    var _animal = require("./animal");
    var _animal2 = _interopRequireDefault(_animal);
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
    var person = { name: 'wsl', pet: _animal2.default };
    exports.default = person;
    return exports
})({},require)

下面就需要添加 require 方法进行 animal 对象的返回逻辑

//引入模块路径
let importFilesPaths = []
//引入路径下的模块代码
let importFilesCodes = {}

//获取import节点,保存模块路径
traverse(ast,{
    ImportDeclaration:({ node })=>{
        importFilesPaths.push(node.source.value)
    }
})

//解析import逻辑
function perfectImport(){
    //遍历解析import里面对应路径下的模块代码
    importFilesPaths.forEach((path)=>{
        let content = fs.readFileSync(path + '.js','utf-8')
        let ast = babylon.parse(content,{
            sourceType:'module'
        })
        let { code } = transformFromAst(ast, null, {
            presets: ['es2015']
        });
        //转换code
        code = perfectImportCode(code)
        importFilesCodes[path] = code
    })
}

//完善import代码
function perfectImportCode(code){
    let exportsCode = `(
        function(){
                let require = function(path){
                    let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
                    return exports
                }
                return (function(exports,require){${code}
                    return exports
                })({},require)
            }
        )()
    `
    return exportsCode
}

//完善最终输出代码
function perfectCode(code){
    let exportsCode = `
        let require = function(path){
            let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
            return exports
        }
        let exports = (function(exports,require){
            ${code}
            return exports
        })({},require)
        console.log(exports.default)
    `
    return exportsCode
}

上面的代码其实没有什么特别难理解的部分,里面的自执行闭包看着乱,最终的目的也很清晰,就是找到对应模块下的文件 code 代码进行自运行返回一个对应的模块对象即可。

看下转换后的 main.js 代码

let require = function(path){
    let exports = (function(){ return eval({"./animal":"(\n function(){\n let require = function(path){\n let exports = (function(){ return eval({}[path])})()\n return exports\n }\n return (function(exports,require){\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar animal = { name: 'dog' };\n\nexports.default = animal; \n return exports\n })({},require)\n }\n )()\n "}[path])})()
    return exports
}

let exports = (function(exports,require){
    "use strict";
    Object.defineProperty(exports, "__esModule", {
    value: true
    });

    var _animal = require("./animal");
    var _animal2 = _interopRequireDefault(_animal);

    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }


    var person = { name: 'wsl', pet: _animal2.default };
    exports.default = person;
    return exports
})({},require)
console.log(exports.default)

刷新浏览器,打印结果如下:

image.png

可以看到,pet 属性被赋予了新值。

三、完整的入口文件代码

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const { transformFromAst } = require('./babel-core');
//解析person文件
let content = fs.readFileSync('./person.js','utf-8')
let ast = babylon.parse(content,{
    sourceType:'module'
})

//引入模块路径
let importFilesPaths = []
//引入路径下的模块代码
let importFilesCodes = {}

//保存import引入节点
traverse(ast,{
    ImportDeclaration:({ node })=>{
        importFilesPaths.push(node.source.value)
    }
})

//person.js 对应的code
let { code } = transformFromAst(ast, null, {
    presets: ['es2015']
});

//解析import逻辑
function perfectImport(){
    importFilesPaths.forEach((path)=>{
        let content = fs.readFileSync(path + '.js','utf-8')
        let ast = babylon.parse(content,{
            sourceType:'module'
        })
        let { code } = transformFromAst(ast, null, {
            presets: ['es2015']
        });
        code = perfectImportCode(code)
        importFilesCodes[path] = code
    })
}

//完善import代码
function perfectImportCode(code){
let exportsCode = `
    (
    function(){
        let require = function(path){
        let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
            return exports
        }
        return (function(exports,require){${code}
            return exports
        })({},require)
        }
    )()
    `
    return exportsCode
}

//开始解析import逻辑
perfectImport()

//完善最终代码
function perfectCode(code){
    let exportsCode = `
        let require = function(path){
            let exports = (function(){ return eval(${JSON.stringify(importFilesCodes)}[path])})()
            return exports
        }
        let exports = (function(exports,require){
            ${code}
            return exports
        })({},require)
        console.log(exports.default)
    `
    return exportsCode
}

//最后的代码
code = perfectCode(code)

//删除文件操作
const deleteFile = (path)=>{
    if(fs.existsSync(path)){
        let files = []
        files = fs.readdirSync(path)
        files.forEach((filePath)=>{
            let currentPath = path + '/' + filePath
            if(fs.statSync(currentPath).isDirectory()){
                deleteFile(currentPath)
            } else {
                fs.unlinkSync(currentPath)
            }
        })
        fs.rmdirSync(path)
    }
}

deleteFile('cache')

//写入文件操作
fs.mkdir('cache',(err)=>{
        if(!err){
            fs.writeFile('cache/main.js',code,(err)=>{
            if(!err){
                console.log('文件创建完成')
            }
        })
    }
})

四、总结与思考

古代钻木取火远比现代打火机烤面包的意义深远的多。这个世界做过的事情没有对或错之分,但有做与不做之别。代码拙劣,大神勿笑[抱拳][抱拳][抱拳]

更多node相关知识,请访问:nodejs 教程

相关专题

更多
Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

8

2026.01.15

公务员递补名单公布时间 公务员递补要求
公务员递补名单公布时间 公务员递补要求

公务员递补名单公布时间不固定,通常在面试前,由招录单位(如国家知识产权局、海关等)发布,依据是原入围考生放弃资格,会按笔试成绩从高到低递补,递补考生需按公告要求限时确认并提交材料,及时参加面试/体检等后续环节。要求核心是按招录单位公告及时响应、提交材料(确认书、资格复审材料)并准时参加面试。

44

2026.01.15

公务员调剂条件 2026调剂公告时间
公务员调剂条件 2026调剂公告时间

(一)符合拟调剂职位所要求的资格条件。 (二)公共科目笔试成绩同时达到拟调剂职位和原报考职位的合格分数线,且考试类别相同。 拟调剂职位设置了专业科目笔试条件的,专业科目笔试成绩还须同时达到合格分数线,且考试类别相同。 (三)未进入原报考职位面试人员名单。

58

2026.01.15

国考成绩查询入口 国考分数公布时间2026
国考成绩查询入口 国考分数公布时间2026

笔试成绩查询入口已开通,考生可登录国家公务员局中央机关及其直属机构2026年度考试录用公务员专题网站http://bm.scs.gov.cn/pp/gkweb/core/web/ui/business/examResult/written_result.html,查询笔试成绩和合格分数线,点击“笔试成绩查询”按钮,凭借身份证及准考证进行查询。

11

2026.01.15

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

65

2026.01.14

php与html混编教程大全
php与html混编教程大全

本专题整合了php和html混编相关教程,阅读专题下面的文章了解更多详细内容。

36

2026.01.13

PHP 高性能
PHP 高性能

本专题整合了PHP高性能相关教程大全,阅读专题下面的文章了解更多详细内容。

75

2026.01.13

MySQL数据库报错常见问题及解决方法大全
MySQL数据库报错常见问题及解决方法大全

本专题整合了MySQL数据库报错常见问题及解决方法,阅读专题下面的文章了解更多详细内容。

21

2026.01.13

PHP 文件上传
PHP 文件上传

本专题整合了PHP实现文件上传相关教程,阅读专题下面的文章了解更多详细内容。

35

2026.01.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Node.js 教程
Node.js 教程

共57课时 | 8.6万人学习

CSS3 教程
CSS3 教程

共18课时 | 4.6万人学习

Vue 教程
Vue 教程

共42课时 | 6.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号