数据库

建立一张 student 表

字段:

id          学生 id, 使用无实际意义的字段, 作为主键
stu_num     学生学号, 可以唯一标识一位学生
name        
age
class       班级号
psw         密码

注意: 只是一个 demo, 密码也是明文, 没有MD5加密, ajax请求, 不管是get还是post, 都是基于http协议, 都是明文传输, 都不安全.

登录验证验证的就是根据学生学号和密码进行验证.

然后随意给这张表添加几条记录

DAO层, 连接数据库

DBUtil.js

// 数据库工具定义的 js 文件
const mysql = require('mysql');
function createConnection() {
    return mysql.createConnection({
        host: '127.0.0.1',
        port: '3306',
        user: 'root',
        password: 'vey0831',
        database: 'school'
    });
}

module.exports.createConnection = createConnection;

/*
    这个文件用来导出一个创建数据库连接的方法, 里面包含了连接数据库需要所有信息.
    每调用一次这个函数, 都会创建一个与 mysql 的连接
*/

studentDao.js

// 导入上面的 utils
const dbUtil = require('./dbutils');

// 此方法用来查询 student 表中所有的学生信息
function queryAllStudent(success) {
    let querySQL = "select * from student;";
    let connection = dbUtil.createConnection();
    connection.connect();
    connection.query(querySQL, function (error, data) {
        if(error){
            console.log(error);
        } else {
            // console.log(data);
            success(data);
        }
    });
    connection.end();
}

// 通过 stu_num 查询某个学生的信息
function queryStudentByStuNum(stuNum, success) {
    let querySQL = "select * from student where stu_num = ?;";
    let connection = dbUtil.createConnection();
    connection.connect();
    connection.query(querySQL, stuNum, function (error, data) {
        if(error){
            console.log(error);
        } else {
            success(data);
        }
    });
    connection.end();
}


// 通过某个班级号, 查询这个班级内所有的学生信息
function queryStudentByClass(classNum, age) {
    let querySQL = "select * from student where class = ? and age > ?;";
    // 传入多个参数, 用数组装起来传递给 query 函数
    let queryParams = [classNum, age];
    connection.connect();
    connection.query(querySQL, queryParams, function (error, data) {
        if(error){
            console.log(error);
        } else {
            console.log(data);
        }
    });
    connection.end();
}

// 将以上三个方法导出以备服务层使用
module.exports = {
    "queryAllStudent": queryAllStudent,
    "queryStudentByClass": queryStudentByClass,
    "queryStudentByStuNum": queryStudentByStuNum
};

​ 这里有两个需要注意的点:

​ 其一, 如果在 dbUtil 里面 直接导出一个创建好的 connection 对象, 那么就只可以查询一次数据库, 第二次查询将会报错, 因为每创建一次连接, 当查询完成之后, 是一定要将数据库连接关闭的, 否则当一千万个用户请求都需要查询的时候, 数据库的压力可想而知.

​ 所以在这里导出了一个函数, 当需要查询, 创建一个连接的时候, 再去实例化一个 connection (即函数执行, 返回一个 connection 对象)执行查询任务, 查询完了之后, 依然将它关闭.

let connection = dbUtil.createConnection();
// 每创建一次连接任务实例化一次 connection

​ 另外一个点是, 如果数据库查询, 需要传递参数, 不要拼串! 不要拼串! 不要拼串! 在数据库查询中, 拼串是一种很 low, 且很危险的行为, 主要是 SQL 注入.

服务层(service)

studentService.js

// 引入 DAO 层的方法
let studentDao = require('../dao/studentDao');

function queryAllStu(success) {
    studentDao.queryAllStudent(success);
}

function queryStuNum(stuNum, suc) {
    studentDao.queryStudentByStuNum(stuNum, suc);
}

module.exports = {
    queryAllStu: queryAllStu,
    queryStuNum: queryStuNum
};

// 这里是简单的查询任务, 在服务层的业务逻辑不算复杂, 所以只需要将上一层的方法在这里导出去就好了
// 但是一旦服务层的逻辑复杂了, 这里就需要写业务逻辑代码了

web层

loginController.js

