淺嘗全棧項目,體驗(yàn)前后端開發(fā)異同,會把開發(fā)過程中的一些亮點(diǎn)和難點(diǎn)記錄下來,已備不時之需。
前端:react + ant-design-mobile
后端:express.js + mysql
目錄結(jié)構(gòu)
- 準(zhǔn)備階段
- 數(shù)據(jù)庫
- 后端-express
- 前端-react
準(zhǔn)備階段:
一.準(zhǔn)備階段
1.數(shù)據(jù)庫建表
(1).用戶基礎(chǔ)信息表

(2).清單表(業(yè)務(wù)表)

2.后端構(gòu)建項目
下載express-generator
npm install -g express-generator
構(gòu)建項目
express --view=ejs myproject
安裝依賴包
cd myproject
啟動項目
npm start
目錄結(jié)構(gòu)通常如下:
- app.js:應(yīng)用的主文件,設(shè)置中間件和路由。
- bin/www:啟動腳本,設(shè)置服務(wù)器端口并啟動服務(wù)器。
- public/:靜態(tài)文件目錄,存放 CSS、JavaScript 和圖片等。
- routes/:路由目錄,定義應(yīng)用的路由。
- views/:視圖目錄,存放模板文件。
- node_modules/:依賴包目錄。
3.前端構(gòu)建項目
npm create-react-app my-app
cd my-app
npm start
目錄結(jié)構(gòu)通常如下:
- node_modules:安裝依賴
- public:公共文件
- src:主體文件
- .gitignore:忽略文件
- package.json:版本管理信息
- package-lock.json:版本管理信息
二.數(shù)據(jù)庫
作為一個前端開發(fā)的打工仔,凡事都是以前端思維為導(dǎo)向,大學(xué)的數(shù)據(jù)庫知識不扎實(shí),導(dǎo)致在一開始設(shè)立數(shù)據(jù)庫時,磕磕絆絆,但好在結(jié)果還算可以。
數(shù)據(jù)庫連接
1.建立mysql單個數(shù)據(jù)庫連接對象,并連接
const mysql = require("mysql"); //引入mysql
// 設(shè)置MySQL連接配置
const base_url = 'your_url'
const connection = mysql.createConnection({
host: base_url,
user: "root", //數(shù)據(jù)庫賬號
password: "your_code", //數(shù)據(jù)庫密碼
database: "test", //數(shù)據(jù)庫名稱
timezone: "Asia/Shanghai"
});
// 連接到MySQL數(shù)據(jù)庫
connection.connect((error) => {
if (error) throw error;
//連接成功打印結(jié)果`如果連接失敗,記得查看數(shù)據(jù)庫是否開啟`
});
2.建立數(shù)據(jù)庫連接池
const mysql = require('mysql');
const pool = mysql.createPool({
host: 'localhost',
user: 'yourUsername',
password: 'yourPassword',
database: 'yourDatabase'
});
pool.query('SELECT 1 + 1 AS solution', function (error, results, fields) {
if (error) throw error;
console.log('The solution is: ', results[0].solution);
});
兩種方法的取舍,取決于使用的場景:
如果是高并發(fā),需要頻繁訪問多個數(shù)據(jù)庫的情況下,用createPool。
如果是復(fù)雜sql查詢,多表鏈表查詢,則用createConnection。
三.后端
1.基礎(chǔ)配置
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const bodyParser = require('body-parser');
require('dotenv').config();
//引入跨域中間件
const cors = require('cors');
//引入路由
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var todolistsRouter = require('./routes/todolists');
//引入中間件
const errorHandler = require('./utils/errorMiddleware');
var app = express();
//解析中間件
app.use(bodyParser.json()); // 解析JSON格式的數(shù)據(jù)
app.use(bodyParser.urlencoded({ extended: true })); // 解析URL編碼的數(shù)據(jù)
app.use(cookieParser());//解析cookie
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
//配置
var session = require('express-session');
// 設(shè)置跨域 需要用
app.use(cors({
// 設(shè)置為本地前端域名,不設(shè)置為*是因?yàn)樾枰獋鱟ookies,不能違反cors的規(guī)范,出于安全考慮
origin: `http://${process.env.BASE_URL}:3000`, // 允許的源
credentials: true, // 允許憑證
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'Origin', 'Referer'],
exposedHeaders: ['Content-Range', 'Content-Disposition']
}));
//驗(yàn)證jwt 驗(yàn)證這塊最好放在其他中間件的下面,因?yàn)橛械闹虚g件會影響
const expressjwt = require("express-jwt");
// 設(shè)置一個私鑰
const secret = process.env.JWT_SECRET;
app.use(expressjwt({
secret: secret, algorithms: ['HS256'],
}).unless({ path: ['/users/login', '/users/register'] }));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/todoLists', todolistsRouter);
app.set('trust proxy', 1)
// //使用錯誤中間件
app.use(errorHandler);
module.exports = app;
2.登錄狀態(tài)管理
(1).session
session是會話管理,在服務(wù)端管理用戶狀態(tài),基本配置如下:
const session = require('express-session');
app.use(session({
secret: 'your_secret_key', // 用于簽名會話ID的密鑰
resave: false,
saveUninitialized: true,
cookie: { maxAge: 30000 } // 例如,設(shè)置會話有效期為30秒
}));
用法:在登錄接口完成后設(shè)置,退出登錄之后清空銷毀,在每個接口處驗(yàn)證一下是否含有session的值
app.post('/login', (req, res) => {
// 假設(shè)用戶驗(yàn)證成功
req.session.user = { id: 1, username: 'exampleUser' };
res.send('User logged in');
});
app.get('/profile', (req, res) => {
if (req.session.user) {
res.send(`Welcome, ${req.session.user.username}`);
} else {
res.send('You are not logged in');
}
});
app.get('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.send('Error logging out');
}
res.send('Logged out');
});
});
這里有個小技巧,每個接口都要判斷session,所以根據(jù)懶人定律,應(yīng)該把此處抽離出來。
(2).cookie
cookie進(jìn)行登陸狀態(tài)管理配合JWT,我項目中也是使用的這種,基本配置如下:
const jwt = require('jsonwebtoken');
app.post('/login', (req, res) => {
// 假設(shè)用戶驗(yàn)證成功
const token = jwt.sign({ username: 'exampleUser' }, 'your_secret_key', { expiresIn: '1h' });
res.send({ token });
});
驗(yàn)證token過期時間
用expressJWT驗(yàn)證的時候需要注意:
1.版本問題。
2.jwt通過authorization獲取進(jìn)行驗(yàn)證的,所以需要手動設(shè)置一下token。
3.需要設(shè)置跨域
tips:試過直接設(shè)置頭進(jìn)行跨域,但是發(fā)現(xiàn)get請求可以獲取到cookie,post請求獲取不到,原因不詳,所以還是用cors插件。
const expressJWT = require('express-jwt');
//引入跨域中間件
const cors = require('cors');
// 設(shè)置跨域 需要用
app.use(cors({
// 設(shè)置為本地前端域名,不設(shè)置為*是因?yàn)樾枰獋鱟ookies,不能違反cors的規(guī)范,出于安全考慮
origin: 'http://127.0.0.1:3000', // 允許的源
credentials: true, // 允許憑證
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'Origin', 'Referer'],
exposedHeaders: ['Content-Range', 'Content-Disposition']
}));
4.客戶端應(yīng)該設(shè)置
axios.defaults.withCredentials = true;
// 從cookie中讀取JWT的中間件
const jwtFromCookie = (req, res, next) => {
const token = req.cookies.jwt; // 從cookie中獲取名為'jwt'的cookie值,jwt是自己設(shè)置的
if (token) {
req.headers.authorization = `Bearer ${token}`; // 設(shè)置Authorization請求頭
}
next();
};
// 使用自定義中間件,因?yàn)槟J(rèn)jwt驗(yàn)證是從authorization獲取的,所以要把a(bǔ)uthorization設(shè)置一下
app.use(jwtFromCookie);
app.use(expressJWT({ secret: 'your_secret_key' }).unless({ path: ['/login'] }));
app.get('/protected', (req, res) => {
res.send(`Hello, ${req.user.username}`);
});
若驗(yàn)證不通過需拋出錯誤,填寫錯誤中間件
function errorHandler(err, req, res, next) {
let statusCode = err.statusCode || 500;
let message = err.message || 'Internal Server Error';
//主要是這部分
if (err.name === 'UnauthorizedError') {
console.log('err.code === credentials_required',)
return res.status(401).json({
success: false,
error: {
status: 401,
message: 'token過期或無效',
},
});
}
// 如果是開發(fā)環(huán)境,返回更詳細(xì)的錯誤信息
if (process.env.NODE_ENV === 'development') {
return res.status(statusCode).json({
success: false,
error: {
status: statusCode,
message: message,
stack: err.stack,
},
});
}
// 如果是生產(chǎn)環(huán)境,只返回狀態(tài)碼和簡單的錯誤信息
return res.status(statusCode).json({
success: false,
error: {
status: statusCode,
message: message,
},
});
}
module.exports = errorHandler;
如果token過期則讓用戶重新進(jìn)行登錄即可。
以上第二點(diǎn)手動設(shè)置token也可由前端傳過來,后端需返回token給前端進(jìn)行存儲并傳回后臺,一開始使用直接后臺設(shè)置,但是發(fā)現(xiàn)后臺設(shè)置的話,比較不容易判斷crud的時候是查哪個用戶的信息,大多是以下的操作步驟:
a.用戶登錄后,后臺會返回token給前端
b.前端拿到token后,在請求攔截器那里設(shè)置一下authorization,傳給后臺
c.后臺拿到后會經(jīng)過jwt校驗(yàn)登陸狀態(tài),成功后在每個接口處可以獲取到信息req.user,再做crud操作。
具體代碼等整理后在進(jìn)行發(fā)布....