從零開(kāi)始搭建一個(gè)Express應(yīng)用

業(yè)務(wù)前景

其實(shí)許多技術(shù)還是要應(yīng)用到業(yè)務(wù)中去做,才會(huì)有不一樣的挑戰(zhàn)和收獲,公司有自己內(nèi)部管理系統(tǒng),主要是用于客戶維護(hù),和核算成本。基于這樣的情況,上方?jīng)Q定前端自己來(lái)建立和維護(hù)這樣的系統(tǒng),前兩天重新搭建了一遍,現(xiàn)在打算整理出來(lái),來(lái)一起討論這個(gè)搭建過(guò)程。

一,寫(xiě)一個(gè)hello world

1,新建項(xiàng)目文件夾

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")});

終端執(zhí)行

node server

我們可以看到,服務(wù)已經(jīng)運(yùn)行了

image

打開(kāi)瀏覽器,輸入地址 http://localhost:3000/,可以看到我們?cè)L問(wèn)成功,hello world

image

到此為止我們已經(jīng)實(shí)現(xiàn)了所有語(yǔ)言初始化的第一步,hello, world。

二,訪問(wèn)靜態(tài)文件

1,使用express的static中間件函數(shù)

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

訪問(wèn)根路徑之下任何路由返回的是絕對(duì)路徑+“/static”下的index.html文件,接下來(lái)我們實(shí)驗(yàn)一下

server.js同級(jí)下新增文件夾static,里面創(chuàng)建一個(gè)index.html文件,文件結(jié)構(gòu)如下

image

重啟服務(wù)

node server

效果如圖所示

image

現(xiàn)在我們成功運(yùn)行了一個(gè)本地服務(wù),可以通過(guò)我們本地ip地址<u style="box-sizing: border-box;">localhost:3000</u>,訪問(wèn)到static文件下的靜態(tài)資源,默認(rèn)是index.html,如果是example.html則直接訪問(wèn)<u style="box-sizing: border-box;">localhost:3000/example.html,其實(shí)這時(shí)候</u>我們可以通過(guò)本地啟動(dòng)一個(gè)服務(wù),來(lái)讓同一局域網(wǎng)下的計(jì)算機(jī)訪問(wèn)我們的靜態(tài)網(wǎng)頁(yè)。

三,寫(xiě)一個(gè)接口出來(lái)

1,server.js同級(jí)目錄下新增一個(gè)app文件夾,文件夾下新增index.js,文件目錄此時(shí)如下

image

分成這樣的項(xiàng)目結(jié)構(gòu),主要是為了server.js,做總的中間件的控制,在index.js中做路由的請(qǐng)求分發(fā)。

代碼如下:

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

通過(guò)app.use來(lái)捕獲異常,如果沒(méi)有next(err),這個(gè)異常會(huì)被掛起,不會(huì)被垃圾回收機(jī)制所回收,所有的中間件通過(guò)next()方法才會(huì)向下執(zhí)行。

將index.js引入到server.js中

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

執(zhí)行 node server,這時(shí)發(fā)現(xiàn),報(bào)錯(cuò)了。

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

報(bào)錯(cuò)的原因是import是es6語(yǔ)法中引入方式,此時(shí)我們項(xiàng)目不支持es6,咋辦呢?

辦法總比困難多,編譯一下就完了。(:

2,通過(guò)babel將es6轉(zhuǎn)為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"
}

執(zhí)行命令

npm run start

這時(shí)候發(fā)現(xiàn)運(yùn)行起來(lái)了。

3,新建路由文件login.js,和index.js同級(jí)

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);

通過(guò)wrap函數(shù)包裹住路由接口函數(shù),可以及時(shí)捕獲到異步錯(cuò)誤。

在index.js中,引入login.js中的login函數(shù),這時(shí)候這是一個(gè)get請(qǐng)求,我們用postman試一下

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

返回結(jié)果

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

我們已經(jīng)完成一個(gè)了一個(gè)簡(jiǎn)單的get請(qǐng)求。

4,接下來(lái)我們來(lái)整一個(gè)post請(qǐng)求。

首先我們先安裝一個(gè)中間件body-parser,將post請(qǐng)求攜帶的參數(shù)解析之后放到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}));

這里只是做了參數(shù)大小限制,更多api用法訪問(wèn)https://github.com/expressjs/body-parser

繼續(xù)在 login.js中新增一個(gè)login函數(shù),為了方便我們對(duì)code和msg進(jìn)行管理,我們和app文件夾同級(jí)新增一個(gè)config文件夾,文件夾下新增constants.js文件,里面放我們一些配置信息。

