WEB实战:使用MERN技术栈开发项目

本文介绍如何使用 MERN 技术栈开发一个前后端分离的电商项目,水平有限,不足之处,请指出, Github

后端开发

安装好mongodb 并启动, 新建一个后端项目目录, 目录结构如下

WEB实战:使用MERN技术栈开发项目
WEB实战:使用MERN技术栈开发项目
当然也可以按自己的方式创建, 执行 npm init

后,安装需要用到的库

npm i express mongoose multer validator jsonwebtoken dotenv cors bcrypt -S

图片上传 multer , 验证表单数据 validator , 配置环境变量 dotenv , 跨域处理 cors

新建 .env 文档,在里面配置数据库等参数

DB_HOST=localhost
DB_PORT=27017
DB_NAME=cake-shop
JWT_KEY=my_jwt_key
PORT=9090
HOSTNAME=http://localhost
复制代码

接着在 models 目录下定义数据模型 product.js 代表产品,其他同理

// product.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const productSchema = new Schema(
  {
    name: {
      type: String,
      required: true,
      default: ''
    },
    description: {
      type: String,
      default: ''
    },
    price: {
      type: Number,
      required: true
    },
    stock: {
      type: Number,
      default: 0
    },
    imgList: {
      type: Array,
      default: ''
    },
    category: {
      type: Array
    },
    top: {
      type: Boolean,
      default: false
    },
    rate: {
      type: Number,
      default: 5.0
    },
    publish: {
      type: Boolean,
      default: false
    }
  },
  {
    timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
  }
)

module.exports = mongoose.model('Product', productSchema)
复制代码

在 routes 目录下创建 product.js 用来定义产品相关的路由,创建 index.jsproduct.js 路由导出, 然后在 controllers 目录下创建 product.js 用来处理产品相关的路由, 然后在入口文档中使用即可

// routes/product.js
const express = require('express')
const router = express.Router()
const auth = require('../middleware/auth')
const controller = require('../controllers/product')

router.get('/', controller.getAllProducts)
router.get('/top', controller.getTopProducts)
router.get('/recommend', controller.getRecommendProducts)
router.get('/detail/:productId', controller.getProductDetail)
router.get('/:sort', controller.getProducts)
router.post('/', controller.addProduct)

router
  .route('/:productId')
  .put(auth, controller.updateProduct)
  .delete(auth, controller.deleteProduct)

module.exports = router

复制代码
// index.js
const routes = require('./routes')
.....
app.use('/api/product', routes.product)
复制代码

在 controller 里面编写路由对应的逻辑代码,用 Postman 将代码逻辑跑通,用户注册时要对字段进行验证,以及需要对用户密码加密,用户登录时要将 jsonwentoken 生成的token 返回给前端

// controllers/user.js
// 注册
async function signUp(req, res, next) {
  let { name, email, password } = req.body

  if (!isVerifiedField(name)) {
    return res.status(400).json({ success: false, message: '请输入字符长度大于4的用户名' })
  }

  if (!isVerifiedEmail(email)) {
    return res.status(400).json({ success: false, message: '请输入正确的邮箱地址' })
  }

  if (!isVerifiedField(password)) {
    return res.status(400).json({ success: false, message: '请设置长度不小于4个字符的密码' })
  }

  const oldUser = await User.findOne({ email }).exec() // 检验用户是否已存在

  if (oldUser !== null) {
    return res.status(409).json({ success: false, message: '用户已存在' })
  }

  password = await bcrypt.hash(password, 10)  // 对用户密码加密

  const newUser = new User({ name, email, password })

  newUser
    .save()
    .then(result => {
      return res.status(201).json({ success: true, result })
    })
    .catch(error => {
      return res.status(500).json({ success: false, error })
    })
}
// 登录
async function login(req, res, next) {
  const { name, email, password } = req.body

  if (name) {
    checkField({ name })
  }

  if (email) {
    checkField({ email })
  }

  async function checkField(field) {
    const user = await User.findOne(field).exec()

    if (user === null) {
      return res.status(404).json({ success: false, message: '用户不存在' })
    }

    const isMatch = await bcrypt.compare(password, user.password)

    if (isMatch) {
      const token = jwt.sign({ field, id: user._id }, process.env.JWT_KEY) // 生成token

      return res.status(200).json({ success: true, message: '登录成功', token }) // 返回token
    } else {
      return res.status(401).json({ success: false, message: '密码错误' })
    }
  }
}
复制代码

管理后台

使用 create-react-app 创建项目,使用 yarn 安装需要的依赖包