const studentService = require('../service/studentService');
const url = require('url');
// 创建一个 map, 保存请求路径
// 例如, 用户需要登录, 那么请求就是这个样子的: 
// xhr.open('GET', '/login');
// 其中, '/login' 就是 map 的 key, login() 方法就是对应的处理函数
// 然后将其导出, 给服务器(server.js)使用
let path = new Map();

// 查询所有学生的方法
function getData(req, res) {

    studentService.queryAllStu(function (data) {
        let userArr = [];
        for (let i = 0; i < data.length; i ++){
            console.log(data[i].name);
            userArr.push(data[i].name);
        }
        res.writeHead(200);
        res.write(userArr.toString());
        res.end();
    });

}

// 通过 GET 方法发送数据给后台.
function login(req, res) {
    let query = url.parse(req.url, true).query;
    studentService.queryStuNum(query.username, function (result) {
        if(result == null || result.length === 0){

        } else {
            if(result[0].psw === query.password){
                res.writeHead(200);
                res.write("OK");
                res.end();
            } else {
                res.writeHead(200);
                res.write("Fail");
                res.end();
            }
        }


    })
}

// 通过 POST 请求发送数据给后台
function login(request, res) {
    request.on('data', function (data) {
        console.log(data.toString());
        let stuNum = data.toString().split('&')[0].split('=')[1];
        let password = data.toString().split('&')[1].split('=')[1];
        studentService.queryStuNum(stuNum, function (data) {
            if (data && data.length > 0) {
                if(password === data[0].psw){
                    res.writeHead(200);
                    res.write('OK');
                    res.end();
                } else {
                    res.writeHead(200);
                    res.write('Fail');
                    res.end();
                }
            }
        })

    })
}

path.set('/getData', getData);
path.set('/login', login);

module.exports.path = path;

服务器

server.js

const http = require('http');   // 创建 http 服务的模块
const url  = require('url');    // 解析请求 url 的模块
const fs = require('fs');       // 读取文件模块
const config = require('./config'); // 全局的配置文件, 读取 server.conf 导出
const loader = require('./loader'); // 加载文件的模块, 后面的 loader.js
const log = require('./log');   // 打日志

http.createServer((request, response) => {
    let pathName = url.parse(request.url).pathname;
    let params = url.parse(request.url, true).query;
    let isStatic = isStaticRequest(pathName);
    log(pathName);
    if(isStatic){ // 请求的是静态文件
        console.log(config["page_path"] + pathName);
        // 防止路径不对导致服务器停掉
        try {
            // 读取请求的静态文件, 将文件作为数据写回
            let data = fs.readFileSync(config["page_path"] + pathName);
            response.writeHead(200);
            response.write(data);
            response.end();
        }catch (e) {
            // 出错, 文件没有找到或者其他问题出现, 返回一个 404 页面
            response.writeHead(404);
            response.write("<html><body><h1>404 Not Found</h1></body></html>");
            response.end();
        }
    } else { // 请求动态数据
        if(loader.get(pathName) !== null){
            try {
                loader.get(pathName)(request, response);
            } catch (e) {
                response.writeHead(500);
                response.write("<html><body><h1>500 Bad Server</h1></body></html>");
                response.end();
            }
        } else {
            response.writeHead(404);
            response.write("<html><body><h1>404 Not Found</h1></body></html>");
            response.end();
        }
    }
}).listen(config["port"]);

log('服务已启动');

// 判断是否是静态文件的方法
// pathName.length - temp.length
// 例如: index.html?a=1&b=2 
// pathName: /index.html
// pathName.length: 11
// temp.length: 5
// pathName.indexOf('.html'): 6
// pathName.length - temp.length = 11 - 5 = 6

isStaticRequest = pathName => {
    for (let i = 0; i < config['static_file_type'].length; i ++){
        let temp = config['static_file_type'][i];
        if(pathName.indexOf(temp) === pathName.length - temp.length){
            return true;
        }
    }
    return false;
};

config.js

/*
    此文件的作用是对整个服务进行配置
    导出的 config 就是配置对象
    通过读取 config 里面的值
    在需要它的地方使用
    当配置改变, 只需要修改 server.conf 文件即可
 */

