module 模块
module 模块
这一页专门讲 ScriptX 里“项目脚本模块”这一套,也就是:
require(...)exports.xxx = ...module.exports = ...
它讲的不是 Node.js 那整套完整模块系统,而是 ScriptX 当前项目工作区里的本地脚本模块加载机制。
如果你先只想记住核心结论,先看下面 10 条:
require(...)目前只负责加载当前项目目录里的脚本文件。- 它不是 npm,也不支持从网络、包管理器或
node_modules自动找包。 - 当前实现会把路径里的反斜杠
\统一转成/。 - 开头连续的
./会被去掉。 - 开头的
/也会被去掉,所以它最终还是按项目根目录相对路径理解。 - 如果你没写
.js,当前实现会自动补.js。 - 模块内容为空,或模块文件不存在时,
require(...)会返回null。 - 同一个模块重复
require(...)会直接返回缓存结果,不会重复执行文件内容。 - 当前项目模块包装函数只注入了
exports和module,没有注入__filename/__dirname/ 模块内私有require参数。 module.exports和exports.xxx都能用,但混用时要清楚最终谁是导出结果。
这页和其他页面的分工
- 这页:专门讲项目模块系统本身
- global-functions 全局函数:只保留
require(...)的快速索引 - project-system 项目系统:讲项目目录、
init.js/main.js、导出链这些更大的结构
require(path)
加载当前项目里的一个脚本模块。
参数
| 参数 | 类型 | 必填 | 可填值 | 说明 |
|---|---|---|---|---|
path | string | 是 | 项目内脚本相对路径 | 要加载的模块路径 |
返回值
| 返回值 | 含义 |
|---|---|
| 任意 JS 值 | 模块最终导出的结果 |
null | 当前不在项目环境、模块不存在、或文件内容为空 |
当前路径会怎么标准化
源码里 normalizeProjectModuleRequest(...) 会做这些事:
- 把
\统一替换成/ - 去掉字符串两端空白
- 把开头连续的
./去掉 - 去掉开头的
/ - 把中间的
/./压成/ - 把连续
//压成/
路径标准化示例
| 你写的路径 | 标准化后 |
|---|---|
"./lib/helper" | "lib/helper" |
"././lib/helper.js" | "lib/helper.js" |
"\\lib\\helper" | "lib/helper" |
"/lib/helper" | "lib/helper" |
"lib//helper" | "lib/helper" |
会自动补 .js
如果标准化后的路径不是以 .js 结尾,当前实现会自动拼上 .js。
require("lib/helper");
等价于按下面这个文件名去找:
lib/helper.js
模块到底从哪里找
当前实现会去读:
Project/<当前项目名>/<模块路径>
也就是项目根目录下的相对文件。
什么情况下会返回 null
当前脚本不属于项目
当前实现一开始就会判断 projectName 是否为空。
如果当前运行上下文没有项目名,直接返回 null。
传入空字符串
require("");
标准化后路径为空,会直接返回 null。
文件不存在
require("./not-exists");
如果底层读文件拿到的是空结果,当前实现会返回 null。
文件内容为空
哪怕文件真的存在,只要读出来是空文本,也会返回 null。
最常见写法
const helper = require("./lib/helper");
const helper = require("lib/helper.js");
一个稳妥的判空写法
const helper = require("./lib/helper");
if (!helper) {
throw new Error("helper 模块加载失败");
}
exports.xxx
exports 是模块默认给你的导出对象。
最常见用法就是给它挂字段、挂方法。
最常见写法
exports.sum = function(a, b) {
return a + b;
};
exports.name = "helper";
然后入口文件里:
const helper = require("./lib/helper");
log(helper.name);
log(helper.sum(2, 3));
什么时候适合用 exports.xxx
适合你要导出一组方法或一组字段时:
exports.parse = function(text) {
return text.trim();
};
exports.stringify = function(obj) {
return JSON.stringify(obj);
};
这种写法的好处是:
- 看起来直观
- 多方法模块很顺手
- 后续补新字段时不容易把整个导出结构推翻
module.exports
module.exports 是模块最终的完整导出值。
最常见写法:直接导出函数
module.exports = function installLoginHook() {
log("login hook module loaded");
};
入口文件:
const installLoginHook = require("./hooks/login");
installLoginHook();
也可以直接导出对象
module.exports = {
name: "helper",
add(a, b) {
return a + b;
}
};
也可以直接导出基础值
module.exports = "hello";
const text = require("./lib/text");
log(text);
module 当前有哪些字段
在当前项目模块实现里,module 只显式放了:
| 字段 | 类型 | 说明 |
|---|---|---|
module.exports | 任意 | 最终导出值 |
和插件模块系统不同,当前项目模块这条 require(...) 链并没有额外给你 module.filename、module.id、module.path 这些字段。
exports.xxx 和 module.exports
这是最容易被一句带过、但又最容易踩坑的一节。
当前包装方式
模块执行时,运行时会先创建:
const exports = {};
const module = { exports };
然后用近似下面的包装形式执行你的脚本:
(function(exports, module) {
// 你的模块源码
return module.exports;
})
所以最终 require(...) 拿到的,永远是:
module.exports
示例 1:只给 exports 挂字段
exports.a = 1;
exports.b = 2;
最终导出结果是:
{ a: 1, b: 2 }
示例 2:后面直接重写 module.exports
exports.a = 1;
module.exports = function() {
return "hello";
};
最终导出结果是函数,不再是前面的对象。
示例 3:混用时容易误判
module.exports = {
run() {
log("run");
}
};
exports.name = "demo";
这里最终导出的不是“函数对象再附带一个 name 字段”,因为 module.exports 已经被你改成了另一个新对象,后面再给旧的 exports 挂字段,不会自动同步到新的 module.exports 上。
新手最稳的规则
二选一就行:
- 要导出一组字段,就一直用
exports.xxx = ... - 要导出一个整体值,就直接写一次
module.exports = ...
模块缓存
同一个模块重复 require(...),当前实现会直接走缓存。
缓存键就是标准化后、并补上 .js 的模块名。
示例
const helper1 = require("./lib/helper");
const helper2 = require("lib/helper.js");
log(helper1 === helper2);
这意味着什么
模块初始化代码通常只跑一次
log("helper loaded");
exports.run = function() {};
第一次 require(...) 时会输出日志,后面再次 require(...) 通常不会重复执行。
模块内部状态会被复用
let count = 0;
exports.next = function() {
count += 1;
return count;
};
const counter1 = require("./lib/counter");
const counter2 = require("./lib/counter");
log(counter1.next()); // 1
log(counter2.next()); // 2
这不是“重新 new 了一份”,而是同一个缓存模块对象。
当前项目模块系统不支持什么
不支持 npm 包解析
下面这种当前不是同一套机制:
require("axios");
除非你项目目录里真的有一个叫 axios.js 的本地脚本模块,否则它不会像 Node.js 一样去包管理器目录里找。
不支持 __filename
当前项目 require(...) 包装函数只传了 exports 和 module。
不支持 __dirname
同上,当前没有把 __dirname 注入进来。
不支持模块内私有 require
当前包装不是:
(function(exports, module, require, __filename, __dirname) { ... })
而是:
(function(exports, module) { ... })
所以这条项目模块链不是插件模块系统那套更完整的 CommonJS 包装。
最常见目录组织方式
工具模块
项目结构:
Project/MyDemo/
├─ main.js
└─ lib/
└─ helper.js
lib/helper.js
exports.sum = function(a, b) {
return a + b;
};
exports.clamp = function(value, min, max) {
return Math.max(min, Math.min(max, value));
};
main.js
const helper = require("./lib/helper");
log(helper.sum(3, 5));
log(helper.clamp(12, 0, 10));
按业务拆 Hook
Project/MyDemo/
├─ main.js
└─ hooks/
├─ login.js
└─ profile.js
main.js
const hookLogin = require("./hooks/login");
const hookProfile = require("./hooks/profile");
hookLogin();
hookProfile();
hooks/login.js
module.exports = function hookLogin() {
log("login hook ready");
};
集中导出 API
api/client.js
exports.baseUrl = "https://example.com";
exports.buildUserUrl = function(uid) {
return `${exports.baseUrl}/users/${uid}`;
};
main.js
const api = require("api/client.js");
log(api.buildUserUrl(1001));
一个完整示例:可复用模块 + 入口文件
第一步:写模块
lib/runtime-report.js
exports.printBasic = function() {
log(`package=${lpparam.packageName}`);
log(`process=${lpparam.processName}`);
};
exports.printClassLoader = function() {
log(String(lpparam.classLoader));
};
第二步:在入口里加载
main.js
const report = require("./lib/runtime-report");
if (!report) {
throw new Error("runtime-report 模块加载失败");
}
report.printBasic();
report.printClassLoader();
排错思路
require(...) 返回 null
优先检查:
- 当前是不是项目脚本环境
- 路径是不是写错了
- 文件是不是空内容
- 文件后缀和目录是不是对得上
明明改了模块代码,但效果像没变
优先想到缓存。
如果当前运行流程没有重建运行时,重复 require(...) 会直接拿缓存结果。
exports.xxx 和 module.exports 结果不一致
说明你很可能混用了两套写法,而且后面又重写了 module.exports。
