官方文档

provider-如何获取数据

开始

在本章节中,我们主要介绍比较常见的两种获取数据的办法:HTML文件获取、fetch数据获取

HTML文件获取

在课表加载完成之后,大部分系统会将数据以文本的形式呈现给我们

那么我们就可以直接把页面的数据截取并返回

但是要注意页面返回的编码不会被解析为乱码

第一步是声明provider.js的函数,代码运行的时候是调用文件中的函数进行运算。我们需要定义一个小爱课程表规定的函数

代码运行时通过这个函数来返回提供的数据,你可以在文件中写入多个函数,但是最终读取的是scheduleHtmlProvider()的结果。

接下来写入获取当前本地页面的功能,并返回为字符串(string),而且要注意到页面数据的格式,通过格式转换,确保显示不会是乱码。

该方法是通过截取本地的页面数据来获得包含课表的源数据,方法直接简单。容易上手。

而且在Provider中支持异步操作,那么花活就可以整起来了

在测试中,我们发现一些设备对于alert和prompt的支持并不是很好,甚至会导致代码运行失败,所以我们在这些设备上停用了它们。

作为替代,我们提供了一个“课表风格”的AIScheduleAlert和AISchedulePrompt来替代alert来进行导入前交互


async function scheduleHtmlProvider() {
	// 此步为必须,用于加载这个工具,才能进行弹窗交互,后续会增加更多方法
	await loadTool('AIScheduleTools')
	// 使用它们的时候务必带上await,否则没有系统alert的时停效果
	await AIScheduleAlert('这是一条提示信息')
	// Prompt的参数比较多,所以传了个对象,最后会返回用户输入的值
	const userInput = await AISchedulePrompt({
		titleText: '提示', // 标题内容,字体比较大,超过10个字不给显示的喔,也可以不传就不显示
		tipText: '这是一条提示信息', // 提示信息,字体稍小,支持使用``达到换行效果,具体使用效果建议真机测试,也可以不传就不显示
		defaultText: '默认内容', // 文字输入框的默认内容,不传会显示版本号,所以空内容要传个''
		validator: value => { // 校验函数,如果结果不符预期就返回字符串,会显示在屏幕上,符合就返回false
		  console.log(value)
		  if (value === '1') return '这里的结果不可以是1'
		  return false
		}})  //通过这里的工具,可以预先获取到部分数据中的变量,例如需要课表的范围、学年信息时,或者向用户传递消息
	
	//获取当前页面的 HTML 内容
	const htmlContent = document.documentElement.outerHTML;
	
	// 将 HTML 内容转换为 UTF-8 编码
	const encoder = new TextEncoder();
	const uint8Array = encoder.encode(htmlContent);
		
	// 解码为 UTF-8 字符串
	const decoder = new TextDecoder('utf-8');
	const utf8HtmlContent = decoder.decode(uint8Array);
		
	return utf8HtmlContent;
}

那么,我们提供一个简单的copy即用的本地HTML获取代码


async function scheduleHtmlProvider() {
	// 加载小爱组件
	await loadTool('AIScheduleTools');
	try {
	// 直接获取当前页面的 HTML 内容
	const htmlContent = document.documentElement.outerHTML;

	// 将 HTML 内容转换为 UTF-8 编码
	const encoder = new TextEncoder();
	const uint8Array = encoder.encode(htmlContent);

	// 解码为 UTF-8 字符串
	const decoder = new TextDecoder('utf-8');
	const utf8HtmlContent = decoder.decode(uint8Array);

	return utf8HtmlContent;
	} 
	
	catch (error) {
	//这里是通过try-catch来容错,可以避免用户在错误的地方点击导入按钮
	console.error('发生错误: ', error);
	await AIScheduleAlert("请登录后打开课程表详细界面导入" + error.message);
	return 'do not continue';
	}
}

当然,我们需要注意原HTML数据的格式,如果是GBK编码,那么需要转换一下,避免解析时因为格式错误而失败

同时也要避免html转换时的实体解码而导致的以下问题

这时就要在加载html源码时加入取消解码Unicode的命令 decodeEntities: false, xmlMode: true

