# 1.环境搭建和路由处理

# 1.1.环境搭建

TIP

当前我用的node版本为 v14.15.3,npm版本为6.14.9

创建项目文件夹

mkdir blog && cd blog

初始化项目

npm init -y

创建相关目录

// 启动项目相关命令
mkdir bin
// public 为静态文件目录
mkdir public
// routes 路由相关目录
mkdir routes
// app.js 为入口文件
touch app.js

使用nodemon自动重启

npm i nodemon -g

使用cross-env设置环境变量

npm i cross-env -g

创建服务器程序

代码位置:bin/www.js

const http = require("http")
const url = require('url')
const querystring = require('querystring')



const server = http.createServer((req, res)=>{
    // 设置返回数据类型
    res.setHeader('Content-type', 'application/json;charset=utf-8')
    // 解析url字符串
    let url_obj = url.parse(req.url)
    // 获取到path 用来做路由判断
    req.path = url_obj.pathname

    // 获取到query
    req.query = querystring.parse(url_obj.query)

    // 根据id来获取博客详情
    const method = req.method
    if(method === "GET" && req.path === "/api/article/detail"){
        let result = {"message": "成功", "data": [{"title": "这是一个标题"}]}
        res.end(JSON.stringify(result))
    }

})
server.listen(3000)

运行命令测试程序是否正常启动

nodemon ./bin/www.js

访问:http://localhost:3000/api/article/detail,可以查看到相应结果

接下来我们去配置启动命令

在package.json中做如下配置,即可兼容mac平台和windows平台

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "cross-env NODE_ENV=dev nodemon ./bin/www.js",
    "serve": "cross-env NODE_ENV=production nodemon ./bin/www.js"
  },

运行命令即可启动

// 开发环境启动
npm run dev 
// 生产环境启动
npm run serve 

接下来,我们需要拆分文件,我们把服务对应的处理逻辑和服务器启动的相关配置拆分成不同的文件,在app.js入口文件中,添加代码

const url = require('url')
const querystring = require('querystring')
const serveCallBack = (req, res)=>{
    console.log(process.env.NODE_ENV)
    // 设置返回数据类型
    res.setHeader('Content-type', 'application/json;charset=utf-8')
    // 解析url字符串
    let url_obj = url.parse(req.url)
    // 获取到path 用来做路由判断
    req.path = url_obj.pathname

    // 获取到query
    req.query = querystring.parse(url_obj.query)

    // 根据id来获取博客详情
    const method = req.method
    if(method === "GET" && req.path === "/api/article/detail"){
        let result = {"message": "成功", "data": [{"title": "这是一个标题"}]}
        res.end(JSON.stringify(result))
    }

}


module.exports = serveCallBack

./bin/www.js 中的代码

const http = require("http")

const app = require('../app') 


const server = http.createServer(app)
server.listen(3000)

# 1.2.初始化路由

前面我们已经跑通了服务器,接下来,我们需要做路由层面的拆分,在routes目录下,新建article.js文件,代码如下:

const articleRouter = (req, res) =>{
    // 根据id来获取博客详情
    const method = req.method
    if(method === "GET" && req.path === "/api/article/detail"){
        let result = {"message": "成功", "data": [{"title": "这是一个标题"}]}
        return result
    }
}

module.exports = articleRouter

app.js需要做一下修改,引入我们创建好的article路由模块,并且使用

const url = require('url')
const querystring = require('querystring')
const articleRouter = require('./routes/article')
const serveCallBack = (req, res)=>{
    console.log(process.env.NODE_ENV)
    // 设置返回数据类型
    res.setHeader('Content-type', 'application/json;charset=utf-8')
    // 解析url字符串
    let url_obj = url.parse(req.url)
    // 获取到path 用来做路由判断
    req.path = url_obj.pathname

    // 获取到query
    req.query = querystring.parse(url_obj.query)

    // 处理博客路由
    const articleData = articleRouter(req, res)
    if(articleData){
        res.end(JSON.stringify(articleData))
    }

}

module.exports = serveCallBack

在我们开发api的时候,返回数据格式需要统一,例如:

/**
 * 
 * 设置统一的返回格式为:
 * 
 * {
 *    
 *     message: "获取成功",
 *     data: [
 *          {
 *              "id":1,
 *              "title": "今天遇见一个大美女",
 *              "content": "今天早上,9点去坐公交车,在公交车上..."
 *          }
 *     ],
 *     errno: 0
 * }
 * 
 * 
 */

