美文网首页
从零开始搭建一个Express应用

从零开始搭建一个Express应用

作者: 喜欢喝橙汁的 | 来源:发表于2020-02-07 15:42 被阅读0次

    业务前景

    其实许多技术还是要应用到业务中去做,才会有不一样的挑战和收获,公司有自己内部管理系统,主要是用于客户维护,和核算成本。基于这样的情况,上方决定前端自己来建立和维护这样的系统,前两天重新搭建了一遍,现在打算整理出来,来一起讨论这个搭建过程。

    一,写一个hello world

    1,新建项目文件夹

    mkdir express-2020 && cd express-2020
    

    2,安装express

    yarn add express --save 或 npm install express --save
    

    3,新建server.js文件

    const express = require("express");
    const app = express();
    app.get("/",(req,res)=>{res.send("hello world")});
    app.listen("3000",()=>{    console.log("run at 3000")});
    

    终端执行

    node server
    

    我们可以看到,服务已经运行了

    image

    打开浏览器,输入地址 http://localhost:3000/,可以看到我们访问成功,hello world

    image

    到此为止我们已经实现了所有语言初始化的第一步,hello, world。

    二,访问静态文件

    1,使用express的static中间件函数

    const path = require("path");app.use(express.static(__dirname + '/static'))
    app.get('/*', function (req, res){    
        res.sendFile(path.resolve(__dirname, 'static', 'index.html'))
    })
    

    访问根路径之下任何路由返回的是绝对路径+“/static”下的index.html文件,接下来我们实验一下

    server.js同级下新增文件夹static,里面创建一个index.html文件,文件结构如下

    image

    重启服务

    node server
    

    效果如图所示

    image

    现在我们成功运行了一个本地服务,可以通过我们本地ip地址<u style="box-sizing: border-box;">localhost:3000</u>,访问到static文件下的静态资源,默认是index.html,如果是example.html则直接访问<u style="box-sizing: border-box;">localhost:3000/example.html,其实这时候</u>我们可以通过本地启动一个服务,来让同一局域网下的计算机访问我们的静态网页。

    三,写一个接口出来

    1,server.js同级目录下新增一个app文件夹,文件夹下新增index.js,文件目录此时如下

    image

    分成这样的项目结构,主要是为了server.js,做总的中间件的控制,在index.js中做路由的请求分发。

    代码如下:

    const express = require("express");
    const app = express();// 处理异常
    app.use((err,req,res,next)=>{    
        next(err);
    })
    export {app as serverIndex};
    

    通过app.use来捕获异常,如果没有next(err),这个异常会被挂起,不会被垃圾回收机制所回收,所有的中间件通过next()方法才会向下执行。

    将index.js引入到server.js中

    import {serverIndex} from "./app"; app.use(serverIndex);
    

    执行 node server,这时发现,报错了。

    import {serverIndex} from "./app"; 
    ^^^^^^
    
    SyntaxError: Unexpected token import
        at createScript (vm.js:80:10)
        at Object.runInThisContext (vm.js:139:10)
        at Module._compile (module.js:617:28)
        at Object.Module._extensions..js (module.js:664:10)
        at Module.load (module.js:566:32)
        at tryModuleLoad (module.js:506:12)
        at Function.Module._load (module.js:498:3)
        at Function.Module.runMain (module.js:694:10)
        at startup (bootstrap_node.js:204:16)
        at bootstrap_node.js:625:3
    

    报错的原因是import是es6语法中引入方式,此时我们项目不支持es6,咋办呢?

    办法总比困难多,编译一下就完了。(:

    2,通过babel将es6转为es5,安装babel

    npm i -D babel-cli babel-preset-es2015 babel-preset-stage-2
    

    然后在根目录下,新增.babelrc文件,代码如下:

    {     
        "presets": ["es2015", "stage-2"]
    }
    

    在package.json中新增如下代码

    "scripts": {        
        "start": "babel-node server.js --presets es2015,stage-2"
    }
    
    

    执行命令

    npm run start
    

    这时候发现运行起来了。

    3,新建路由文件login.js,和index.js同级

    async function getAsync(req,res){
        res.json(Object.assign({},{msg:"成功",code:0},{data:null}))
    }
    const wrap = fn => (...args) => fn(...args).catch(e=>{console.log(e)})
    let get = wrap(getAsync);
    

    通过wrap函数包裹住路由接口函数,可以及时捕获到异步错误。

    在index.js中,引入login.js中的login函数,这时候这是一个get请求,我们用postman试一下

    import * as user from "./login";
    app.get("/get",user.get);
    

    返回结果

    {
        "msg": "成功",
        "code": 0,
        "data": null
    }
    

    我们已经完成一个了一个简单的get请求。

    4,接下来我们来整一个post请求

    首先我们先安装一个中间件body-parser,将post请求携带的参数解析之后放到req.body中

    npm i body-parser
    

    在server.js中引入

    import bodyParser from 'body-parser';
    app.use(bodyParser.json({limit: '100mb'}));// 解析文本格式
    app.use(bodyParser.urlencoded({limit: '100mb', extended: true}));
    

    这里只是做了参数大小限制,更多api用法访问https://github.com/expressjs/body-parser

    继续在 login.js中新增一个login函数,为了方便我们对code和msg进行管理,我们和app文件夹同级新增一个config文件夹,文件夹下新增constants.js文件,里面放我们一些配置信息。

    文件目录如图所示

    image

    constants.js

    export const Success = {code:0,msg:"成功"};
    export const ErrorParam = {code:10001,msg:"参数错误"};
    export const ErrorAuthentication = {code:10002,msg:"无权限"};
    export const ErrorToken = {code:10003,msg:"token失效"};
    

    login.js

    import * as constants from "../config/constants";
    async function loginAsync(req,res){    
        let username = req.body.username;    
        let password = req.body.password;    
        if(!username||!password){        
             return res.json(Object.assign({},constants.ErrorParam,{data:null}));
        }    
        if(username=="123" && password=="1"){
            return res.json(Object.assign({},constants.Success,{data:null}));    
        }else{        
            return res.json(Object.assign({},constants.ErrorAuthentication,{data:null}));
        }
    }
    let login = wrap(loginAsync);
    export {login}
    

    接下来在index.js中新增路由

    app.post("/login",user.login);
    

    重启服务

    npm run start
    

    访问结果如图所示

    image

    现在我们已经实现了常用的两种请求,get,post。

    四,为请求添加log日志

    1,引入express中间件morgan(获取所有的请求)和winston

    npm install --save winston morgan
    

    server.js同级新建util文件夹,文件夹下新增logger.js,目录如下:

    image

    logger.js

    import fs from "fs";import {createLogger,format,transports} from "winston";fs.exists( __dirname + '/../../logs/all.log', function(exists) {    console.log(exists ? "已存在" : "创建成功");  });let logger = createLogger({    level: 'http',    handleExceptions: true,    json: true,    transports: [        // 可以定义多个文件,主要输出的info里面的文件        new transports.File({            level: 'http',            filename: __dirname + '/../../logs/all.log',            maxsize: 52428800,            maxFiles: 50,            tailable: true,            format:format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' })            }),        new transports.Console({            level: 'debug',            prettyPrint: true,            colorize: true        })    ],});logger.stream = {    write: function(message, encoding){        logger.http(message);    }};export {logger};
    

    logger文件主要是记录http日志到all.log文件中,日志文件不存在则创建文件。

    详细用法请查看:https://github.com/bithavoc/express-winston

    2,server.js中引入logger日志功能,<u style="box-sizing: border-box;">切记logger放在路由之前才会输出日志。</u>

    server.js

    import morgan from 'morgan';
    import {logger} from './utils/logger';
    app.use(morgan(":date[iso] :remote-addr :method :url :status :user-agent",{stream:logger.stream}))
    

    morgan输出日志信息可以配置,morgan(format,option),可参考https://github.com/expressjs/morgan

    3,重启服务,请求/login接口,而且文件目录下新增了log/all.log文件,控制台效果如下:

    {"message":"2020-01-19T11:58:31.385Z ::ffff:192.168.1.169 POST /api/login?username=123&password=1 200 PostmanRuntime/7.15.0\n","level":"http"}
    

    现在我们的请求日志就加好了。

    五,连接mysql数据库

    1,安装数据库,执行sql,看这个mysql菜鸟教程https://www.runoob.com/mysql/mysql-install.html

    新建数据库db_user并执行以下sql

    CREATE TABLE `user` (  
    `id` int(11) NOT NULL AUTO_INCREMENT, 
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 
    `password` varchar(128) NOT NULL,  
    `realname` varchar(64) DEFAULT NULL,  
    `email` varchar(32) DEFAULT NULL,  
    `is_link` tinyint(1) DEFAULT '1',  
    PRIMARY KEY (`id`),  
    UNIQUE KEY `email` (`email`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
    

    现在我们创建了一个user表,表结构如下

    image

    user表现在为空表,我们首先写一个接口为表中新增数据,user表里面有用户密码信息,所以我们再接下来的代码中会引入node的<u style="box-sizing: border-box;">crypto</u>模块进行密码加密。

    2,这个时候我们需要安装mysql2/promise

    npm i mysql2/promise
    

    安装完成之后我们可以用async/await来操作数据库,相对于之前的mysql,mysql2/promise好处是,操作数据库完成之后不需要手动释放,可自行释放连接池,减少占用进程。

    3,在config文件夹下新建db.js, 为了对数据库连接的统一管理,在constants.js中配置数据库连接

    export const MysqlUser = "mysql://root:123456@192.168.1.169:3306/db_user";
    

    db.js

    import mysql from "mysql2/promise";
    import {MysqlUser} from "./constants";
    const db_user = mysql.createPool(MysqlUser);
    export {db_user}
    

    4,login.js中引入<u style="box-sizing: border-box;">db_user</u>数据库连接池,新增addUser函数。

    import crypto from "crypto";
    async function addUserAsync(req,res){    
        let realname = req.body.realname;    
        let email = req.body.email;    
        let password = req.body.password;    
        if(!realname||!email||!password){        
            return res.json(Object.assign({},constants.ErrorParam,{data:null}));        
        }    
        let pass = await makePassword(password,'~9MnqsfOH@',1000,32,'sha256');    
        if(pass){        
            pass = 'pbkdf2_sha256$'+1000+"$~9MnqsfOH@$"+pass;   
        }    
        await db_user.execute(`INSERT INTO user (realname,password,email,is_link) VALUES(?,?,?,?)`,[realname,pass,email,1]);    
        res.json(Object.assign({},constants.Success,{data:null}))
    }
    function makePassword(password, salt, iterations, keylen, digest) {    
        return new Promise(function(resolve, reject) {      
            crypto.pbkdf2(password, salt, iterations, keylen, digest, (err, key) => {        
            if (err) {          
                reject(err);        
            } else {          
                resolve(key.toString('base64'));        
            }      
    })})}
    let addUser = wrap(addUserAsync);
    export {addUser}
    

    上面代码crypto.pbkdf2加密,对应的参数依次为,密码,加盐,次数,长度,加密方式

    index.js

    app.post("/user/add",user.addUser);
    

    postman请求/user/add接口

    image

    然后我们通过mysql客户端,navicat查询一下我们刚才新插入的数据

    执行sql

    SELECT * from user WHERE realname = "多啦A梦"
    
    image

    到这一步我们实现了向数据库里添加用户。

    六,查询数据库

    刚才我们在数据新增了一条数据,现在我们新增一个查询接口,参数取自req.query

    login.js

    async function userListAsync(req,res){    
        let realname = req.query.realname;    
        if(!realname){        
            res.json(Object.assign({},constants.ErrorParam,{data:null}));        
            return     
        }    
        let [rows,d] = await db_user.execute(`SELECT * FROM user WHERE realname = ?`,[realname]);    
        res.json(Object.assign({},constants.Success,{data:rows[0]}))
    };
    let userList = wrap(userListAsync);
    export {userList}
    

    index.js

    app.get("/user/query",user.userList);
    

    记得重启服务,请求看一下效果:

    image

    七,JWT(json web token)登录

    大多数网站登录之后返回一个token字符串,每次请求放在header中,后台根据解析token中的信息来返回相应的数据。

    安装jwt

    npm i jsonwebtoken
    

    生成token

    写一个login登录接口,通过正确的用户名密码换取jwt生成的token。

    了解更多jwt https://github.com/auth0/node-jsonwebtoken

    <u style="box-sizing: border-box;">登录生成token思路为</u>:

    将当前请求的用户名在数据库中进行查询,查询到数据之后取出密码,并将当前的密码按照插入数据库的逻辑加密,将加密的字符串和取出的密码进行比对,若相同则认为是密码正确,生成包含email的token返回。

    jwt生成token需要密钥,此时我们将密钥字符串存储在了contants.js中,token失效期10h。

    constants.js

    export const JwtSecret = "test1~@!^";
    

    login.js

    import jwt from "jsonwebtoken";
    async function loginAsync(req,res){    
        let email = req.body.username;    
        let password = req.body.password;    
        if(!email||!password){        
            return res.json(Object.assign({},constants.ErrorParam,{data:null}))   
         }    
        let [result,d] = await db_user.execute(`select password from user where email = ?`,[email]);    
        let [algorithm, iterations, salt, hash] = result[0].password.split('$', 4);    
        let valid = await comparePassword(password, salt, parseInt(iterations, 10), 32, 'sha256', hash);    
        if(valid){       
                 // 返回token        
                 const token = jwt.sign({user:req.body.username},constants.JwtSecret,{expiresIn:"10h"});       
                res.json(Object.assign({},constants.Success,{data:{token:token}}))    
        }else{        
                res.json(Object.assign({},constants.ErrorPassword,{data:null}))    
    }};
    function comparePassword(password, salt, iterations, keylen, digest, hash) {    
    return new Promise(function(resolve, reject) {        
    crypto.pbkdf2(password, salt, iterations, keylen, digest, (err, key) => {           
     if (err) {               
         reject(err);           
     } else {               
         resolve(key.toString('base64') === hash);          
      }})})
    };
    let login = wrap(loginAsync);
    export {login}
    

    index.js

    app.post("/login",user.login);
    

    重启服务之后,请求拿到token

    image

    浏览器请求

    请求相比之前参数携带没什么区别,只是在header请求头中给Authorization赋值:Bearer+“ ”+上面请求返回的token。

    如图所示

    新增token校验中间件

    为了每次校验token,我们在进入逻辑之前先解析token

    index.js

    import jwtFnc from "jsonwebtoken";
    import {db_user} from "../config/db";// 中间件,处理tokenasync 
    function checkToken(req,res,next){    
    let jwt = req.get('Authorization');    
        if(!jwt){        
            return res.json(constants.ErrorAuthentication);    
        }    
        // 解析 jwt.verify    
        let jwtArr = jwt.split(" ");    
        if(jwtArr.length !== 2 || jwtArr[0] !== 'Bearer'){        
            return res.json(constants.ErrorAuthentication)    
        }    
        try{        
            // 解析的时候可以知道token是否过期        
            let userData = jwtFnc.verify(jwtArr[1],constants.JwtSecret);        
            // 校验用户是否存在        
            let [rows,d] = await db_user.execute(`SELECT id FROM user WHERE email = ?`, [userData.user]);        
            if(rows.length>0){            
                req.jwtUsername = userData.user;        
            }else{           
                 return res.json(constants.ErrorAuthentication)        
            }       
         }catch(e){        
            return res.json(constants.ErrorToken);   
         }    
            next();
        };
    // 那个接口使用,就在路由后边加上这个中间件,校验通过执行next(),才会往下执行
    app.get("/user/query",checkToken,user.userList);
    

    我们给刚才的/user/query加上了token校验现在,不加token请求一下

    image

    我们在header加上token试一下

    image

    此时我们只是校验了token的格式和有效期,还有客户信息,我们可以看到解析完成之后我们将信息拼在了body中,可以在login函数中进一步去校验权限之类的东西.......

    八,解析excel文件

    解析前端传过来的文件,首先我们需要一个可以接收文件的中间件connect-multiparty,他可以把前端传过来的文件转到req.body.files在接收。

    安装connect-multiparty

    npm i connect-multiparty
    

    要解析excel文件,需要安装node-xlsx

    npm i node-xlsx
    

    login.js新增解析文件方法getFileDataAsync

    import xlsx from "node-xlsx";
    async function getFileDataAsync(req,res){    
        const filePath = req.files.file.path;    
        // 读取xlsx文件    
        const data = xlsx.parse(req.files.file.path);    
        onsole.log(data)    
        res.json(Object.assign({},constants.Success,{data:{token:null}}))
    }
    

    index.js

    import  multipart from 'connect-multiparty';
    const multipartMiddleware = multipart();
    app.post("/upload",checkToken,multipartMiddleware,user.getFileData);
    

    我们新建一个excel,

    image

    请求下,我们在控制台看下输出:

    image

    九,根据不同场景区分不同的路由

    我们有时候可能对于user模块期望访问的是/user/, 对于list期望请求/list/。这时候我们用到express的router模块。

    index.js

    //创建实例
    let usersRouter = express.Router();
    let listRouter = express.Router();
    app.use("/user",usersRouter);
    app.use("/order",listRouter);
    userRouter.get("/list",func) // 相当于请求 “/user/list”
    listRouter.get("/get",func1) //相当于请求 “/list/get”
    

    十,定时任务

    如果有定时任务需要用到node-schedule模块

    可以参考https://github.com/node-schedule/node-schedule.git

    安装node-schedule

    npm i node-schedule
    

    index.js

    import schedule from 'node-schedule';
    //定时任务,可以根据rule配置不同时间间隔
    //每五分钟执行一次
    let rule = new schedule.RecurrenceRule();
    rule.minute = [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56];
    let count = 0;
    schedule.scheduleJob(rule, async function () {   
         console.log(++count);
    });
    

    十一,解决跨域

    本地调试过程中可能会出现跨域问题,我们可以通过如下设置来解决

    server.js

    if (app.get('env') === 'development') {    
        app.use(function (req, res, next) {       
            res.setHeader('Access-Control-Allow-Origin', req.get('Origin') || '');        
            res.setHeader('Access-Control-Allow-Credentials', 'true');       
            res.setHeader('Access-Control-Allow-Headers', 'Authorization,x-requested-with');       
            res.setHeader('Access-Control-Allow-Methods', 'POST, GET');        
         if (req.method == 'OPTIONS') {           
             res.send(200);       
         }else{        
            next();        
        }})
    }
    

    十二,安全最佳实践

    关于最佳实践,了解更多点击http://expressjs.com/zh-cn/advanced/best-practice-security.html

    安装helmet设置请求头

    npm install --save helmet
    

    server.js

    import helmet from 'helmet';
    app.use(helmet());
    app.disable('x-powered-by')
    

    十三,打包文件

    到这一步呢,我们已经实现了express的简单搭建,但是要把代码部署到服务器上,还需要我们进行进一步打包。

    这里呢,我们使用babel进行打包,需要把我们所有文件打进一个文件夹中。

    1,我们需要新建src文件夹,此时的代码结构如下

    --src
        --app
        --config
        --util
        --static
    server.js
    

    package.json 新增打包script

    "build": "babel src -d lib"
    

    执行命令

    npm run build
    

    我们发现src同级目录下新增了lib文件夹

    这时候我们可以直接启动lib/server.js,所以我在script分了三步

    "scripts": {        
               "start": "babel-node src/server.js --presets es2015,stage-2",        
                "build": "babel src -d lib",        
                "dev": "babel-node lib/server.js"    
    },
    

    最后我们的项目结构为

    image

    感兴趣的同学还可以了解下pm2,这就不做展开了。

    补充:

    写的不好还请谅解,以上也有许多疏漏的地方,有些地方毕竟做的不是很严谨,欢迎指正。

    github地址:https://github.com/songtao1/express-2020

    相关文章

      网友评论

          本文标题:从零开始搭建一个Express应用

          本文链接:https://www.haomeiwen.com/subject/zewmxhtx.html