文件目錄如圖所示

image

constants.js

export const Success = {code:0,msg:"成功"};
export const ErrorParam = {code:10001,msg:"參數(shù)錯(cuò)誤"};
export const ErrorAuthentication = {code:10002,msg:"無(wú)權(quán)限"};
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}

接下來(lái)在index.js中新增路由

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

重啟服務(wù)

npm run start

訪問(wèn)結(jié)果如圖所示

image

現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了常用的兩種請(qǐng)求,get,post。

四,為請(qǐng)求添加log日志

1,引入express中間件morgan(獲取所有的請(qǐng)求)和winston

npm install --save winston morgan

server.js同級(jí)新建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 ? "已存在" : "創(chuàng)建成功");  });let logger = createLogger({    level: 'http',    handleExceptions: true,    json: true,    transports: [        // 可以定義多個(gè)文件,主要輸出的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文件中,日志文件不存在則創(chuàng)建文件。

詳細(xì)用法請(qǐng)查看:https://github.com/bithavoc/express-winston

2,server.js中引入logger日志功能,<u style="box-sizing: border-box;">切記logger放在路由之前才會(huì)輸出日志。</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,重啟服務(wù),請(qǐng)求/login接口,而且文件目錄下新增了log/all.log文件,控制臺(tái)效果如下:

{"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"}

現(xiàn)在我們的請(qǐng)求日志就加好了。

五,連接mysql數(shù)據(jù)庫(kù)

1,安裝數(shù)據(jù)庫(kù),執(zhí)行sql,看這個(gè)mysql菜鳥(niǎo)教程https://www.runoob.com/mysql/mysql-install.html

新建數(shù)據(jù)庫(kù)db_user并執(zhí)行以下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;

現(xiàn)在我們創(chuàng)建了一個(gè)user表,表結(jié)構(gòu)如下

image

user表現(xiàn)在為空表,我們首先寫(xiě)一個(gè)接口為表中新增數(shù)據(jù),user表里面有用戶密碼信息,所以我們?cè)俳酉聛?lái)的代碼中會(huì)引入node的<u style="box-sizing: border-box;">crypto</u>模塊進(jìn)行密碼加密。

2,這個(gè)時(shí)候我們需要安裝mysql2/promise

npm i mysql2/promise

安裝完成之后我們可以用async/await來(lái)操作數(shù)據(jù)庫(kù),相對(duì)于之前的mysql,mysql2/promise好處是,操作數(shù)據(jù)庫(kù)完成之后不需要手動(dòng)釋放,可自行釋放連接池,減少占用進(jìn)程。

3,在config文件夾下新建db.js, 為了對(duì)數(shù)據(jù)庫(kù)連接的統(tǒng)一管理,在constants.js中配置數(shù)據(jù)庫(kù)連接

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>數(shù)據(jù)庫(kù)連接池,新增addUser函數(shù)。

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加密,對(duì)應(yīng)的參數(shù)依次為,密碼,加鹽,次數(shù),長(zhǎng)度,加密方式

index.js

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

postman請(qǐng)求/user/add接口

image

然后我們通過(guò)mysql客戶端,navicat查詢一下我們剛才新插入的數(shù)據(jù)

執(zhí)行sql

SELECT * from user WHERE realname = "多啦A夢(mèng)"
image

到這一步我們實(shí)現(xiàn)了向數(shù)據(jù)庫(kù)里添加用戶。

六,查詢數(shù)據(jù)庫(kù)

剛才我們?cè)跀?shù)據(jù)新增了一條數(shù)據(jù),現(xiàn)在我們新增一個(gè)查詢接口,參數(shù)取自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);

記得重啟服務(wù),請(qǐng)求看一下效果:

image

七,JWT(json web token)登錄

大多數(shù)網(wǎng)站登錄之后返回一個(gè)token字符串,每次請(qǐng)求放在header中,后臺(tái)根據(jù)解析token中的信息來(lái)返回相應(yīng)的數(shù)據(jù)。

安裝jwt

npm i jsonwebtoken

生成token

寫(xiě)一個(gè)login登錄接口,通過(guò)正確的用戶名密碼換取jwt生成的token。

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

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

將當(dāng)前請(qǐng)求的用戶名在數(shù)據(jù)庫(kù)中進(jìn)行查詢,查詢到數(shù)據(jù)之后取出密碼,并將當(dāng)前的密碼按照插入數(shù)據(jù)庫(kù)的邏輯加密,將加密的字符串和取出的密碼進(jìn)行比對(duì),若相同則認(rèn)為是密碼正確,生成包含email的token返回。

