一个 Markdown 博客(Node.js,Express 和 MongoDB)

简介 #

简单

设置服务器 #

  1. 首先,我们将通过以下方式创建我们的package.json文件,使用默认值

               npm init -y
    
    

npm: 这是Node Package Manager,是用于管理Node.js包和依赖项的命令行工具。

init: 这是一个用于初始化新的Node.js项目的命令,通过创建一个package.json文件,其中包含有关项目的信息,如名称、版本、描述、依赖项等。

-y: 这是一个代表“是”的标志。在与npm init一起使用时,它告诉npm自动接受所有提示的默认值,并初始化package.json文件,无需任何用户输入。


  1. 现在我们将添加express mongoDB和js。
         npm i express mongoose ejs

进入全屏模式退出全屏模式

npm i express mongoose ejs命令用于安装三个Node.js包:Express、Mongoose和EJS。

以下是每个包的功能:

Express: Express是用于Node.js的快速、无固定意见和极简主义的Web框架。它提供了一套强大的功能,用于构建Web应用程序和API,包括路由、中间件支持和模板渲染。

Mongoose: Mongoose是一个面向MongoDB和Node.js的对象数据建模(ODM)库。它提供了一个直观的基于模式的解决方案,用于建模应用程序数据并与MongoDB数据库交互。Mongoose简化了定义模式、验证数据和执行CRUD操作的过程。

EJS: EJS(嵌入式JavaScript)是一种简单的模板语言,让我们可以使用纯JavaScript生成HTML标记。它允许我们直接在HTML模板中嵌入JavaScript代码,从而轻松通过动态呈现数据创建动态网页。

  • 现在我们将添加开发依赖和nodemon(它将帮助我们自动刷新网页);

       npm i --save-dev nodemon
    
    

要运行nodemon,我们必须添加

   "devStart": "nodemon server.js"

进入全屏模式退出全屏模式

在我们的package.json文件中。


创建索引路由 #

  app.set('view engine', 'ejs');

进入全屏模式退出全屏模式

这里,app.set是一个用于为我们的Express应用分配设置的方法。'view engine'设置用于指定应用程序的模板引擎。

'EJS'代表嵌入式JavaScript。它是一种简单的模板语言/引擎,让我们可以使用纯JavaScript生成HTML标记。

  1. 现在我们将创建一个views文件夹并创建index.ejs文件,编写一些基本的HTML代码,并通过以下方式从server.js文件访问它,

    res.render('index');


创建文章路由 #

在我们的项目中将有很多路由,如显示、编辑、删除等。我们不会从server.js文件访问它们。因此,我们将创建routes文件夹,在其中创建article.js。

const express = require('express');
const router = express.Router();

module.exports = router;

进入全屏模式退出全屏模式

在server.js中,我们可以轻松访问article.js添加,

const articleRouter = require('./routes/articles');

app.use(articleRouter);

我们想在每个路由文件的前面加上/article。这样我们可以使用

    app.use('/articles', articleRouter);

进入全屏模式退出全屏模式

这将使articles.js文件在搜索localhost:5000/articles时获得 '/'。在articles.js中,我们可以通过以下方式轻松捕获它,

articleRouter是一个Express路由的实例。路由器是一种中间件,允许您以模块化的方式对应用程序的不同部分进行分组和处理路由。这个路由器将处理所有以/articles开头的请求。

router.get('/', (req, res) => {
    res.send('This is from article.js');
})

``` * * *

#### 将文章传递给索引

目前我们只传递索引,但我们希望传递所有文章的值。

```javascript
const articles = [{
        title: '测试文章',
        createdAt: Date.now(),
        description: '测试描述'
    }]
    res.render('index', {articles: articles});

我们稍后将在index.ejs中传递文章值。

  • 现在我们将在index.ejs文件中添加Bootstrap链接。
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">

并添加新文章按钮。

<div class="container">
        <h1 class="mb-4">博客文章</h1> 

        <a href="/articles/new" class="btn btn-success">新文章</a>
    </div>

创建主体 #

对于主体,我们将在index.ejs中添加forEach方法并在server.js上做一些更改。

在index.ejs中我们要添加,

<div class="container">
        <h1 class="mb-4">博客文章</h1> 

        <a href="/articles/new" class="btn btn-success">新文章</a> 

        <% articles.forEach(article => {
            %>
            <div class="card mt-4"> 
                <div class="card-body"> 
                    <h4 class="card-title"> <%= article.title %></h4>
                    <div class="card-subtitle text-muted mb-2">
                        <%= article.createdAt.toLocaleDateString() %> 
                    </div>

                    <div class="card-text mb-2">
                        <%= article.description %>
                    </div>
                </div>
            </div> 
            <%
        }) %>
    </div>

在server.js中我们更新了文章的创建时间值,

createdAt: new Date(),


创建新文章路由 #

