業(yè)務(wù)前景
其實(shí)許多技術(shù)還是要應(yīng)用到業(yè)務(wù)中去做暇榴,才會(huì)有不一樣的挑戰(zhàn)和收獲橱赠,公司有自己內(nèi)部管理系統(tǒng)尤仍,主要是用于客戶維護(hù),和核算成本狭姨≡桌玻基于這樣的情況,上方?jīng)Q定前端自己來建立和維護(hù)這樣的系統(tǒng)饼拍,前兩天重新搭建了一遍赡模,現(xiàn)在打算整理出來,來一起討論這個(gè)搭建過程师抄。
一漓柑,寫一個(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)行了
打開瀏覽器叨吮,輸入地址 http://localhost:3000/辆布,可以看到我們?cè)L問成功,hello world
到此為止我們已經(jīng)實(shí)現(xiàn)了所有語言初始化的第一步挤安,hello, world谚殊。
二,訪問靜態(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'))
})
訪問根路徑之下任何路由返回的是絕對(duì)路徑+“/static”下的index.html文件蛤铜,接下來我們實(shí)驗(yàn)一下
server.js同級(jí)下新增文件夾static,里面創(chuàng)建一個(gè)index.html文件嫩絮,文件結(jié)構(gòu)如下
重啟服務(wù)
node server
效果如圖所示
現(xiàn)在我們成功運(yùn)行了一個(gè)本地服務(wù),可以通過我們本地ip地址<u style="box-sizing: border-box;">localhost:3000</u>围肥,訪問到static文件下的靜態(tài)資源剿干,默認(rèn)是index.html,如果是example.html則直接訪問<u style="box-sizing: border-box;">localhost:3000/example.html,其實(shí)這時(shí)候</u>我們可以通過本地啟動(dòng)一個(gè)服務(wù),來讓同一局域網(wǎng)下的計(jì)算機(jī)訪問我們的靜態(tài)網(wǎng)頁穆刻。
三置尔,寫一個(gè)接口出來
1,server.js同級(jí)目錄下新增一個(gè)app文件夾氢伟,文件夾下新增index.js榜轿,文件目錄此時(shí)如下
分成這樣的項(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};
通過app.use來捕獲異常诚些,如果沒有next(err)飞傀,這個(gè)異常會(huì)被掛起皇型,不會(huì)被垃圾回收機(jī)制所回收,所有的中間件通過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語法中引入方式唬格,此時(shí)我們項(xiàng)目不支持es6,咋辦呢雪隧?
辦法總比困難多西轩,編譯一下就完了。(:
2脑沿,通過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)行起來了庄拇。
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);
通過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蚀腿,接下來我們來整一個(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用法訪問https://github.com/expressjs/body-parser
繼續(xù)在 login.js中新增一個(gè)login函數(shù),為了方便我們對(duì)code和msg進(jìn)行管理,我們和app文件夾同級(jí)新增一個(gè)config文件夾碳抄,文件夾下新增constants.js文件,里面放我們一些配置信息场绿。
文件目錄如圖所示
constants.js
export const Success = {code:0,msg:"成功"};
export const ErrorParam = {code:10001,msg:"參數(shù)錯(cuò)誤"};
export const ErrorAuthentication = {code:10002,msg:"無權(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}
接下來在index.js中新增路由
app.post("/login",user.login);
重啟服務(wù)
npm run start
訪問結(jié)果如圖所示
現(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,目錄如下:
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ù)庫
1宫补,安裝數(shù)據(jù)庫檬姥,執(zhí)行sql,看這個(gè)mysql菜鳥教程https://www.runoob.com/mysql/mysql-install.html
新建數(shù)據(jù)庫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)如下
user表現(xiàn)在為空表健民,我們首先寫一個(gè)接口為表中新增數(shù)據(jù),user表里面有用戶密碼信息贫贝,所以我們?cè)俳酉聛淼拇a中會(huì)引入node的<u style="box-sizing: border-box;">crypto</u>模塊進(jìn)行密碼加密秉犹。
2,這個(gè)時(shí)候我們需要安裝mysql2/promise
npm i mysql2/promise
安裝完成之后我們可以用async/await來操作數(shù)據(jù)庫稚晚,相對(duì)于之前的mysql崇堵,mysql2/promise好處是,操作數(shù)據(jù)庫完成之后不需要手動(dòng)釋放客燕,可自行釋放連接池鸳劳,減少占用進(jìn)程。
3也搓,在config文件夾下新建db.js, 為了對(duì)數(shù)據(jù)庫連接的統(tǒng)一管理赏廓,在constants.js中配置數(shù)據(jù)庫連接
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ù)庫連接池还绘,新增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接口
然后我們通過mysql客戶端,navicat查詢一下我們剛才新插入的數(shù)據(jù)
執(zhí)行sql
SELECT * from user WHERE realname = "多啦A夢(mèng)"
到這一步我們實(shí)現(xiàn)了向數(shù)據(jù)庫里添加用戶踏揣。
六庆亡,查詢數(shù)據(jù)庫
剛才我們?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)求看一下效果:
七拼缝,JWT(json web token)登錄
大多數(shù)網(wǎng)站登錄之后返回一個(gè)token字符串,每次請(qǐng)求放在header中彰亥,后臺(tái)根據(jù)解析token中的信息來返回相應(yīng)的數(shù)據(jù)咧七。
安裝jwt
npm i jsonwebtoken
生成token
寫一個(gè)login登錄接口,通過正確的用戶名密碼換取jwt生成的token任斋。
了解更多jwt https://github.com/auth0/node-jsonwebtoken
<u style="box-sizing: border-box;">登錄生成token思路為</u>:
將當(dāng)前請(qǐng)求的用戶名在數(shù)據(jù)庫中進(jìn)行查詢继阻,查詢到數(shù)據(jù)之后取出密碼,并將當(dāng)前的密碼按照插入數(shù)據(jù)庫的邏輯加密废酷,將加密的字符串和取出的密碼進(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
瀏覽器請(qǐng)求
請(qǐng)求相比之前參數(shù)攜帶沒什么區(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是否過期
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)通過執(zhí)行next(),才會(huì)往下執(zhí)行
app.get("/user/query",checkToken,user.userList);
我們給剛才的/user/query加上了token校驗(yàn)現(xiàn)在橙垢,不加token請(qǐng)求一下
我們?cè)趆eader加上token試一下
此時(shí)我們只是校驗(yàn)了token的格式和有效期垛叨,還有客戶信息,我們可以看到解析完成之后我們將信息拼在了body中柜某,可以在login函數(shù)中進(jìn)一步去校驗(yàn)權(quán)限之類的東西.......
八嗽元,解析excel文件
解析前端傳過來的文件,首先我們需要一個(gè)可以接收文件的中間件connect-multiparty喂击,他可以把前端傳過來的文件轉(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翰绊,
請(qǐng)求下佩谷,我們?cè)诳刂婆_(tái)看下輸出:
九,根據(jù)不同場(chǎng)景區(qū)分不同的路由
我們有時(shí)候可能對(duì)于user模塊期望訪問的是/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)試過程中可能會(huì)出現(xiàn)跨域問題桐猬,我們可以通過如下設(shè)置來解決
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)為
感興趣的同學(xué)還可以了解下pm2,這就不做展開了惶傻。
補(bǔ)充:
寫的不好還請(qǐng)諒解棍郎,以上也有許多疏漏的地方,有些地方畢竟做的不是很嚴(yán)謹(jǐn)银室,歡迎指正涂佃。