npx create-react-app you-project

yarn add antd react-router-dom axios

根据 create-react-appUser Guide 配置 CSS 预处理器等

创建项目目录结构

WEB实战:使用MERN技术栈开发项目
WEB实战:使用MERN技术栈开发项目

配置 axios

import axios from 'axios'

const token = localStorage.getItem('CAKE_SHOP_AUTH_TOKEN')

const Request = axios.create({
  baseURL: 'http://localhost:9090/api',
  timeout: 5000,
  headers: {
    authorization: token ? token : '' // 如果有token就在请求headers里面带上
  }
})

export default Request

复制代码

在 pages 目录下创建管理员注册,登录等页面,用户登录后把后端返回的token放到 localStorage 里面,然后跳转到工作台首页

// pages/Login

import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import { Layout, Form, Icon, Input, Button, Checkbox, message as Message } from 'antd'

import '../account.css'
import { ManagerContext } from '../../store/manager'
import ManagerService from '../../services/manager'
import { CAKE_SHOP_AUTH_TOKEN, CAKE_SHOP_USER_INFO } from '../../constant'

const { Item } = Form
const { Content, Footer } = Layout

class Login extends Component {
  handleSubmit = e => {
    e.preventDefault()
    this.props.form.validateFields(async (err, values) => {
      if (!err) {
        const { name, password } = values

        ManagerService.login(name, password)  // 请求登录接口
          .then(res => {
            const { history, login } = this.props // history 对象来自于 react-router-dom
            const { message, token, manager } = res.data

            Message.success(message)
            localStorage.setItem(CAKE_SHOP_AUTH_TOKEN, `Bearer ${token}`)
            localStorage.setItem(CAKE_SHOP_USER_INFO, JSON.stringify(manager))
            login(manager)
            history.push('/dashboard')
          })
          .catch(error => {
            const { data } = error.response

            Message.error(data.message)
          })
      }
    })
  }

  render() {
    const { getFieldDecorator } = this.props.form

    return (
      <ManagerContext.Consumer>
        {login => (
          <Layout className="account" login={login}>
            <Content className="account__content">
              <h1 className="account__title">店铺管理系统</h1>
              <sub className="account__sub-title">登录</sub>
              <Form className="account__form" onSubmit={this.handleSubmit}>
                <Item>
                  {getFieldDecorator('name', {
                    rules: [{ required: true, message: '请输入你的用户名!' }]
                  })(
                    <Input
                      prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                      placeholder="Username admin"
                    />
                  )}
                </Item>
                <Item>
                  {getFieldDecorator('password', {
                    rules: [{ required: true, message: '请输入你的密码!' }]
                  })(
                    <Input
                      prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                      type="password"
                      placeholder="Password admin"
                    />
                  )}
                </Item>
                <Item>
                  <Button type="primary" htmlType="submit" block>
                    登录
                  </Button>
                </Item>
                <Item>
                  {getFieldDecorator('remember', {
                    valuePropName: 'checked',
                    initialValue: true
                  })(<Checkbox>记住我</Checkbox>)}
                </Item>
              </Form>
            </Content>
            <Footer className="account__footer">
              <a
                className="account__link"
                href="https://xrr2016.github/io"
                target="_blank"
                rel="noopener noreferrer"
              >
                <Icon type="github" />
              </a>
              <a className="account__link" href="mailto:xiaoranran1993@outlook.com">
                <Icon type="mail" />
              </a>
            </Footer>
          </Layout>
        )}
      </ManagerContext.Consumer>
    )
  }
}

export default withRouter(Form.create()(Login))

复制代码

在 routes 目录下创建工作台里面的子路由页面,编写对应逻辑, 处理不同的业务, 最后在入口文档中定义 react-router-dom 的路由

WEB实战:使用MERN技术栈开发项目
WEB实战:使用MERN技术栈开发项目

前端页面

同样使用 create-react-app 创建项目,使用 yarn 安装需要的依赖包

npx create-react-app you-project

yarn add antd-mobile react-router-dom axios

创建项目目录结构

WEB实战:使用MERN技术栈开发项目
WEB实战:使用MERN技术栈开发项目

这里的逻辑跟管理后台主要的区别在于请求的数据接口不同,以及页面的UI不同,具体实现,UI交互等按个人而定。

源码地址

部署

功能开发完毕后,使用 yanr build 将前端以及管理后台项目打包,将代码传到服务器上,配置不同的域名,使用 nginx 进行反向代理,防止刷新浏览器后404。

location / {
  try_files $uri $uri/ /index.html;
}
复制代码