为了方便我们返回统一格式的数据,我们新建一个model层来处理相关数据,models/resultModel.js

/**
 * 
 * 设置统一的返回格式为:
 * 
 * {
 * 
 *     message: "获取成功",
 *     data: [
 *          {
 *              "title": "今天遇见一个大美女",
 *              "content": "今天早上,9点去坐公交车,在公交车上..."
 *          }
 *     ],
 *     errno: 0
 * }
 * 
 * 
 */

class SuccessModel {
    constructor(data, message){
        if(typeof data === "string"){
            // 如果第一个参数传进来是个字符串,就直接把字符串作为message
            this.message = data
        }else if(data && typeof data === "object"){
            this.data = data
        }
        if(message){
            this.message = message
        }
        this.errno = 0
    }
}

class ErrorModel {
    constructor(data, message){
        if(typeof data === "string"){
            // 如果第一个参数传进来是个字符串,就直接把字符串作为message
            this.message = data
        }else if(data && typeof data === "object"){
            this.data = data
        }
        if(message){
            this.message = message
        }
        this.errno = -1
    }
}

module.exports = {
    SuccessModel,
    ErrorModel
}

上面代码可以通过继承的方式简化


class BaseModel {
    constructor(data, message){
        if(typeof data === "string"){
            // 如果第一个参数传进来是个字符串,就直接把字符串作为message
            this.message = data
        }else if(data && typeof data === "object"){
            this.data = data
        }
        if(message){
            this.message = message
        }
    }
}
class SuccessModel extends BaseModel {
    constructor(data, message){
        super(data, message)
        this.errno = 0
    }
}

class ErrorModel extends BaseModel {
    constructor(data, message){
        super(data, message)
        this.errno = -1
    }
}

module.exports = {
    SuccessModel,
    ErrorModel
}

在返回数据的时候,使用这个model来返回统一格式的数据,

const { SuccessModel} = require('../models/reesultModel')
const articleRouter = (req, res) =>{
    // 根据id来获取博客详情
    const method = req.method
    if(method === "GET" && req.path === "/api/article/detail"){
        let result = [{"title": "这是一个标题"}]
        return new SuccessModel(result)
    }
}

module.exports = articleRouter

路由层的功能应该只是做路由表的映射,具体的数据输入应该由控制器层来完成,在MVC模式中中,可以将控制器层理解为数据输入,因此,我们需要新建一个controller层

controllers/article.js

const getDetail = (id) => {
    // 这里的数据为假数据,真实数据应该从数据库中去取,因此,还会具有model层来处理数据
    return {
        id: 1,
        title: "这是文章标题",
        content: "这是文章内容",
        create_at: Date.now(),
        author: "张三",
        views: 1988
    }
}
module.exports =  {
    getDetail
}

# 1.3.文章列表路由

经过前面的小节,我们除了真实数据库没有连接之外,基本的路由已经跑通了,接下来,我们就根据上面的模式,来按部就班的完成文章相关的其他功能

文章列表功能,是根据用户的搜索,返回搜索的数据,api地址为: /api/article/getlist?author=zhangsan&keywords=html

根据这个api地址,我们需要在路由里面做处理:routes/article.js

if(method === "GET" && req.path === "/api/article/getlist"){
    let author = req.query.author || ""
    let keywords = req.query.keywords || ""
    let data = getList(author, keywords)
    return new SuccessModel(data)
}

注意:上面的getList方法是从controlles/article.js里面引入的

const {getDetail, getList} = require("../controllers/article")

routes/article.js的完整代码为

const { SuccessModel} = require('../models/reesultModel')
const {getDetail, getList} = require("../controllers/article")
const articleRouter = (req, res) =>{
    // 根据id来获取博客详情
    const method = req.method
    if(method === "GET" && req.path === "/api/article/detail"){
        let id = req.query.id || ''
        let result = getDetail(id)
        return new SuccessModel(result)
    }

    if(method === "GET" && req.path === "/api/article/getlist"){
        let author = req.query.author || ""
        let keywords = req.query.keywords || ""
        let data = getList(author, keywords)
        return new SuccessModel(data)
    }
}

module.exports = articleRouter

controllers/article.js 的完整代码为:

const getDetail = (id) => {
    return {
        id: 1,
        title: "这是文章标题",
        content: "这是文章内容",
        create_at: Date.now(),
        author: "张三",
        views: 1988
    }
}