一开始我们创建了“新文章”按钮。现在我们要为其创建新路由。

  1. 在article.js中,我们更新了路由get,
router.get('/new', (req, res) => {
    res.render('articles/new');
})
  1. 现在我们在views中创建一个新文件夹,命名为articles,并在其中创建一个名为new.ejs的新文件,将index.ejs文件移动到articles文件夹中。

  2. 我们在server.js中更新路径,

res.render('articles/index', {articles: articles});
  1. 在new.ejs中我们添加,
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">

    <title>博客</title>
</head>
<body>
    <div class="container">
        <h1 class="mb-4">新文章</h1>

        <form action="/articles" method="POST">

        </form>
    </div>
</body>
</html>

但这个新页面还没有完成,我们还需要做更多。我们需要创建一个示例格式,为我们提供与“新文章”输入和“编辑文章”相同界面的样式。因此,我们将在文件夹views->articles->_form_fields.ejs中创建一个名为_form_fields.ejs的新文件。 <%- 这个符号帮助我们渲染实际的HTML,而不是文本。

现在我们将把这部分添加到我们的 new.ejs 文件,

 <%- include('_form_fields') %>

通过这种方式,我们可以访问 _form_fields.ejs,无论我们在表单文件中写了什么,都会显示在我们的新文章页面上。

现在我们写一些示例,
图片描述

输出为,
图片描述

好的,现在我们要修改我们的 form_fields 文件,

  • 现在我们编辑后的 _form_fields.ejs 文件如下,
<!-- 这是标题-->
<div class="form-group">
    <label for="title">标题</label>
    <input required type="text" name="title" id="title" class="form-control"> <!-- form-control 为我们提供了漂亮的表单格式,required 为我们提供了必须填写表单的约束-->
</div>

<!-- 这是描述-->
<div class="form-group">
    <label for="description">描述</label>
    <textarea name="description" id="description" class="form-control"></textarea><!-- form-control 为我们提供了漂亮的表单格式,required 为我们提供了必须填写表单的约束-->
</div>

<!-- 这是markdown-->
<div class="form-group">
    <label for="markdown">Markdown</label>
    <textarea name="markdown" id="markdown" class="form-control"></textarea><!-- form-control 为我们提供了漂亮的表单格式,required 为我们提供了必须填写表单的约束-->
</div>

<!-- 取消按钮将取消编辑并带我们回到主页-->

<a href="/" class="btn btn-secondary">取消</a>

<!-- 提交按钮-->
<button type="submit" class="btn btn-primary">保存</button>

现在这个取消按钮将带我们回到主页,保存按钮将把数据发布到我们的服务器,但是我们必须将数据存储到我们的数据库中。因此,我们需要将我们的数据库连接到服务器。


连接数据库 #

  1. 首先我们要打开我们的 server.js 文件。并添加这些行,
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/blog');

就是这样。
图片描述

根据您的mongodb版本不同,您可能会遇到不同的 过时警告,比如我没有遇到任何过时警告,但如果您遇到过时警告,您必须添加到,

mongoose.connect('mongodb://localhost/blog', { useNewUrlPerser: true}); .

图片描述

现在我们将创建一个 mongoose模型 来存储文章。


Mongoose 模型 #

图片描述

在 article.js 中,我们将添加以下内容,

const mongoose = require('mongoose');

const articleSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    description: {
        type: String
    },
    markdown:{
        type: String,
        required: true
    },
    craetedAt: {
        type: Date,
        default: Date.now
    }
})

// 导出模式
module.exports = mongoose.model('Article', articleSchema)

图片描述

好的,现在我们要从
routes->articles.js 访问它,

  1. const Article = require('./../models/article')
    , 这将有路径到 models article.js

  2. 现在我们要创建一个对象,它将接收用户的发布数据,

const article = new Article({

    })

图片描述

但是我们必须告诉 express 如何访问这个对象。因此,我们需要进入 server.js 并添加,

app.use(express.urlencoded({extended: false}))

图片描述

在将此添加到 server.js 后,我们可以通过 req.body.title、req.body.description、req.body.markdown 轻松访问 新文章 的标题、描述和 markdown。

const express = require('express');

// 访问模型 article
const Article = require('./../models/article')
const router = express.Router();

router.get('/new', (req, res) => {
    res.render('articles/new');
})

