UDN-企业互联网技术人气社区

板块导航

浏览  : 643
回复  : 3

[原生js] Architecture And Code Analysis Of Teaching Evaluation

[复制链接]
htmlman的头像 楼主
发表于 2017-1-10 15:14:45 | 显示全部楼层 |阅读模式

  大到一个企业级应用,小到类似于该一键评教软件,都有自己的软件架构设计。通常来说,对于同一个需求,实现方式是多种多样的。如何设计应用逻辑,如何组织代码模块,如何确定目录结构等等, 都需要在编码之前进行考虑。每个人的编码风格不尽相同,写出来的代码也各有千秋。要想得出一个最佳实践,就要不断总结自己的过往经验,学习别人的优秀设计,并再次将其运用于实践才能真正理解其中的奥义。

  本文主要就是介绍一键评教程序的软件结构设计,并对代码进行简要分析,同时也会讲述一些自己遇到的问题。

  1. 什么是一键评教

  首先声明,该程序的本质目的是用于学习交流。

  每学期进行教学评估的时候,都要评很多教师,每个教师都有很多选项,再加上教务系统网站比较老旧,操作不方面,评教总是要花很长时间。

  “一键评教,用过都说好”。线上地址 http://pj.fyscu.com ,源码 https://github.com/nodejh/teach_evaluation

  爱美之心人皆有之,我也很喜欢好用又好看的事物,所以我在写代码的时候也尽量做到好看又好用。软件截图如下,是不是很好看:
1.jpg