你应该也注意到了,刚刚的示例代码中在错误时返回了do not continue,我们规定,如果在Provider代码中返回这个字符串,则停止下一步的操作,等待用户再次点按开始导入

这个设计是用来防止用户在非预定的界面点击导入从而导致导入失败

fetch数据获取

在有些学校的教务系统中,数据的传递使用xhr的方法。这样传递的json数据我们可以更加便捷地解析

我们在这时就需要用到浏览器开发者工具

通过这个方式,模拟浏览器打出请求,来获取课表数据

在检查界面,选择上方的 网络或Network 选项

到这个界面之后,刷新下网页,获取页面加载的全部数据

也可以直接勾选查找fetch/xhr

我们在这些文件中查找,直到找到包含课表数据的一项文件

这时,已经完成了大部分工作。我们右键这个文件,复制为fetch语句


fetch("http://jwxt.ctgu.edu.cn/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do", {
	"headers": {
	  "accept": "application/json, text/javascript, */*; q=0.01",
	  "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,en-GB;q=0.6,fr-FR;q=0.5,fr;q=0.4,zh-TW;q=0.3",
	  "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
	  "proxy-connection": "keep-alive",
	  "x-requested-with": "XMLHttpRequest"
	},
	"referrer": "http://jwxt.ctgu.edu.cn/jwapp/sys/wdkb/*default/index.do?THEME=cherry&EMAP_LANG=zh",
	"referrerPolicy": "strict-origin-when-cross-origin",
	"body": "XNXQDM=2024-2025-2",
	"method": "POST",
	"mode": "cors",
	"credentials": "include"
  });

现在,我们就可以将该语句作为获取课表数据的方式写入provider

我们观察这个语句结构,可以发现,在body部分写着一些调试信息

它规定了课表的范围,即从多个课表中返回的是2024-2025学年的第2学期的课表

那么我们在编写provider的时候就要加强这种获取方式的鲁棒性,不能因为时间变化而让项目不可用

因为工具包含了弹窗提醒

我们就可以通过这些方式来向用户收集信息,并将返回的信息拼接成一个新的body部分

通过这种方式来达到修改body部分,来适用不同时期的课表导入

当然,我们也可以通过时间来自动调整body部分,达到无感快速导入

例如,我们获取当前时间,根据时间判断课表的body部分

而有的学校在fetch时获取到的课表不需要任何时间判断,就是最新课表

这需要我们自己去判断,改写代码达到目的

这是完整的基于金智系统的provider部分


async function scheduleHtmlProvider() {
	// 加载小爱组件
	await loadTool('AIScheduleTools')
	url = 'http://jwxt.ctgu.edu.cn/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do'
	// 获取cookie和UA
	const cookie = document.cookie
	const UA = navigator.userAgent
	// 计算学期字段
	const d = new Date()
	var year = d.getFullYear()
	const month = d.getMonth() + 1
	var XNXQDM = 2024 - 2025 - 2
	var term = 1
	// 2-7月份视为第二学期
	if (month > 1 && month < 8) {
		term = 2
		year--
	}
	XNXQDM = year + "-" + (year + 1) + "-" + term
	try {
		const res = await fetch(url,
			{
				method: "post",
				mode: 'no-cors',
				credentials: "include",
				"Access-Control-Allow-Origin": "*",
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
					"Accept": "application/json, text/javascript, */*; q=0.01",
					"Accept-Encoding": "gzip, deflate",
					"Accept-Language": "zh-CN,zh;q=0.9",
					"Content-Length": "25",
					"Cookie": cookie,
					"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
					"Host": "jwxt.niit.edu.cn",
					"Origin": "http://jwxt.ctgu.edu.cn",
					"Referer": "http://jwxt.ctgu.edu.cn/jwapp/sys/wdkb/*default/index.do?EMAP_LANG=zh#/xskcb",
					"User-Agent": UA,
					"X-Requested-With": "XMLHttpRequest",
				},
				body: 'XNXQDM=' + XNXQDM
			})
		resJson = await res.json()
		return JSON.stringify(resJson)
	} catch (error) {
		// console.error(error)
		await AIScheduleAlert("请登录后打开课表界面导入" + error.message)
		return 'do not continue'
	}
}

请注意,无论你用什么方式,返回值必须是string(字符串),否则error