jwt生成token需要密鑰,此時(shí)我們將密鑰字符串存儲(chǔ)在了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);

重啟服務(wù)之后,請(qǐng)求拿到token

image

瀏覽器請(qǐng)求

請(qǐng)求相比之前參數(shù)攜帶沒(méi)什么區(qū)別,只是在header請(qǐng)求頭中給Authorization賦值:Bearer+“ ”+上面請(qǐng)求返回的token。

如圖所示

新增token校驗(yàn)中間件

為了每次校驗(yàn)token,我們?cè)谶M(jìn)入邏輯之前先解析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{        
        // 解析的時(shí)候可以知道token是否過(guò)期        
        let userData = jwtFnc.verify(jwtArr[1],constants.JwtSecret);        
        // 校驗(yàn)用戶是否存在        
        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();
    };
// 那個(gè)接口使用,就在路由后邊加上這個(gè)中間件,校驗(yàn)通過(guò)執(zhí)行next(),才會(huì)往下執(zhí)行
app.get("/user/query",checkToken,user.userList);

我們給剛才的/user/query加上了token校驗(yàn)現(xiàn)在,不加token請(qǐng)求一下

image

我們?cè)趆eader加上token試一下

image

此時(shí)我們只是校驗(yàn)了token的格式和有效期,還有客戶信息,我們可以看到解析完成之后我們將信息拼在了body中,可以在login函數(shù)中進(jìn)一步去校驗(yàn)權(quán)限之類的東西.......

八,解析excel文件

解析前端傳過(guò)來(lái)的文件,首先我們需要一個(gè)可以接收文件的中間件connect-multiparty,他可以把前端傳過(guò)來(lái)的文件轉(zhuǎn)到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);

我們新建一個(gè)excel,

image

請(qǐng)求下,我們?cè)诳刂婆_(tái)看下輸出:

image

九,根據(jù)不同場(chǎng)景區(qū)分不同的路由

我們有時(shí)候可能對(duì)于user模塊期望訪問(wèn)的是/user/, 對(duì)于list期望請(qǐng)求/list/。這時(shí)候我們用到express的router模塊。

index.js

//創(chuàng)建實(shí)例
let usersRouter = express.Router();
let listRouter = express.Router();
app.use("/user",usersRouter);
app.use("/order",listRouter);
userRouter.get("/list",func) // 相當(dāng)于請(qǐng)求 “/user/list”
listRouter.get("/get",func1) //相當(dāng)于請(qǐng)求 “/list/get”

十,定時(shí)任務(wù)

如果有定時(shí)任務(wù)需要用到node-schedule模塊

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

安裝node-schedule

npm i node-schedule

index.js

import schedule from 'node-schedule';
//定時(shí)任務(wù),可以根據(jù)rule配置不同時(shí)間間隔
//每五分鐘執(zhí)行一次
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);
});

十一,解決跨域

本地調(diào)試過(guò)程中可能會(huì)出現(xiàn)跨域問(wèn)題,我們可以通過(guò)如下設(shè)置來(lái)解決

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();        
    }})
}

十二,安全最佳實(shí)踐

關(guān)于最佳實(shí)踐,了解更多點(diǎn)擊http://expressjs.com/zh-cn/advanced/best-practice-security.html

安裝helmet設(shè)置請(qǐng)求頭

npm install --save helmet

server.js

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

十三,打包文件

到這一步呢,我們已經(jīng)實(shí)現(xiàn)了express的簡(jiǎn)單搭建,但是要把代碼部署到服務(wù)器上,還需要我們進(jìn)行進(jìn)一步打包。

這里呢,我們使用babel進(jìn)行打包,需要把我們所有文件打進(jìn)一個(gè)文件夾中。

1,我們需要新建src文件夾,此時(shí)的代碼結(jié)構(gòu)如下

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

package.json 新增打包script

"build": "babel src -d lib"

執(zhí)行命令

npm run build

我們發(fā)現(xiàn)src同級(jí)目錄下新增了lib文件夾

這時(shí)候我們可以直接啟動(dòng)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"    
},

最后我們的項(xiàng)目結(jié)構(gòu)為

image

感興趣的同學(xué)還可以了解下pm2,這就不做展開(kāi)了。

補(bǔ)充:

寫(xiě)的不好還請(qǐng)諒解,以上也有許多疏漏的地方,有些地方畢竟做的不是很嚴(yán)謹(jǐn),歡迎指正。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容