1.jpg

  2. 功能分析

  需求很明确,就是能够在一个网页上实现点击按钮自动评估所有教师。那么要实现这样的需求,该怎么去做呢?

  先来想想我们通常手动评教的步骤:

  • 登录进入教务系统网站
  • 找到教学评估链接并进入,这个时候就能看到所有需要进行评估的教师列表
  • 从教师列表中点击某个教师,进入到该教师的教学评估页面
  • 填写各种需要填写的表单
  • 填写完毕之后,点击提交按钮进行评教
  • 一切正常的情况下,则对该教师评教成功
  • 然后返回到教师列表页面,选择下一个需要评估的教师
  • 重复 3-7 这五个步骤,直到所有教师评估完毕

  要用程序实现一键评教,其实就是用程序模拟上面的步骤。所以程序要实现的主要功能有:

  • 模拟登录,获取 cookie
  • 获取需要评估的教师列表
  • 对列表中的每个教师进行评教

  然后我们需要一个用户界面来让用户进行操作。这个界面可以是 APP,也可以是网页。由于网页更方便更利于传播,所以我选择了网页。所以我们就还需要一个 HTTP 服务器,用来提供静态页面资源,并且接收并响应用户操作后发送的 HTTP 请求。

  3. 软件设计

  软件设计主要从三个方面来说明。一是技术选择,二是软件架构,三是目录结构。

  3.1 技术选择

  首先是各种技术的选择,包括前后端编程语言(语法)、第三方模块的选择、和服务器部署。

  3.1.1 后端语言

  该程序后端使用的是 Node.js。我的 Node.js 版本是 7.3。大量使用了 ES6 的语法,比如 Promise、模板字符串、箭头函数。只要你的 Node.js 版本 >= 6.0 应该都是可以运行的。

  3.1.2 第三方模块

  在开发程序之前,我想要尽量让应用的体积足够小,所以我尽量不使用第三方模块。

  最终我只使用了 cheerio 和 icon-lite 这两个第三方包。

  • cheerio 主要用来分析抓取到的 HTML 文档。其实最开始也想不用 cheerio ,直接使用正则表达式来分析页面的,但正则表达式编写麻烦,我的能力也有限,所以最终选择了使用 cheerio 。
  • icon-lite 则是用来对 GBK 文本进行解码。因为教务系统网站使用的是 GBK 的编码,所以直接抓取的结果是乱码的。除了 icon-lite 这个包,我找不到其他的可以解决乱码问题的方案了。

  3.1.3 编码规范

  然后使用了 ESLint 来规范代码。主要是用的是 airbnb 的 Eslint 规则,并且根据自己的喜好对 .eslintrc 作了配置。具体配置在源码 .eslintrc 中可以看到。

  3.1.4 前端技术

  为了减小代码体积,提高加载速度,节省带宽,前端没有使用任何第三方 JS 库。并且这只是一个小应用,有没有必须使用庞大(相比该程序而言显得庞大)的第三方库。

  前端使用的是 ES5 的语法。最开始想用 ES6 来写的,但 ES6 写好的代码还需要编译成 ES5 才能在浏览器运行,并且还需要引入各种 polyfill,最终还是决定使用 ES5。

  前端唯一使用了第三方资源的,只有两个字体图标了。一个是 heart 的图标 :heart:,毕竟是用心写的代码, Made with :heart: by nodejh ;还有一个就是“关闭”小图标。图标使用的是 IcoMoon 的字体图标库,可以自己在里面找到需要的图标然后下载使用。

  该程序只有 index.html 这一个 HTML 文件,所以本质上也是一个单页应用。所有的操作和交互都在这一个页面完成。

  3.1.5 前端代码压缩

  为了进一步压缩前端资源文件的体积,所以对静态资源进行了压缩。

  压缩 CSS 使用的是在线压缩工具 CSS Compressor 。

  压缩 JS 使用的是 UglifyJS2 。

  3.1.6 部署

  完成编码后,代码是部署在 Ubuntu 16.04 上的,然后使用了 pm2 进行进程的管理。

  3.2 软件架构

  程序的整体架构主要分为三层,可以就将其理解为 MVC 的三个层次。

  MVC 是一种设计模式,设计模式不是一层不变的,我们需要根据自己的实际业务灵活运用。MVC 是一个很经典的设计模式,生活中的很多事物,我们也可以根据 MVC 对其进行定义。就拿人来进行类比,大脑就是 C(Controller),控制着人的一切活动。躯体外表就是 V(View) 层,一方面是表现着一个人的外观,另一方面是人的各种活动的外在表现。体内各种器官比如心脏、肺等就相当于 M(Model),从表面可能并不能直观看到 M 层的作用,但它受大脑控制,进行着血液循环呼吸系统等重要功能,而这些器官可能又跟躯体相互作用,比如影响人的精神面貌或高矮胖瘦。

  说正经的。

  首先是该一键评教程序的 M 层,包括页面抓取、页面分析和评教等功能模块。

  然后是 V 层,主要是前端页面,直接给用户使用,与用户交互的界面。比如用户点击“开始”按钮的时候,就向 C 层发送一个 HTTP 请求。

  C 是控制中心,接收 V 层的 HTTP 请求,根据 HTTP 请求决定调用哪些 M 层的模块,然后将模块调用后的处理结果返回给 V。

  这样一个事件的处理流程可能就是:

  V(HTTP 请求)---> C (调用 M 的对应模块)---> M(返回处理结果) ---> C(HTTP 响应) ---> V

  3.3 目录结构

  了解了软件的整体架构之后,就来看看代码的目录结构,代码的目录结构也完美地印证了这三层架构。

  代码的主要目录/子目录及其功能如下:
  1. |____app.js  # 入口文件
  2. |____controller  # C 层目录,定义了各种控制器
  3. | |____evaluate.js  # 评教的控制器
  4. | |____evaluationList.js  # 获取需要评教列表的控制器
  5. | |____staticServer.js  # 静态服务器控制器
  6. |____helper  # 一些自定义的功能模块
  7. | |____colors.js  # 十六进制颜色代码,主要是为了改变 console.log 的颜色
  8. | |____dateformat.js  # 时间格式化
  9. | |____getContentType.js  # 获取文件后缀名对应的 Content-Type,用于静态服务器
  10. | |____log.js  # 自定义的彩色 console.log() 输出,告别满屏黑白日志
  11. | |____request.js  # HTTP 请求的封装
  12. |____models  # M 层目录,定义了各种模块及实现
  13. | |____evaluate.js  # 评教功能模块
  14. | |____getEvaluationList.js  # 获取需要评教的教师列表
  15. | |____loginZhjw.js  # 模拟登录教务系统
  16. | |____showEvaluatePage.js  # 显示某个具体的评教页面
复制代码

  看完目录结构,再回头看看软件的三层架构,肯定就清晰很多了。

  4. 代码分析

  接下来再对一些重要的功能模块以及涉及到的代码进行简要分析。相信了解完代码的执行流程之后,对软件的整体架构理解,定会再进一步。

  4.1 app.js

  app.js 是整个项目的入口文件,启动项目的时候使用 node app.js 即可启动。

  在 app.js 里面,主要是创建了 HTTP Server,然后根据请求的路径,调用对应的控制器:
  1. if (method === 'POST' && pathname === '/API/evaluationList') {
  2.     // 模拟登录,获取需要评教的老师列表
  3.     return evaluationListController(req, res);
  4.   }

  5.   if (method === 'POST' && pathname === '/api/evaluate') {
  6.     // 评教
  7.     return evaluateController(req, res);
  8.   }

  9.   if (method === 'GET') {
  10.     // 所有 GET 请求都当作是请求静态资源
  11.     return staticServerController(req, res);
  12.   }
