从零开始搭建一个Express应用

序言

2019年并无发表几篇文章,平时可能有些问题只记在了印象笔记里,应该是以为本身写的太烂,怕招来大佬们的口水。今天斗胆发一篇关于Express的文章,做为本身2019年的总结,和2020年新的开始,人们老是在年末和年初善于作出一些立目标作计划的冲动。而后开始几个月以后忘记了今年要完成的目标,最近也开始看一些理财的文章,其中有一点就是讲怎么去实现计划,把计划细分到天天,而不是说今年我打算作什么,而是制做明确的计划,具体的实现日期和完成目标,可能有些事情坚持下来不是那么的容易,因此也要对本身实行奖励制度,好比完成一个小目标,容许本身买个喜欢的小玩意,来时刻保证本身向前冲的那种劲头。2020年就是要多读书多拓展了,也得慢慢学会理财,但愿2020年多总结本身。  
javascript

业务前景

其实许多技术仍是要应用到业务中去作,才会有不同的挑战和收获,公司有本身内部管理系统,主要是用于客户维护,和核算成本。基于这样的状况,上方决定前端本身来创建和维护这样的系统,前两天从新搭建了一遍,如今打算整理出来,来一块儿讨论这个搭建过程。  
html

一,写一个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复制代码
咱们能够看到,服务已经运行了


打开浏览器,输入地址 http://localhost:3000/,能够看到咱们访问成功,hello world
java


到此为止咱们已经实现了全部语言初始化的第一步,hello, world。node

二,访问静态文件

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

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文件,接下来咱们实验一下
git

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


重启服务github

node server复制代码

效果如图所示web


如今咱们成功运行了一个本地服务,能够经过咱们本地ip地址localhost:3000,访问到static文件下的静态资源,默认是index.html,若是是example.html则直接访问localhost:3000/example.html,其实这时候咱们能够经过本地启动一个服务,来让同一局域网下的计算机访问咱们的静态网页。

三,写一个接口出来

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


分红这样的项目结构,主要是为了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用法访问 github.com/expressjs/b…

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

文件目录如图所示


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复制代码

访问结果如图所示


如今咱们已经实现了经常使用的两种请求,get,post。

四,为请求添加log日志

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

npm install --save winston morgan复制代码

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


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文件中,日志文件不存在则建立文件。

详细用法请查看:github.com/bithavoc/ex…

2,server.js中引入logger日志功能,切记logger放在路由以前才会输出日志。

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),可参考github.com/expressjs/m…

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菜鸟教程www.runoob.com/mysql/mysql…

新建数据库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表,表结构以下


user表如今为空表,咱们首先写一个接口为表中新增数据,user表里面有用户密码信息,因此咱们再接下来的代码中会引入node的crypto模块进行密码加密。

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中引入db_user数据库链接池,新增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接口


而后咱们经过mysql客户端,navicat查询一下咱们刚才新插入的数据

执行sql

SELECT * from user WHERE realname = "多啦A梦"复制代码


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

六,查询数据库

刚才咱们在数据新增了一条数据,如今咱们新增一个查询接口,参数取自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);复制代码

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


七,JWT(json web token)登陆

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

安装jwt

npm i jsonwebtoken复制代码

生成token

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

了解更多jwt github.com/auth0/node-…

登陆生成token思路为

将当前请求的用户名在数据库中进行查询,查询到数据以后取出密码,并将当前的密码按照插入数据库的逻辑加密,将加密的字符串和取出的密码进行比对,若相同则认为是密码正确,生成包含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


浏览器请求

请求相比以前参数携带没什么区别,只是在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请求一下


咱们在header加上token试一下


此时咱们只是校验了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,


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


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

咱们有时候可能对于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模块

能够参考github.com/node-schedu…

安装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();        
    }})
}复制代码

十二,安全最佳实践

关于最佳实践,了解更多点击expressjs.com/zh-cn/advan…

安装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"    
},复制代码

最后咱们的项目结构为


感兴趣的同窗还能够了解下pm2,这就不作展开了。

补充:

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

github地址:github.com/songtao1/ex…

相关文章
相关标签/搜索