前言
大家好,我是鲫小鱼。是一名不写前端代码
的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!
第十四章:模块化与脚本复用
一、理论讲解:模块化开发的思想与优势
1.1 什么是模块化?
模块化是将一个复杂的程序,按照功能或职责,拆分成独立的、可重用的、相互协作的模块单元。每个模块只负责特定的功能,对外暴露接口,对内封装实现细节。
1.2 为什么需要模块化?
- 代码复用:提高代码的可复用性,避免重复编写相似功能。
- 维护性:降低代码的耦合度,使代码更容易理解、修改和维护。
- 可读性:清晰的代码结构有助于团队协作和项目管理。
- 避免命名冲突:模块内部的变量和函数不会污染全局作用域。
- 按需加载:只加载需要的模块,减少内存占用和启动时间。
- 团队协作:多人开发时,可以专注于各自模块,减少冲突。
1.3 Auto.js 中的模块化机制
Auto.js 支持 CommonJS 规范的 require
机制,允许脚本通过 module.exports
或 exports
导出功能,并通过 require()
导入使用。这使得我们可以像 Node.js 一样组织脚本,实现模块化开发。
二、代码示例:模块化与脚本复用全场景实战
2.1 基础模块导出与导入
javascript
// modules/utils.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add: add,
subtract: subtract
};
// main.js
const utils = require('./modules/utils.js');
log("加法:" + utils.add(5, 3));
log("减法:" + utils.subtract(10, 4));
2.2 导出单个函数或变量
javascript
// modules/greeting.js
module.exports = function(name) {
return "你好," + name + "!";
};
// main.js
const greet = require('./modules/greeting.js');
log(greet("鲫小鱼"));
2.3 模块内部数据与对外接口
javascript
// modules/dataStore.js
let _data = {}; // 私有数据
function set(key, value) {
_data[key] = value;
}
function get(key) {
return _data[key];
}
module.exports = {
set: set,
get: get
};
// main.js
const ds = require('./modules/dataStore.js');
ds.set("username", "AutojsUser");
log("用户名:" + ds.get("username"));
// log(_data); // 报错,_data是私有的
2.4 依赖其他模块的模块
javascript
// modules/logger.js
function log(msg) {
console.log(`[LOG] ${new Date().toLocaleTimeString()} ${msg}`);
}
module.exports = { log };
// modules/network.js
const logger = require('./logger.js');
function fetchData(url) {
logger.log(`开始从 ${url} 获取数据...`);
// 模拟网络请求
sleep(1000);
logger.log("数据获取完成。");
return "some data";
}
module.exports = { fetchData };
// main.js
const network = require('./modules/network.js');
network.fetchData("https://api.example.com");
2.5 模块路径解析与常见问题
require('./path/to/module.js')
:相对路径,常用。require('/absolute/path/to/module.js')
:绝对路径。- 建议使用相对路径,确保脚本在不同设备上的兼容性。
javascript
// main.js 导入当前目录下的 module.js
const myModule = require('./myModule.js');
// main.js 导入上级目录下的 otherModule.js
const otherModule = require('../otherModule.js');
2.6 模块的循环依赖与解决方案
循环依赖可能导致模块加载顺序混乱或 undefined 错误。Auto.js 会返回一个未完全加载的模块对象,需要特别注意。
javascript
// a.js
var b = require('./b.js');
log("a.js load");
exports.done = false;
log('in a, b.done = %j', b.done);
exports.done = true;
// b.js
var a = require('./a.js');
log("b.js load");
exports.done = false;
log('in b, a.done = %j', a.done);
exports.done = true;
// main.js
log('main starting');
var a = require('./a.js');
var b = require('./b.js');
log('in main, a.done=%j, b.done=%j', a.done, b.done);
/*
输出:
main starting
b.js load
in b, a.done = false
b.js load
a.js load
in a, b.done = true
in main, a.done=true, b.done=true
*/
解决方案:
- 重新设计模块结构,避免循环依赖。
- 延迟 require:在函数内部 require 模块,而不是在顶部。
2.7 动态加载模块与条件加载
javascript
// 根据条件动态加载模块
let env = "prod"; // or "dev"
let configModule;
if (env === "dev") {
configModule = require('./config_dev.js');
} else {
configModule = require('./config_prod.js');
}
log("加载的配置:" + JSON.stringify(configModule.settings));
// config_dev.js
module.exports = { settings: { debug: true, api: "dev.api.com" } };
// config_prod.js
module.exports = { settings: { debug: false, api: "prod.api.com" } };
2.8 模块化组件与 UI 复用
javascript
// components/MyButton.js
function createButton(text, onClick) {
return <button text={text} onClick={onClick} />;
}
module.exports = { createButton };
// main.js
"ui";
const MyButton = require('./components/MyButton.js');
ui.layout(
<vertical padding="16">
{MyButton.createButton("点击我", () => {
toast("按钮被点击了");
})}
{MyButton.createButton("另一个按钮", () => {
log("另一个按钮被点击了");
})}
</vertical>
);
2.9 发布与共享模块
- 将通用模块打包成
.js
文件或文件夹。 - 可以在 Auto.js 社区、GitHub 等平台分享。
- 建议附带
README.md
说明模块功能、使用方法、依赖等。
2.10 ES6 模块的未来展望 (虽然 Auto.js 暂不支持)
虽然 Auto.js 当前主要支持 CommonJS,但了解 ES6 模块(import
/export
)有助于未来发展,它提供了更强大的静态分析和 Tree Shaking 能力。
三、实战项目:通用弹窗处理模块
3.1 项目需求
开发一个通用的弹窗处理模块,能够自动识别并关闭常见的系统弹窗、广告弹窗、升级提示等。该模块应具备高可复用性,可被其他脚本轻松引入。
3.2 项目结构
plaintext
autojs-modules-demo/
├── main.js
├── modules/
│ ├── popupHandler.js
│ └── logger.js
├── logs/
│ └── app.log
└── README.md
3.3 logger.js 日志模块 (复用)
javascript
// modules/logger.js
function log(msg, level = "INFO") {
let line = `[${new Date().toLocaleTimeString()}][${level}] ${msg}`;
files.append("../logs/app.log", line + "\n");
console.log(line); // 同时输出到控制台
}
module.exports = { log };
3.4 popupHandler.js 弹窗处理模块
javascript
// modules/popupHandler.js
const logger = require('./logger.js');
const COMMON_POPUP_KEYWORDS = [
"关闭", "取消", "我知道了", "以后再说", "跳过", "不再提示", "确定",
"允许", "拒绝", "同意", "拒绝并退出", "稍后", "稍后再说"
];
let watcherThread = null;
/**
* 启动弹窗监控线程
* @param {Array<string>} customKeywords 自定义弹窗关键词
* @param {number} interval 检测间隔(毫秒)
*/
function startPopupWatcher(customKeywords = [], interval = 1000) {
if (watcherThread && watcherThread.isAlive()) {
logger.log("弹窗监控已在运行,无需重复启动。", "WARN");
return;
}
const allKeywords = COMMON_POPUP_KEYWORDS.concat(customKeywords);
watcherThread = threads.start(function() {
logger.log("弹窗监控线程已启动。");
while (true) {
let handled = false;
for (let i = 0; i < allKeywords.length; i++) {
let keyword = allKeywords[i];
let uiObject = text(keyword).findOne(200) || desc(keyword).findOne(200);
if (uiObject) {
try {
uiObject.click();
logger.log(`自动关闭弹窗:'${keyword}'`, "INFO");
handled = true;
break; // 找到并点击一个就退出当前循环,等待下次检测
} catch (e) {
logger.log(`点击弹窗失败 '${keyword}': ${e}`, "ERROR");
}
}
}
if (handled) {
sleep(interval + 500); // 弹窗处理后稍微多等一下,避免误触
} else {
sleep(interval);
}
// 检测线程是否被中断
if (threads.currentThread().isInterrupted()) {
logger.log("弹窗监控线程已中断。", "INFO");
break;
}
}
});
logger.log("弹窗监控线程已成功启动。", "INFO");
}
/**
* 停止弹窗监控线程
*/
function stopPopupWatcher() {
if (watcherThread && watcherThread.isAlive()) {
watcherThread.interrupt(); // 中断线程
watcherThread = null;
logger.log("弹窗监控线程已停止。", "INFO");
} else {
logger.log("弹窗监控未运行。", "WARN");
}
}
module.exports = {
startPopupWatcher,
stopPopupWatcher
};
3.5 main.js 主入口脚本
javascript
// main.js
const popupHandler = require('./modules/popupHandler.js');
const logger = require('./modules/logger.js');
// 启动弹窗监控,可以传入自定义关键词
popupHandler.startPopupWatcher(["立即升级", "领取奖励"], 800);
logger.log("主脚本开始执行...");
// 模拟主流程操作
for (let i = 0; i < 5; i++) {
logger.log(`主流程执行第 ${i + 1} 步...`);
sleep(2000);
// 模拟出现一个弹窗
if (i === 2) {
// 为了演示,这里我们手动模拟一个UI,实际中是App界面弹窗
threads.start(function(){
"ui";
ui.layout(<frame><button id="closeBtn" text="关闭" w="100dp" h="50dp"/></frame>);
ui.closeBtn.on("click",()=>{ ui.finish(); });
setTimeout(() => ui.finish(), 3000); // 3秒后自动消失
});
logger.log("模拟弹窗出现。");
}
}
logger.log("主脚本执行完毕。");
// 停止弹窗监控(可选,如果主脚本结束后不再需要)
// popupHandler.stopPopupWatcher();
3.6 运行效果与分析
- 运行
main.js
后,弹窗监控线程会在后台持续运行。 - 当模拟的"关闭"弹窗出现时,弹窗处理模块会自动识别并点击关闭按钮。
- 日志模块会记录弹窗处理的历史,方便调试。
- 该模块可以在其他 Auto.js 脚本中被
require
引入,实现弹窗处理的复用。
四、常见问题与解决方案
问题 | 解决方案 |
---|---|
require 路径错误/模块找不到 |
检查模块文件路径是否正确,注意相对路径和绝对路径的使用 |
模块变量污染/命名冲突 | 确保模块导出明确,避免全局变量;使用 let /const 声明局部变量 |
循环依赖导致 undefined |
重新设计模块结构,避免循环依赖;或使用延迟 require |
模块加载性能问题 | 避免加载不必要的模块;优化模块内部逻辑;按需加载 |
模块更新后不生效 | Auto.js 默认有模块缓存,重启脚本或清除缓存尝试 |
模块化后调试困难 | 利用日志模块详细记录模块间的调用和数据流 |
线程安全问题(多线程模块) | 对共享资源加锁或使用同步机制,避免数据竞争 |
模块功能单一,复用性差 | 提取通用逻辑,使模块功能更抽象、更灵活 |
UI 模块无法直接操作 UI | UI 操作必须在主线程执行,子线程通过 ui.run() 调度 |
五、性能优化建议
- 按需加载 :只在需要时
require
模块,尤其对于大型或不常用的模块。 - 精简模块:模块功能应尽可能单一,避免大而全的"巨石模块"。
- 避免不必要的
require
:如果在循环或高频函数内部require
模块,可能造成性能开销。 - 缓存模块引用 :一旦
require
过的模块会被缓存,不要重复require
,直接使用已有的引用。 - 优化模块内部逻辑:确保模块内部的代码高效,减少不必要的计算和 I/O。
- 使用
console.log
进行调试 :避免在生产环境大量使用toast
,它会占用 UI 线程资源。 - 资源释放:模块中如果创建了持久性资源(如线程、定时器),确保在不再需要时及时释放。
- 错误处理:模块内部的错误应妥善处理并记录日志,避免因一个模块的崩溃导致整个脚本停止。
- 打包与压缩:对于大型项目,可以考虑将多个模块打包成一个文件,或对模块进行压缩混淆,减少文件大小和加载时间(Auto.js Pro 支持)。
最后感谢阅读!欢迎关注我,微信公众号:
《鲫小鱼不正经》
。欢迎点赞、收藏、关注,一键三连!!!