复制代码

  当请求方法是 POST 且路径是 /api/evaluationList 时,就说明前端是发送的一个获取需要评估的教师列表的请求,所以紧接着执行 evaluationListController(req, res); ,调用该控制器,并且使用 return 来停止代码的执行。

  如果有新的 API 的请求,都可以在这里加。

  如果所有的自定义的请求及路径都不满足,并且请求的方法是 GET ,那就当作是请求静态资源文件,如 HTML、CSS、JS 或图片等。这里就调用 staticServerController(req, res) 。 staticServerController 是在 Controller 里面定义的返回静态文件的方法。

  如果 GET 请求也不是,则返回 400 Bad Request 。

  然后程序监听了 5000 端口,这样发送请求到 5000 端口,代码就能接收到请求并进行处理了。

  4.2 静态资源服务器

  前面已经提到了, staticServerController 是在 Controller 里面定义的返回静态文件的方法,也就是一个静态资源服务器。

  因为我们的软件很简单,所以完全没有必要使用 express 或 koa 等框架,自己写一个简单的静态服务器完全足够应对所有业务需求了。

  主要代码如下,代码优美,注释详尽,通俗易懂:
  1. /**
  2. * 静态服务器
  3. * @param  {object} req request
  4. * @param  {object} res response
  5. * @return {null}   null
  6. */
  7. const staticServerController = (req, res) => {
  8.   let pathname = url.parse(req.url).pathname;
  9.   if (path.extname(pathname) === '') {
  10.     // 没有扩展名,则指定访问目录
  11.     pathname += '/';
  12.   }

  13.   if (pathname.charAt(pathname.length - 1) === '/') {
  14.     // 如果访问的是目录,则添加默认文件 index.html
  15.     pathname += 'index.html';
  16.   }
  17.   // 拼接实际文件路径
  18.   const filepath = path.join(__dirname, './../public', pathname);
  19.   fs.access(filepath, fs.F_OK, (error) => {
  20.     if (error) {
  21.       res.writeHead(404);
  22.       res.end('<h1>404 Not Found</h1>');
  23.       return false;
  24.     }
  25.     const contentType = getContentType(filepath);
  26.     res.writeHead(200, { 'Content-Type': contentType });
  27.     // 读取文件流并使用管道将文件流传输到HTTP流返回给页面
  28.     fs.createReadStream(filepath)
  29.       .pipe(res);
  30.   });
  31. };
复制代码

  这里需要稍微留意的是 getContentType 这个方法,这个方法的定义和实现被放在了 helper/getContentType.js 里面,其主要作用,就是根据请求路径的后缀名来确定 HTTP Response 里面的 Content-Type 类型,以便浏览器或客户端识别:
  1. /**
  2. * 获取 Content-Type
  3. * @param  {string} filepath 文件路径
  4. * @return {string}          文件对应的 Conent-Type
  5. */
  6. const getContentType = (filepath) => {
  7.   let contentType = '';
  8.   const ext = path.extname(filepath);
  9.   switch (ext) {
  10.     case '.html':
  11.       contentType = 'text/html';
  12.       break;
  13.     case '.js':
  14.       contentType = 'text/JavaScript';
  15.       break;
  16.     case '.css':
  17.       contentType = 'text/css';
  18.       break;
  19.     case '.gif':
  20.       contentType = 'image/gif';
  21.       break;
  22.     case '.jpg':
  23.       contentType = 'image/jpeg';
  24.       break;
  25.     case '.png':
  26.       contentType = 'image/png';
  27.       break;
  28.     case '.ico':
  29.       contentType = 'image/icon';
  30.       break;
  31.     case '.manifest':
  32.       contentType = 'text/cache-manifest';
  33.       break;
  34.     default:
  35.       contentType = 'application/octet-stream';
  36.   }
  37.   return contentType;
  38. };
