第一次用小爱课程表导入树维系统的课表时,我就被泼了一盆冷水——内置浏览器里那个课程表页面死活显示不出来。这感觉就像你兴冲冲打开外卖APP准备点餐,结果发现商家页面一直转圈圈,饿着肚子干着急。
树维系统在电脑浏览器上跑得好好的,怎么到了小爱课程表里就罢工了?我对比了两种访问方式,发现关键差异在请求处理上。电脑浏览器会完整加载所有资源文件,而小爱课程表的内置浏览器像是个挑食的孩子,某些关键请求直接被它跳过了。最要命的是获取课程数据的第15号POST请求,这个承载着全部课程信息的核心请求在小爱环境下压根没发出去。
这种情况在教务系统适配中很典型。很多老牌教务系统前端代码写得很"随性",严重依赖特定浏览器环境。就像你用新款咖啡机去冲泡传统茶包,机器再高级也可能因为适配问题泡不出味道。树维系统那个藏在iframe里的js脚本,还有依赖jQuery的DOM操作,都是典型的"历史包袱"。
抓包工具就像X光机,能让我们看清正常访问时浏览器和服务器之间的所有对话。我用Fiddler同时抓取电脑和手机端的请求,发现电脑端比手机端多发了5个关键请求。这就像对比两个不同厨师做同一道菜,少放调料的那个肯定味道不对。
重点观察第15号请求,它的POST数据里藏着两个命门:
semester.id=48:这个数字对应2020-2021学年第二学期ids=1234567:学生的唯一标识符更 tricky 的是这个ids的获取方式。它在页面里一段js代码中硬编码写着:
javascript复制function searchTable(){
if(jQuery("#courseTableType").val()=="std"){
bg.form.addInput(form,"ids","1234567");
}
}
但直接用document.getElementsByTagName('script')经常抓不到这段代码,因为它藏在iframe里。这就好比你去图书馆找书,管理员告诉你书在A区3架,但没说是藏在某个暗格里。
为了确保在各种环境下都能拿到ids,我设计了两套方案:
代码实现上大概长这样:
javascript复制function getIds() {
// 方案一:直接查找
let scripts = document.getElementsByTagName('script');
for(let script of scripts) {
let match = script.textContent.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
if(match) return match[1];
}
// 方案二:iframe查找
let iframe = document.getElementById('iframeId');
if(iframe) {
let iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
let iframeScripts = iframeDoc.getElementsByTagName('script');
for(let script of iframeScripts) {
let match = script.textContent.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
if(match) return match[1];
}
}
throw new Error('获取ids失败');
}
拿到ids只是第一步,真正的挑战在于模拟那个关键的第15号请求。这个请求需要以下参数:
semester.id:学期ID,会随学期变化ids:学生唯一IDignoreHead:固定值1setting.kind:固定值"std"我最初尝试用jQuery的$.ajax,结果在小爱环境里直接报错。后来改用原生XMLHttpRequest才搞定:
javascript复制function fetchCourseTable(ids, semesterId) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('POST', 'courseTableForStd!courseTable.action', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => {
if(xhr.status === 200) resolve(xhr.responseText);
else reject(new Error(`请求失败: ${xhr.status}`));
};
xhr.onerror = () => reject(new Error('网络错误'));
let params = new URLSearchParams();
params.append('semester.id', semesterId);
params.append('ids', ids);
params.append('ignoreHead', '1');
params.append('setting.kind', 'std');
xhr.send(params.toString());
});
}
学期ID这个参数特别烦人,每个学期都要手动更新。通过分析第12号请求的响应,我发现其中包含所有学期的ID映射关系。虽然可以写代码自动解析,但考虑到这个结构十年不变,我最终选择硬编码:
javascript复制const SEMESTER_MAP = {
'2020-2021-1': 47,
'2020-2021-2': 48,
'2021-2022-1': 74,
// 其他学期...
};
function getCurrentSemesterId() {
let now = new Date();
let year = now.getFullYear();
let month = now.getMonth() + 1;
let semester = month >= 9 ? '1' : '2'; // 9月后算第一学期
let key = `${year-1}-${year}-${semester}`;
return SEMESTER_MAP[key] || SEMESTER_MAP['2020-2021-2']; // 默认值
}
服务器返回的HTML里藏着一段神奇的js代码,它用二维数组存储所有课程信息。以这个片段为例:
javascript复制// 周三第0节和第1节的网络营销课
activity = new TaskActivity(
"7357", // 老师ID
"罗翔", // 老师姓名
"13015(02365)", // 课程代码
"网络营销", // 课程名称
"8866", // 课程类型代码
"新浪总部335", // 教室
"0000011110000", // 周次标记(1表示上课)
null, "", "", "", ""
);
table0.activities[2][0] = activity; // 周三第0节
table0.activities[2][1] = activity; // 周三第1节
这个数据结构有几个关键点:
activities[星期][节次]的二维结构解析这种非标准数据结构,正则表达式是最佳武器。我设计了三层过滤:
核心代码片段:
javascript复制function parseCourses(html) {
let pattern = /activity\s*=\s*new\s*TaskActivity\(([^)]+)\);[\s\S]*?table0\.activities\[(\d+)\]\[(\d+)\]/g;
let courses = [];
let match;
while(match = pattern.exec(html)) {
let args = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
let day = parseInt(match[2]);
let timeSlot = parseInt(match[3]);
let weekPattern = args[6]; // 周次字符串
let weeks = [];
for(let i = 0; i < weekPattern.length; i++) {
if(weekPattern[i] === '1') weeks.push(i + 1);
}
courses.push({
name: args[3],
teacher: args[1],
location: args[5],
day,
timeSlot,
weeks
});
}
return courses;
}
在小爱课程表环境里直接发XHR请求会遇到跨域问题。我的解决方案是利用小爱提供的JSBridge能力,通过原生层转发请求。这就像在两国边境设立特殊通道,让受限的物资也能正常流通。
关键代码:
javascript复制function safeFetch(url, data) {
if(window.MIJSBridge) {
return new Promise(resolve => {
MIJSBridge.call('httpRequest', {
method: 'POST',
url,
data,
success: resolve
});
});
} else {
// 降级方案
return fetchCourseTable(data.ids, data.semesterId);
}
}
频繁请求课表数据会给服务器造成压力,我实现了简单的本地缓存:
javascript复制const CACHE_KEY = 'course_cache';
const CACHE_EXPIRE = 3600 * 6; // 6小时
function getWithCache(ids, semesterId) {
let cache = localStorage.getItem(CACHE_KEY);
if(cache) {
cache = JSON.parse(cache);
if(Date.now() - cache.timestamp < CACHE_EXPIRE * 1000) {
return Promise.resolve(cache.data);
}
}
return fetchCourseTable(ids, semesterId).then(html => {
let data = parseCourses(html);
localStorage.setItem(CACHE_KEY, JSON.stringify({
data,
timestamp: Date.now()
}));
return data;
});
}
将所有模块组合起来,完整的处理流程如下:
初始化阶段:
数据获取阶段:
数据处理阶段:
结果输出阶段:
核心代码结构:
javascript复制class TJUAdapter {
constructor() {
this.semesterId = getCurrentSemesterId();
}
async getCourses() {
try {
let ids = await this.getIds();
let html = await this.fetchData(ids);
let courses = this.parseHtml(html);
return this.formatForXiaoAi(courses);
} catch(e) {
console.error('获取课表失败:', e);
return this.getFallbackData();
}
}
// 其他方法实现...
}
这个项目给我的最大启示是:解决兼容性问题就像做翻译工作,不仅要理解双方的语言习惯,还要能在不完美的条件下找到最优的沟通方式。有时候最直接的方法(比如iframe处理)反而比追求完美架构更有效。