利用业余时间,自己做了一个食物热量参考网站,数据参考自一个app食物库
。技术栈使用了sass
+react
+react-router
+redux
+antd
+express
+mongoose
。
一、How To Use 下载 首先将代码clone到本地
1 git clone https ://github.com/mescalchuan/node -health .git
安装依赖包
引入数据并启动mongodb服务 要确保你已经安装了mongodb
,然后在自己电脑上新建数据库文件夹(我的是E:\mongodbData\db)。在mongodb
安装目录的bin
文件夹下启动mongodb
服务:
1 mongodb --dbpath= "E:\mongodbData\db" --port 27017 -journal
启动成功后,数据库是没有任何数据的,我们需要将一些默认数据导入进来,我已经将这些数据导出成json
了,你只需要重开一个命令行并输入:
1 2 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
这里推荐一个超轻量级数据库操作工具:adminMongo 。
如果数据导入成功,那么在food
和category
表里会看到导入进来的数据,否则,你需要在adminMongo
里自己手动创建这两张表,然后再导入数据就可以了。
启动前端服务 1 2 cd /e/node -health webpack --watch
用户:http://localhost:8888
管理员:http://localhost:8888/admin.html
,用户名和密码均为admin
启动后台服务 1 2 cd /e/node -health node app
项目截图
二、说明 该项目适用于有一定前端基础(包括react
和redux
)和node.js
基础的同学,如果你正在学习node
,但又无法将一系列知识体系串起来,那么本项目同样适合你~
三、环境搭建 整体目录结构
controller:后端控制层
db:导出的数据库json
文件
model:后端模型层
router:后端路由
src:前端代码
admin.ejs:管理员页面(模板引擎)
app.js:后端根文件
index.ejs:用户页面(模板引擎)
webpack.config.js:webpack
配置文件
前端 目录结构如下:
从头搭建webpack
吧,由于用到了后台模板引擎,因此我们就不再单独用webpack
启动一个服务了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 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" ); var HappyPack = require("happypack" ); var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; var NoEmitOnErrorsPlugin = webpack.NoEmitOnErrorsPlugin; var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; var externals = { "React" : "react" , "ReactDOM" : "react-dom" } var entry = { "index" : "./src/index.js" , "admin" : "./src/admin.js" }; 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 }), new OptimizeCSSPlugin() ] }; module.exports = webpackConfig;
之后,使用webpack --watch
既可以完成打包。
后端 后端基于express
和mongoose
,用到了express-session
和body-parser
,所以我们先把这些包安装好:
1 npm i express mongoose express -session body-parser -S
然后我们看一下app.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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());app .use (bodyParser.json());app .use (bodyParser.urlencoded({ extended: false }));app .use (session({ secret: 'keyboard cat ', resave: false, saveUninitialized: true })); app .set ("views" , __dirname);app .set ("view engine" , "ejs" );app .use (express.static("src" ));app .use (express.static(__dirname));app .get ("/" , (req, res) => { return res.render("index" , { userName: "" , token : "" , hasLogin: false }) }) 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!" ); })
运行node app
:
我们还可以使用supervisor
实现代码更新功能,只需要npm i supervisor -g
然后用supervisor app
代替node app
即可。每次代码有了变更都会自动帮你重启服务器。
环境搭建结束 到此步为止,环境搭建已经结束,项目也可以成功跑起来了,只不过没有任何内容,剩下的就是一步一步写业务。
四、CSRF防范 在写业务之前,简单实现了一下CSRF的
防范,我的做法是管理员登录成功后,后端直接在页面中生成一个script
标签,标签内包含了简单的登录信息和token
。之后管理员每一次与后端交互都要发送这个token
,由后端校验token
,如果不一致,则直接返回,不再执行正常逻辑。
模板引擎 由后端生成script
标签,让我最先想到了模板引擎,因此我使用了ejs
来实现该功能,这也是为什么用户页面和管理员页面的后缀不是html
的原因。我们看一下admin.html
里面的内容吧:
当管理员登录成功后,就可以全局访问userInfo
了。
下面我们看一下登录的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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请求和资源请求,在其中做一些操作然后控制请求是否继续往下执行,就像一个管道一样。在express
中,中间件
的作用和其是一样的,我们看一下中间件的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 const interceptor = require("./controller/interceptorCtrl" );app .use ((req, res, next) => { interceptor(req, res, next); }) const jwt = require('jsonwebtoken');const interceptor = module.exports = (req, res, next) => { let url = req.path; if (!!(~url.indexOf(".html" ))) { url = url.replace (/\ const page = url.split ("." )[0]; 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(); } 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
并将其和登录信息渲染到页面上;如果没有登录信息,则渲染空值即可,执行next()
让请求继续往下执行。如果是ajax请求,获取请求参数中的token
并解密,校验值的正确性:如果不正确,则直接返回错误信息,请求不再往下执行;如果正确,执行next()
让请求继续往下执行。
五、写一个Ajax吧 我们以管理员获取所有分类为例,看一下前后端分别是如何实现的。
前端 组件在componentDidMount
阶段发起server
的请求 –> 等待后端返回数据 –> 发起action
–> reducer
中保存数据 –> 更新视图
由于用户和管理员都需要获取分类列表,因此我将分类的server
和action
都划分到了用户模块。
src/components/admin/center.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... 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 1 2 3 4 5 6 7 ... export function getCategory (category) { return { type : types.GET_CATEGORY, category } }
src/reducer/adminReducer.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... const default State = { category: [] } const adminReducer = (state = default State, action) => { switch(action.type) { case types.GET_CATEGORY: return Object.assign({}, state , { category: action.category }) default : return state ; } } export default adminReducer;
后端 中间件
拦截请求,校验token
并继续执行 –> 路由映射 –> 转发给控制层 –> 处理并返回数据
app.js 1 2 3 4 5 ... const adminRouter = require ("./router/adminRouter" ); app.use("/api/admin" , adminRouter); ...
router/adminRouter.js 1 2 3 4 5 6 7 8 9 10 const express = require ("express" );const category= require ("../controller/user/category" );const router = express.Router();... router.get("/getCategory" , (req, res ) => { category.getCategory(req, res); }) module .exports = router;
controller/user/category 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const models = require ("../../model/index" );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 }
六、图片上传 管理员添加和修改食物信息时需要上传图片。如果只是练习的话,可以将图片保存到本地并将图片绝对路径保存到数据库中。但是,我们来个更加贴切真实项目的吧,将图片保存到图片服务器中~
我们将图片保存到七牛云
存储系统中,你需要先注册个账号,官网地址在这里 。 因七牛云
取消了测试账号,现已将图片全部存储在阿里云
中,官网地址在这里 。
在管理控制台 –> 对象存储 –> 内容管理中可以看到已经存储的图片: 在控制台 –> 对象存储 OSS –> 文件管理中可以看到已经存储的图片:
下一步要做的就是前端上传图片发送给后端,后端上传到七牛云阿里云并将图片链接保存到数据库。
前端上传图片 使用<input type="file" />
实现图片选择。默认样式比较丑,因此我自己重写了样式:
然后要做的就是使用formData
对象将图片信息发送给后端。
1 2 3 4 5 6 7 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
即可。
1 2 3 4 5 6 7 8 9 10 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 。
我们首先要做一些配置:
1 2 3 4 5 6 7 8 9 10 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访问权限,建议遵循阿里云安全最佳实践,部署在服务端使用RAM子账号或STS,部署在客户端使用STS。 accessKeyId: 'LTAIa2EaQxqPMBfb' , accessKeySecret: 'WjKeNw8gAdU1y80SpO1JYnWfzq9Pbe' , bucket: 'mescal-chuan' });
接下来要做的就是将图片信息上传到七牛云阿里云:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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; //保存到数据库即可 } }
我们可以在七牛云上看到已经上传的图片:
结束语 本项目从功能上来说只是简单的CRUD
,但用到的技术比较多,也是为了给自己做一个整体技术栈的实战,后期还可以考虑添加分页和排序功能。
如果你觉得对你有帮助,欢迎star
~,如果有任何疑问或bug,也欢迎提供issue
。