复制代码

  这样我们的一个简单的静态资源文件服务器就成型了,单独把这两段代码拿出去也是完全可以运行的。

  当用户请求 localhost:5000 的时候,根据上面的代码,就会去寻找 public/index.html 这个文件然后返回给客户端。

  index.html 就是我们的前端页面。

  4.3 模拟登录

  要想获取评教列表或进行评教,第一步就是登录教务系统。经抓包分析,教务系统使用的是 session cookie 的认证机制,关于如何抓包分析,可以看我的另一篇文章《模拟登录某某大学图书馆系统》[ http://nodejh.com/post/Crawler-for-SCU-Libirary/]。这一步我们需要获取登录后的 cookie 。

  登录的时候,是向 http://202.115.47.141/loginAction.do 发送的 POST 请求,请求的 Content-Type 是 application/x-www-form-urlencoded ,参数是 zjh=xx&mm=xx 。

  曾经教务系统可以使用 GET 方式登录,所有有一种快捷登录方式,就是在浏览器地址栏 http://202.115.47.141/loginAction.do?zjh=[你的学号]&&mm=[你的密码] 。而且这种方法可以绕过“登录人数已满”的限制。这在选课时期,这种强制的登录方式还是很好用的。不过 GET 方法也有缺点就是,你的学号和密码就直接暴露了,不安全。曾经还通过 Google 搜索,搜到了某个同学的账号及密码。现在教务系统估计是升级了禁止了这个方法。

  模拟登录教务系统的程序在 models/loginZhjw.js 里面,详细代码就不贴了,总的来说,就是通过 Node.js 的 HTTP 模块,设置一个自定义的 HTTP Headers 信息,然后发送 HTTP 请求。当然,其他任何编程语言道理都一样。

  模拟登录后,教务系统会返回 HTTP Response。HTTP Response 的 Content-Type 都是 text/html ,也就是说返回的始终都是 HTML 文本。所以我们就可以根返回的 HTML 文本的内容判断是否登录成功。

  如果文本包含下面 errorText 对象的属性字符串之一,都是登录失败:
  1. const errorText = {
  2.         number: '你输入的证件号不存在,请您重新输入!',
  3.         password: '您的密码不正确,请您重新输入!',
  4.         database: '数据库忙请稍候再试',
  5.         notLogin: '请您登录后再使用',
  6.       };
复制代码

  同时,也经过抓包发现,登录成功后返回的 HTML 文本的 title 部分是:

  <title>学分制综合教务</title>

  而其他情况都不是。所以就可以大致判断,除了上面几种 errorText 是登录失败之前,只有返回的 HTML 包含 <title>学分制综合教务</title> 才是返回成功。

  登录成功后的 HTTP Response Headers 部分含有一个 set-cookie 属性,而这个属性的值就是登录成功后的 cookie。我们抓那么多包,做了那么多准备,找的就是它。所以最终从登录成功的响应中取出 cookie 的代码如下:
  1. const cookie = result.headers['set-cookie'].join().split(';')[0];
复制代码

  之后获取需要评估的教师列表和评教,都需要在发送 HTTP 请求时在 HTTP Headers 里面带上该 cookie。

  4.3 获取需要评估的教师列表

  获取需要评估的教师列表就简单很多了,发送的是 GET 请求,然后在 HTTP Headers 里面设置 Cookie 即可,其头发送 HTTP 请求的头信息大概如下:
  1. const options = {
  2.     hostname: '202.115.47.141',
  3.     port: 80,
  4.     path: '/jxpgXsAction.do?oper=wjShow',
  5.     method: 'POST',
  6.     headers: {
  7.       Cookie: data.cookie,
  8.       'Content-Type': 'application/x-www-form-urlencoded',
  9.       'Content-Length': Buffer.byteLength(postData),
  10.     },
  11.   };
复制代码

  4.4 进行评教

  获取到教师列表之后,就可以进行评教了。但这里有一个坑,就是进行评教之前,必须先访问评教页面,再发送评教请求。不然是无法评教成功的。就是这个问题,导致我纠结了好久。

  也就是说,用程序模拟评教的时候,就要发送两个 HTTP 请求了,一是发送请求到某个老师的评教页面,对应的是 models/showEvaluatePage.js 这个文件;二是发送评教请求,对应的是 models/evaluate.js 。而且这两个请求都是 POST 类型的。所以代码类逻辑似于下面这样:
  1. // 显示评教页面
  2. showEvalutePage(data)
  3.     .then(() => {
  4.     // 评教
  5.     return evaluate(data)
  6.     })
  7.     .then((result) => {
  8.     // 评教结果
  9.     })
  10.         .catch((exception) => {
  11.             // 捕获异常
  12.         });
复制代码

  4.5 public/js/style.js

  前端的 JS 代码都在 style.js 这个文件里面了,主要就是监听了按钮的点击事件,然后发送 HTTP 请求,并根据请求结果增删页面的 DOM。

  前端由于没有使用 jQuery 等第三方库,所以操作 DOM 和事件监听都是原生 JS 实现的。发送 AJAX 请求也是自己封装的 XHR 对象。

相关帖子

发表于 2017-1-10 15:15:14 | 显示全部楼层
感觉JavaScript很有前途
使用道具 举报

回复

发表于 2017-1-22 22:01:45 | 显示全部楼层
总觉得哪里有点问题啊
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部