const getList = (author, keywords) => {
    // 先返回假数据
    return [
        {
            id: 1,
            title: "这是一个标题",
            content: "这是内容",
            create_at: 1609530003708,
            author: "张三",
            views: 1987
        },
        {
            id: 2,
            title: "这是一个标题",
            content: "这是内容",
            create_at: 1609530003708,
            author: "张三",
            views: 1988
        },
        {
            id: 3,
            title: "这是一个标题",
            content: "这是内容",
            create_at: 1609530003708,
            author: "张三",
            views: 1989
        }
    ]
}

module.exports =  {
    getDetail,
    getList
}

# 1.4.新建和更新文章

前面的路由主要涉及到的是get方式的请求,接下来的新建和更新文章,需要用到的是post请求,因为,我们需要对node里面post请求做封装

在node里面,post方式发送的数据,在后台接收属于异步操作,因此,我们需要使用Promise来解决

app.js里面的关键代码


const getPostData =  (req) => {

    return new Promise((resolve, reject) => {
        
        if (req.method !== "POST" || req.headers['content-type'] !== 'application/json'){
            resolve({})
            return
        }
        let postData = ''
        req.on('data', chunk => {

            postData += chunk
        })
        req.on('end', () => {
            if (!postData) {
                resolve({})
                return
            }
            resolve(JSON.parse(postData))
        })
    })
}

注意,为了在路由中,都能使用到post数据,需要将使用路由写到Promise的then方法里面

getPostData(req).then(postData => {

    req.body = postData
    // 处理博客路由
    const articleData = articleRouter(req, res)
    if(articleData){
        res.end(JSON.stringify(articleData))
    }
})

post方式获取数据封装好了以后,我们就可以方便的获取到数据了,接下来,我们开始完成新建文章的路由, api地址:http://localhost:3000/api/article/add

routes/article.js

if(method === "POST" && req.path === "/api/article/add"){
    const data = addArticle(req.body)
    return new SuccessModel(data)
}

注意,这里使用的addArticle是从controllers/article.js里面引入的

const {getDetail, getList, addArticle} = require("../controllers/article")

controllers/article.js里面需要新增一个addArticle方法,并且导出

const addArticle = (data = {}) => {
    // 这里需要连接数据库操作,操作完成后,返回新增这篇文章的id
    return {
        id: 1
    }
}

接下来,我们完成文章更新的功能

routes/article.js

if(method === "POST" && req.path === "/api/article/update"){
    const result = updateArticle(req.query.id, req.body)
    if(result){
        return new SuccessModel("更新文章成功")
    }else{
        return new ErrorModel("更新文章失败")
    }
}

controllers/article.js

const updateArticle = (id, data = {}) => {
    // 返回true 表示更新成功
    return true
}

# 1.5.删除文章和登录

删除文章需要获取到被删除文章到id,然后通知数据库执行删除即可,api地址: http://localhost:3000/api/article/delete?id=11

routes/article.js

// 删除文章
if(method === "POST" && req.path === "/api/article/delete"){
    const result = deleteArticle(req.query.id)
    if(result){
        return new SuccessModel("删除文章成功")
    }else{
        return new ErrorModel("删除文章失败")
    }
}

注意,需要引入deleteArticle方法

const {getDetail, getList, addArticle, updateArticle, deleteArticle} = require("../controllers/article")

controllers/article.js, 注意需要导出deleteArticle.js

const deleteArticle = (id) => {
    return true
}

接下来,我们实现登录的路由接口

routes/user.js

const { SuccessModel, ErrorModel} = require('../models/reesultModel')
const { checkuser }  = require('../controllers/user')

const userRouter = (req, res) => {
    const method = req.method

    // 登录接口
    if(method === "POST" && req.path === "/api/user/login"){
        const {username, password} = req.body
        const result = checkuser(username, password)
        if(result){
            return new SuccessModel("登录成功")
        }else{
            return new ErrorModel("登录失败")
        }
    }
}

module.exports = userRouter

注意,需要引入controllers/user.js ,代码如下:


const checkuser = (username, password) => {
    if(username === "admin" && password === "123456"){
        return true
    }
    return false
}

module.exports = {
    checkuser
}

在app.js中需要使用user相关路由


    getPostData(req).then(postData => {

        req.body = postData
        // 处理博客路由
        const articleData = articleRouter(req, res)
        if(articleData){
            res.end(JSON.stringify(articleData))
        }
        // 处理用户路由
        const userData = userRouter(req, res)
        if(userData){
            res.end(JSON.stringify(userData))
        }
    })