從零開始搭建一個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…

相關文章
相關標籤/搜索