const fs = require('fs');

// 解析 server.conf 之后需要导出的配置对象
let config = {};
// 读取配置文件
let conf = fs.readFileSync('server.conf');
let confArr = conf.toString().split('\r\n');
for (let i = 0; i < confArr.length; i ++){
    let temp = confArr[i].split('=');
    if(temp.length === 2){
        config[temp[0]] = temp[1];
    }
}

// 将静态文件拆分成单个存在数组中
if (config["static_file_type"]){
    config["static_file_type"] = config["static_file_type"].split('|');
}else {
    // 如果没有静态文件, 那么肯定是有问题的
    throw new Error("配置文件异常, 缺少静态文件类型");
}
module.exports = config;

server.conf

port=8080
page_path=page
static_file_type=.html|.css|.js|.png|.jpg|.gif|.ico
web_path=web
log_path=log/
log_name=server.log

loader.js

const fs = require("fs");
const config = require('./config');

// readdir 读出的是某个目录下所有的文件
let files = fs.readdirSync(config['web_path']);

// web 层不可能只有一个 loginController, 还有其他的 controller
// 所以下面这是一个 controller 的集合, 在此 demo 中没有什么用, 但是业务复杂了, 就有用了
let controllerSet = [];
let pathMap = new Map();

for (let i = 0; i < files.length; i++){
    // 引入 web_path 路径下的所有文件
    let temp = require('./' + config['web_path'] + '/' + files[i]);
    // 如果有 path 这个属性, 
    if(temp.path){
        for (let [key, value] of temp.path){
            // 如果 pathMap 中没有, 就设置进去
            // 要放进 if 判断的原因是防止重复, 因为没有一个请求对应两个处理函数
            if(pathMap.get(key) == null){
                pathMap.set(key, value);
            } else {
                throw new Error('url path 异常, url:' + key);
            }
            controllerSet.push(temp);
        }
    }
}
module.exports = pathMap;

展示层

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title>Title</title>
</head>
<body>
    <input type="text" id="username" placeholder="用户名">
    <input type="password" id="password" placeholder="密码">
    <input type="submit" value="登录" onclick="login()">
    <!--<script type="text/javascript" src="./login.js"></script>-->
    <script type="text/javascript">
        function login() {
            let user = document.getElementById('username').value;
            let pasd = document.getElementById('password').value;
            let params = "?username=" + user + "&password=" + pasd;
            let xhr = new XMLHttpRequest();
            xhr.open('POST', '/login', true);
            xhr.send(params);
            xhr.onreadystatechange = function () {
                if(xhr.readyState === 4 && xhr.status === 200){
                    suc(xhr.responseText);
                }
            }
        }
        function suc(data) {
            if(data === 'OK'){
                alert("登录成功");
            } else {
                alert("登录失败, 用户名或密码错误!");
            }
        }
    </script>
</body>
</html>

总结:

业务流程:

展示层:

​ 输入stu_num, 密码, 点击提交, 触发login()方法, 发送 ajax 请求;

服务端:

​ 这个请求被 server 监听到, 进行处理(解析url), 判断出这是请求动态数据, 需要查询数据库;

web层:

​ 根据解析好的pathName, 去pathMap中调用对应的方法执行, 这里是{"/login" => login()}, 所以就触发了loginController.js中的 login 方法, login方法解析了POST传递过去的参数, 解析出stuNum以及password, 根据stuNum, 调用服务层的方法studentService.queryStuNum 去查询数据库, 并且需要在此传递过去回调函数;

服务层:

​ 这里的服务层没有什么业务逻辑, 只是将DAO层的的方法进行了一次转发, 这个层的作用在这个demo里面不明显.

DAO层:

​ 查询数据库, 将查询结果传递给web层的回调函数, 这里就回到了web层;

web层:

​ 在回调函数中判断, 用户输入的stuNum对应密码和数据库中的密码是否一致, 一直则res.write('OK'), 否则res.write('Fail'), 在这之后, 返回到 ajax 处, 此时已经能够拿到 xhr.response 了. 所以回到展示层.

展示层:

​ 根据 ajax 的返回结果, 判断是否成功登录.