router.get('/:id', (req, res) ```
router.get('/:id', async (req, res) => {
    const article = await Article.findById(req.params.id)
    if(article == null) res.redirect('/')
    res.render('articles/show', {article: article })
})

在articles.js中的此部分中,我们首先要编辑这个部分。 在vies->articles-show.ejs中创建一个名为show.ejs的新文件。

在修复之后,

访问所有文章 #

所以,我们将访问新创建的数据。

我们必须转到server.js。我们需要删除此部分,并且我们需要从server.js访问文章模型。它在models->article.js中。

const Article = require('./models/article')

const articles = await Article.find().sort({
    createdAt: 'desc'
})

添加显示按钮 #

我们将在主页上创建一个显示按钮。

首先,我们必须转到我们的index.ejs文件。并添加这个,

现在我们将使用slug,slug是article_id的替代品。

SLUG #

所以我们将导入slug库

npm i marked slugify

marked: 这是一个允许我们在Node.js中解析Markdown的软件包。它将Markdown语法转换为HTML。

slugify: 此软件包用于将字符串转换为slug,slug是字符串的URL友好版本。它删除特殊字符,将空格转换为破折号,并将字符串转换为小写。

然后我们转到models->article.js。并添加,

const marked = require('marked')
const slugify = require('slugify') 

另外,

articleSchema.pre('validate', function(next) {
    if(this.title) {
        this.slug = slugify(this.title, { lower: true, strict: true })
    }

    next()
})

为了使用slug,我们需要做一些更改。首先在routes-articles.js中,

  • router.get('/:slug', async (req, res) => 添加slug取代id。
  • const article = await Article.find({ slug: req.params.id }) 将findById更改为添加slug部分。
  • res.redirect(/articles/${article.slug}) 删除id并添加slug。

但是如果您刷新站点,会出现一些错误。

为了解决这个问题,我们需要转到views->articles->index.ejs并更改,

  • <a href="articles/<%= article.slug %>" class="btn btn-primary">Read More</a> 将article.id更改为article.slug。

问题仍未解决。所以我们需要转到routes->articles.js并更改,

  • const article = await Article.findOne({ slug: req.params.id }) 我们将find更改为findOne。

一些类型被修复 #

在routes->articles.js中:const article = await Article.findOne({ slug: req.params.slug }) 在models->article.js中:this.slug = slugify(this.title, { lower: true,

修复完所有这些问题后,可能会显示弃用的内容,就像一开始显示的那样。请像以前在数据库连接中显示的那样添加这些弃用内容。所有这些都是由于mongodb版本引起的。

创建DELETE路由 #

所以我们可以看到,只有新闻文章添加了slug,而旧文章则没有显示任何内容。

不同的文章显示不同。

为了解决这个问题,我们将删除旧文章。为此,我们必须创建一个DELETE路由。

在routes->articles.js中首先要做的是,我们需要DELETE方法,因此,我们必须导入它。因此,在终端中我们使用:

npm i method-override

method-override包允许我们在客户端不支持的地方使用诸如PUT或DELETE之类的HTTP动词。这在处理Express.js中的表单数据时可能很有用,因为HTML表单只支持GET和POST请求。

创建DELETE表单 #

首先我们要去我们的index.ejs文件,然后,

<form action="/articles/<%= article.id %>?_method=DELETE" method="POST" class="d-inline"><button type="submit" class="btn btn-danger">删除</button></form>

删除部分已经完成,但在routes->articles.js中有一些拼写错误。请更正redirect的拼写。

DOM净化 #

我们需要导入一些包,

npm i dompurify jsdom

我们去models->article.js,并在顶部添加这些内容,

const createDomPurify = require('dompurify')
const { JSDOM } = require('jsdom')
const dompurify = createDomPurify(new JSDOM().window)

在图片中我们写了createdomPurify,这个拼写错误。应该是createDomPurify。

然后我们还添加这些内容,

sanitizedHtml: {
    type: String,
    required: true
}

然后添加这些内容,

if(this.markdown) {
    this.sanitizedHtml = dompurify.sanitized(marked(this.markdwon))
}

接下来我们要去show.ejs并更新,

<%- article.sanitizedHtml %>

编辑博客 #

所以我们在routes->articles.js

我们添加路由,

router.get('/edit/:id', async (req, res) => {
    const article = await Article.findById(req.params.id)
    res.render('articles/edit', {article: article})
})
``` 我们在views->articles中添加了一个新文件**edit.ejs**。然后添加,

<title>Blog</title>

Edit Article

    <form action="/articles/<%= article.id %>?_method=PUT" method="POST">
        <%- include('_form_fields') %>
    </form>
</div>

然后我们添加了put方法,因为put和post方法相同,所以我们将使用中间件。

function saveArticleAndRedirect(path) { return async (req, res) => { let article = req.article article.title = req.body.title article.description = req.body.description article.markdown = req.body.markdown try{ article = await article.save() res.redirect(/articles/${article.slug}) }catch(e){ res.render(articles/${path}, {article: article }) } } }


1.我们还在这里更新了我们的**POST**方法,

router.post('/', async(req, res, next) =>{ req.article = new Article() next()
}, saveArticleAndRedirect('new'))


所以我们的post现在完成了,**PUT**。

但是在添加该方法后,它停止工作。

我们稍后会继续解决这个问题。