利用業(yè)余時間同廉,自己做了一個食物熱量參考網(wǎng)站苟穆,數(shù)據(jù)參考自一個app食物庫
藤肢。技術(shù)棧使用了sass
+react
+react-router
+redux
+antd
+express
+mongoose
。
一愧旦、How To Use
下載
首先將代碼clone到本地
git clone https://github.com/mescalchuan/node-health.git
安裝依賴包
cd node-health && npm i
引入數(shù)據(jù)并啟動mongodb服務(wù)
要確保你已經(jīng)安裝了mongodb
枢贿,然后在自己電腦上新建數(shù)據(jù)庫文件夾(我的是E:\mongodbData\db)。在mongodb
安裝目錄的bin
文件夾下啟動mongodb
服務(wù):
mongodb --dbpath="E:\mongodbData\db" --port 27017 -journal
啟動成功后举户,數(shù)據(jù)庫是沒有任何數(shù)據(jù)的器联,我們需要將一些默認(rèn)數(shù)據(jù)導(dǎo)入進(jìn)來二汛,我已經(jīng)將這些數(shù)據(jù)導(dǎo)出成json
了,你只需要重開一個命令行并輸入:
mongoexport -d db -c category -o "E:node-health\db\category.json" --type json --port 27017
mongoexport -d db -c food -o "E:node-health\db\food.json" --type json --port 27017
這里推薦一個超輕量級數(shù)據(jù)庫操作工具:adminMongo拨拓。
如果數(shù)據(jù)導(dǎo)入成功肴颊,那么在food
和category
表里會看到導(dǎo)入進(jìn)來的數(shù)據(jù),否則渣磷,你需要在adminMongo
里自己手動創(chuàng)建這兩張表婿着,然后再導(dǎo)入數(shù)據(jù)就可以了。
啟動前端服務(wù)
cd /e/node-health
webpack --watch
用戶:http://localhost:8888
管理員:http://localhost:8888/admin.html
醋界,用戶名和密碼均為admin
啟動后臺服務(wù)
cd /e/node-health
node app
項(xiàng)目截圖
二竟宋、說明
該項(xiàng)目適用于有一定前端基礎(chǔ)(包括react
和redux
)和node.js
基礎(chǔ)的同學(xué),如果你正在學(xué)習(xí)node
形纺,但又無法將一系列知識體系串起來丘侠,那么本項(xiàng)目同樣適合你~
三、環(huán)境搭建
整體目錄結(jié)構(gòu)
- controller:后端控制層
- db:導(dǎo)出的數(shù)據(jù)庫
json
文件 - model:后端模型層
- router:后端路由
- src:前端代碼
- admin.ejs:管理員頁面(模板引擎)
- app.js:后端根文件
- index.ejs:用戶頁面(模板引擎)
- webpack.config.js:
webpack
配置文件
前端
目錄結(jié)構(gòu)如下:
從頭搭建webpack
吧逐样,由于用到了后臺模板引擎蜗字,因此我們就不再單獨(dú)用webpack
啟動一個服務(wù)了打肝。
var path = require("path");
var webpack = require("webpack");
var OpenBrowserPlugin = require("open-browser-webpack-plugin");
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var OptimizeCSSPlugin = require("optimize-css-assets-webpack-plugin");
//提高loader的解析速度
var HappyPack = require("happypack");
var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
var NoEmitOnErrorsPlugin = webpack.NoEmitOnErrorsPlugin;
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
//externals配置的對象在生產(chǎn)環(huán)境下會自動引入CDN的對象,不會將node_modules下的文件打包進(jìn)來
var externals = {
"React": "react",
"ReactDOM": "react-dom"
}
//配置多入口文件挪捕,包括用戶和管理員
var entry = {
"index": "./src/index.js",
"admin": "./src/admin.js"
};
//最基本的webpack配置
var webpackConfig = {
entry: entry,
output: {
path: path.resolve(__dirname, "src/build"),
filename: "[name].bundle.js"
},
externals: externals,
devtool: "source-map",
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ["happypack/loader?id=babel"]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 8192,
name: "[name].[ext]"
}
}, {
test: /\.css$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ["css-loader"]
})
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
use: ["css-loader", "sass-loader"],
fallback: "style-loader"
})
}
]
},
resolve: {
extensions: [".js", ".json"]
},
plugins: [
new HappyPack({
id: "babel",
loaders: [{
loader: "babel-loader",
options: {
presets: ["es2015", "stage-2", "react"]
}
}]
}),
new CommonsChunkPlugin({
name: ["vendor"],
filename: "vendor.bundle.js",
minChunks: Infinity
}),
new NoEmitOnErrorsPlugin(),
new OpenBrowserPlugin({
url: "http://localhost:8888"
}),
new ExtractTextPlugin("[name].bundle.css", {
allChunks: false
}),
//為了方便調(diào)試闯睹,暫時屏蔽
// new UglifyJsPlugin({
// minimize: true,
// output: {
// comments: false,
// beautify: false
// },
// compress: {
// warnings: false,
// drop_console: true,
// collapse_vars: true,
// reduce_vars: true
// }
// }),
new OptimizeCSSPlugin()
]
};
module.exports = webpackConfig;
之后,使用webpack --watch
既可以完成打包担神。
后端
后端基于express
和mongoose
,用到了express-session
和body-parser
始花,所以我們先把這些包安裝好:
npm i express mongoose express-session body-parser -S
然后我們看一下app.js
:
const express = require("express");
const mongoose = require("mongoose");
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser");
const session = require("express-session");
const app = express();
app.use(cookieParser());
//解析post請求
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
//設(shè)置session
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}));
//設(shè)置存放模板文件的目錄
app.set("views", __dirname);
//設(shè)置模板引擎為ejs
app.set("view engine", "ejs");
//訪問靜態(tài)資源文件
app.use(express.static("src"));
app.use(express.static(__dirname));
app.get("/", (req, res) => {
return res.render("index", {
userName: "",
token: "",
hasLogin: false
})
})
//連接mongodb妄讯,db為該工程的數(shù)據(jù)庫名
mongoose.connect("mongodb://localhost/db", function(err, db) {
if(err) {
console.log("連接失敗");
process.exit(1);
}
else {
console.log("連接成功")
}
})
app.listen("8888", () => {
console.log("server created!");
})
運(yùn)行node app
:
我們還可以使用supervisor
實(shí)現(xiàn)代碼更新功能,只需要npm i supervisor -g
然后用supervisor app
代替node app
即可酷宵。每次代碼有了變更都會自動幫你重啟服務(wù)器亥贸。
環(huán)境搭建結(jié)束
到此步為止,環(huán)境搭建已經(jīng)結(jié)束浇垦,項(xiàng)目也可以成功跑起來了炕置,只不過沒有任何內(nèi)容,剩下的就是一步一步寫業(yè)務(wù)男韧。
四朴摊、CSRF防范
在寫業(yè)務(wù)之前,簡單實(shí)現(xiàn)了一下CSRF的
防范此虑,我的做法是管理員登錄成功后甚纲,后端直接在頁面中生成一個script
標(biāo)簽,標(biāo)簽內(nèi)包含了簡單的登錄信息和token
朦前。之后管理員每一次與后端交互都要發(fā)送這個token
介杆,由后端校驗(yàn)token
,如果不一致韭寸,則直接返回春哨,不再執(zhí)行正常邏輯。
模板引擎
由后端生成script
標(biāo)簽恩伺,讓我最先想到了模板引擎赴背,因此我使用了ejs
來實(shí)現(xiàn)該功能,這也是為什么用戶頁面和管理員頁面的后綴不是html
的原因莫其。我們看一下admin.ejs
里面的內(nèi)容吧:
當(dāng)管理員登錄成功后癞尚,就可以全局訪問userInfo
了。
下面我們看一下登錄的邏輯:
const login = module.exports = (req, res) => {
const {userName, password} = req.body;
const session = req.session;
//用戶名和密碼正確乱陡,保存session浇揩,并告知前端登錄成功
if(userName === "admin" && password === "admin") {
if(session) {
if(session.user) {
res.json({
retCode: -1,
retMsg: "您已登錄過了"
})
}
else {
session.user = {
userName,
password
}
res.json({
retCode: 0,
retInfo: {}
})
}
}
else {
res.json({
retCode: -1,
retMsg: ""
})
}
}
else {
res.json({
retCode: -1,
retMsg: "用戶名或密碼錯誤"
})
}
}
中間件
管理員登錄成功后,session
里面保存了登錄信息憨颠,那么下一步就是生成token
并將其和用戶登錄信息渲染到頁面中胳徽。在asp.net
和java
中有一個叫做攔截器
的東西积锅,它的作用就是攔截所有請求,包括ajax請求和資源請求养盗,在其中做一些操作然后控制請求是否繼續(xù)往下執(zhí)行缚陷,就像一個管道一樣。在express
中往核,中間件
的作用和其是一樣的箫爷,我們看一下中間件的代碼:
//app,js
const interceptor = require("./controller/interceptorCtrl");
app.use((req, res, next) => {
interceptor(req, res, next);
})
//interceptorCtrl.js
const jwt = require('jsonwebtoken');
const interceptor = module.exports = (req, res, next) => {
let url = req.path;
//頁面請求,判斷session是否有值聂儒,如果有的話則生成token并將userName虎锚、token、hasLogin渲染到頁面上
if(!!(~url.indexOf(".html"))) {
url = url.replace(/\//g, "");
const page = url.split(".")[0];
//將用戶登錄信息和token返回給前臺
if(req.session.user) {
const token = jwt.sign({name: "token"}, "node-health", {expiresIn: 600});
const { userName } = req.session.user;
res.render(page, {
userName,
token,
hasLogin: true
})
}
else {
res.render(page, {
userName: "",
token: "",
hasLogin: false
})
}
next();
}
//如果是ajax請求并且請求接口來自管理員衩婚,那么校驗(yàn)請求參數(shù)中的token是否正確窜护,不正確的話則直接返回retCode 500
else if(!!(~url.indexOf("/api/admin"))) {
let token = "";
const method = req.method.toLowerCase();
if(method == "get") {
token = req.query.token;
}
else {
token = req.body.token;
}
jwt.verify(token, "node-health", function (err, decoded) {
if (!err) {
if(decoded.name !== "token") {
return res.json({
retCode: 500,
retMsg: "csrf"
})
}
else {
next();
}
}
else {
return res.json({
retCode: 500,
retMsg: "csrf"
})
}
})
}
else {
next();
}
}
功能很簡單:如果是頁面請求,則判斷session是否有用戶信息:如果有的話說明登錄成功了非春,生成token
并將其和登錄信息渲染到頁面上柱徙;如果沒有登錄信息,則渲染空值即可奇昙,執(zhí)行next()
讓請求繼續(xù)往下執(zhí)行护侮。如果是ajax請求,獲取請求參數(shù)中的token
并解密储耐,校驗(yàn)值的正確性:如果不正確概行,則直接返回錯誤信息,請求不再往下執(zhí)行弧岳;如果正確凳忙,執(zhí)行next()
讓請求繼續(xù)往下執(zhí)行。
五禽炬、寫一個Ajax吧
我們以管理員獲取所有分類為例涧卵,看一下前后端分別是如何實(shí)現(xiàn)的。
前端
組件在componentDidMount
階段發(fā)起server
的請求 --> 等待后端返回數(shù)據(jù) --> 發(fā)起action
--> reducer
中保存數(shù)據(jù) --> 更新視圖
由于用戶和管理員都需要獲取分類列表腹尖,因此我將分類的server
和action
都劃分到了用戶模塊柳恐。
src/components/admin/center.js
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import * as server from "../../server/adminServer";
...
componentDidMount() {
this.props.actions.getCategory({token: userInfo.token}, null, res => message.error(res.retMsg));
}
render() {
return (
<div>
...
{/*渲染分類列表*/}
{this.props.category.map((item, index) => (<div>...</div>)}
</div>
)
}
...
// 將actions綁定到props上
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(server, dispatch)
});
//將state綁定到props上
const mapStateToProps = (state) => ({
category: state.adminReducer.category
});
export default connect(mapStateToProps, mapDispatchToProps)(AdminCenter);
src/server/adminServer.js
這里使用到了redux-thunk
。
...
import * as action from "../action/userAction";
export function getCategory(successBK, errorBK) {
return (dispatch, getState) => {
return getData(url.SERVER_ADMIN + url.GET_CATEGORY).then(res => {
if(res.retCode == 0) {
dispatch(action.getCategory(res.retInfo));
successBK && successBK(res.retInfo);
}
else {
errorBK && errorBK(res);
}
}, e => console.log(e))
.catch(e => console.log(e))
}
}
src/actionType/userAction.js
...
export function getCategory(category) {
return {
type: types.GET_CATEGORY,
category
}
}
src/reducer/adminReducer.js
...
const defaultState = {
category: []
}
const adminReducer = (state = defaultState, action) => {
switch(action.type) {
case types.GET_CATEGORY:
return Object.assign({}, state, {
category: action.category
})
default:
return state;
}
}
export default adminReducer;
后端
中間件
攔截請求热幔,校驗(yàn)token
并繼續(xù)執(zhí)行 --> 路由映射 --> 轉(zhuǎn)發(fā)給控制層 --> 處理并返回數(shù)據(jù)
app.js
...
const adminRouter = require("./router/adminRouter");
app.use("/api/admin", adminRouter);
...
router/adminRouter.js
const express = require("express");
const category= require("../controller/user/category");
const router = express.Router();
...
//調(diào)用控制層
router.get("/getCategory", (req, res) => {
category.getCategory(req, res);
})
module.exports = router;
controller/user/category
const models = require("../../model/index");
//從數(shù)據(jù)庫中讀取分類并返回給前端
const getCategory = (req, res) => {
models.Category.find((err, result) => {
if(err) {
res.json({
retCode: -1,
retMsg: "mongoose error"
})
}
res.json({
retCode: 0,
retInfo: result
})
})
}
module.exports = {
getCategory
}
六乐设、圖片上傳
管理員添加和修改食物信息時需要上傳圖片。如果只是練習(xí)的話绎巨,可以將圖片保存到本地并將圖片絕對路徑保存到數(shù)據(jù)庫中近尚。但是,我們來個更加貼切真實(shí)項(xiàng)目的吧场勤,將圖片保存到圖片服務(wù)器中~
我們將圖片保存到七牛云
存儲系統(tǒng)中戈锻,你需要先注冊個賬號歼跟,官網(wǎng)地址在這里。
因七牛云
取消了測試賬號格遭,現(xiàn)已將圖片全部存儲在阿里云
中哈街,官網(wǎng)地址在這里。
在管理控制臺 --> 對象存儲 --> 內(nèi)容管理中可以看到已經(jīng)存儲的圖片:
在控制臺 --> 對象存儲 OSS --> 文件管理中可以看到已經(jīng)存儲的圖片:
下一步要做的就是前端上傳圖片發(fā)送給后端拒迅,后端上傳到七牛云阿里云并將圖片鏈接保存到數(shù)據(jù)庫骚秦。
前端上傳圖片
使用<input type="file" />
實(shí)現(xiàn)圖片選擇。默認(rèn)樣式比較丑璧微,因此我自己重寫了樣式:
然后要做的就是使用formData
對象將圖片信息發(fā)送給后端骤竹。
const fileEle = this.refs.file;
const file = fileEle.files[0];
let formData = new FormData();
formData.append("imgUrl", file);
formData.append("name", this.state.name);
...
this.props.actions.addFood(formData);
后端接收圖片
后端接收圖片需要用到multiparty
插件,你只需要npm i multiparty -S
即可往毡。
//controller/admin/foodHandler.js
const multiparty = require("multiparty");
const addFood = (req, res) => {
const form = new multiparty.Form();
form.parse(req, (err, fields, files) => {
console.log(fields);
console.log(files);
})
}
上傳到七牛云阿里云
我們需要使用到七牛云的node sdk
,npm i qiniu -S
npm i ali-oss
靶溜。使用文檔請訪問Node.js SDK开瞭。
我們首先要做一些配置:
const domain = "http://mescal-chuan.oss-cn-beijing.aliyuncs.com/";
const OSS = require('ali-oss');
const client = new OSS({
region: 'oss-cn-beijing',
//云賬號AccessKey有所有API訪問權(quán)限,建議遵循阿里云安全最佳實(shí)踐罩息,部署在服務(wù)端使用RAM子賬號或STS嗤详,部署在客戶端使用STS。
accessKeyId: 'LTAIa2EaQxqPMBfb',
accessKeySecret: 'WjKeNw8gAdU1y80SpO1JYnWfzq9Pbe',
bucket: 'mescal-chuan'
});
接下來要做的就是將圖片信息上傳到七牛云阿里云:
const file = files.imgUrl[0];
const localFile = file.path//"/Users/jemy/Documents/qiniu.mp4";
let temp = file.path.split("\\");
if(temp.length <= 1) {
temp = file.path.split("/")
}
const key = temp[temp.length - 1]//'test.mp4';
// 文件上傳
client.put('/' + key, localFile).then((respBody, reject) => {
if (reject) {
res.json({
retCode: -1,
retMsg: "ali yun upload error"
})
throw reject;
}
if(respBody.res.statusCode == 200) {
const imgUrl = domain + respBody.name;
//保存到數(shù)據(jù)庫即可
}
}
我們可以在七牛云阿里云上看到已經(jīng)上傳的圖片:
結(jié)束語
本項(xiàng)目從功能上來說只是簡單的CRUD
瓷炮,但用到的技術(shù)比較多葱色,也是為了給自己做一個整體技術(shù)棧的實(shí)戰(zhàn),后期還可以考慮添加分頁和排序功能娘香。
如果你覺得對你有幫助苍狰,歡迎star
~,如果有任何疑問或bug烘绽,也歡迎提供issue
淋昭。