Notes on node.js+mongodb+express to realize simple version of blog management system

Keywords: Session Mongoose Database JSON

I. Case initialization

  1. Related folders
    • public: static resources
    • model: database related files
    • Route: route
    • views: template files
  2. Build project description file
    npm init -y
  3. Download third party module
    • npm i express mongoose art-template express-art-template
  4. Create server
const express = require('express')
//Turn on the server
const app = express()
//Listen for and respond to request events
app.on('request',(req,res)=>{
    res.send('ok')
})

app.listen(3000)
console.log('Server is running ')
  • Building modular routing
//Routing and mounting
app.use('/home',home)
app.use('/admin',admin)
  • Build blog management page
    • Configuration template
    • Open static resources
    • Write the chain file as an absolute path: because the chain resource in the template file is parsed by the browser, its relative path is relative to the browser's request path
//Configuration template
app.engine('art',require('express-art-template'))//Use express art template to resolve template files with suffix of Art
app.set('views',path.join(__dirname,'view'))//Configure template root
app.set('view engine','art')//Configure template file default suffix
//Open static resources
app.use(express.static(path.join(__dirname,'public')))
  • Template optimization
    • The extracted common part is introduced as a sub template by {{include 'relative path'}}.
    • The HTMl skeleton template of the extracted file is inherited by {{extend 'relative path'}}

II. Project functions

Login function

  • Create user collection, initialize users
    • The email address is unique. It is unique:true when creating the rule (because you want to log in with a mailbox)
  • Display and submit login form
    • When testing, you can prevent the default submission behavior of the form, and use return false to prevent the program from executing downward.
  • Dual authentication of client and server
    • Because the client can disable JavaScript, the server also needs to verify
    • In jquery, there is a method of form. serializeArray(), which can get the input of users in the form. The return value is array object, the number of objects = the number of form controls.
    • Because there may be multiple forms for validation, you can encapsulate a method
    • The form can only be submitted after the client has successfully verified non empty
    • The server side uses the third-party module body parser (node.js parsing Middleware) to obtain the post request parameters, and performs non empty verification. Only when the mailbox and password are non empty, can they be carried out downward.
    • Query database and other operations must use await to get results synchronously, and the function should be changed to asynchronous function.
    • bcrypt (depending on the environment: Python 2. X node gyp window build tools) is used to encrypt the password entering the database.
 	//Client non empty validation
 	$('#loginForm').on('submit',function(){
            let result = serializeToJson($(this))
            if(result.email.trim().length == 0){
                alert('Please enter email address')
                return false
            }
            if(result.password.trim().length == 0){
                alert('Please input a password')
                return false
            }
        })
    //Get post request parameters with bodyparser
    const bodyParser = require('body-parser')
	app.use(bodyParser.urlencoded({extended:false}))
	//Server side non empty validation
	if(email.trim().length == 0 || password.trim().length == 0)return 		res.status(400).render('admin/error',{msg:'Wrong email address or password'})
	//Cipher encryption
	const bcrypt = require('bcrypt')
	const salt =await bcrypt.genSalt(10)
    const password =await bcrypt.hash(req.body.password,salt)
    //The first password is clear text, and the second one is encrypted.
    let isValid = await bcrypt.compare(password,user.password)
  • Login successfully saved login state (cookie and session)
    • cookie: a piece of storage space opened up by the browser in the hard disk of the computer, mainly for the server to store data
      • Data in cookie s are distinguished in the form of domain names
      • The data in the cookie is due to an expired event, which will be automatically deleted by the browser.
      • The first time the server responds to the cookie, the client exists in the client, and then it will be automatically sent to the server along with the request.
    • Session: in fact, it is an object, which is stored in the server-side memory. In the session object, multiple pieces of data can also be stored. Each piece of data has a sessionId as the unique identification (with the help of express session).
const session = require('express-session')
//Call session method
app.use(session({
    secret:'secret key',
    saveUninitialized:false,//No need to save cookie s if you are not logged in
    cookie:{
        maxAge:24*60*60*1000
    }
}))
  • Redirect pages and show internal pages
    • res.redirect('address')
    • Req. QPP. Places. Userinfo = user / / after logging in, the user information can be exposed, so that other places can be used, but it should be cleared after logging out.
  • Implement user logout (client and server disconnect)
req.session.destroy(function(){//Delete session
        res.clearCookie('connect.sid')//delete cookie
        res.app.locals.userInfo = ''
        res.redirect('/admin/login')
    })
  • Login blocking: you cannot access the background internal interface through the web page url without login
    • Background is not logged in and cannot be accessed
    • Log in as an ordinary user and jump to the homepage directly
//Encapsulate a login interception middleware separately
const guard = (req,res,next) => {
    if(req.url != '/login' && !req.session.username){
        res.redirect('/admin/login')
    }else{
        if(req.session.role == 'normal'){
            return res.redirect('/home/index')
        }
        next()
    }
}

module.exports = guard
  • Code optimization
    • Route separation: one route address and a separate folder are used to put different route processing functions. Each route processing function is written as a separate js file.

1. Background - user management

New user (background)

  • Add corresponding route processing, render new user page and accept user information (post request parameter)
  • Validate new user information (rule description language and verifier of joi - js object - third party module)
const Joi = require('joi')
const validateUser = (user)=>{
    const schema={
        name:Joi.string().max(12).min(1).required().error(new Error('Illegal user name')),
        email:Joi.string().email().required().error(new Error('Illegal user mailbox')),
        password:Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).required().error(new Error('Illegal password')),
        role:Joi.string().valid('normal','admin').error(new Error('Illegal role')),
        state:Joi.number().valid(0,1).error(new Error('Illegal status'))
    }
    return Joi.validate(user,schema)
}
  • Verify new user mailbox is available (query database)
  • Password encryption processing
  • Add error handling Middleware
	try{
         await validateUser(req.body)//First, judge whether the information submitted by the user is legal
    }catch(ex){
        return next(JSON.stringify({path:'/admin/user-edit',msg:ex.message}))//After catching the exception, the error information is passed to the error handling middleware, and the processing right is handed to the middleware.
    }
    const user = await User.findOne({email:req.body.email})//Query the email address uploaded by the user in the database
    if(user){
    	//It is found that the description email address is duplicate, and the error handling middleware is called.
        return next(JSON.stringify({path:'/admin/user-edit',msg:'Email address occupied'})) 
    }else{
    	//The mailbox is available, the password of new users is encrypted, stored in the database, and redirected to the user list interface.
        const salt =await bcrypt.genSalt(10)
        const password =await bcrypt.hash(req.body.password,salt)
        req.body.password = password
        await User.create(req.body)
        res.redirect('/admin/user')
    }

User list rendering

Use template syntax to pass in data and render it in the template

Data paging

	let page = req.query.page || 1//To carry parameters in the address bar when switching pages
    let pageSize = 8//Customize the number of data entries per page
    let count =await User.countDocuments({})//Total number of data in query data set
    let total =Math.ceil(count /pageSize)//Page number, rounding up must
    let start = (page-1)*pageSize//Calculate the number of start data bars corresponding to each page
    let users = await User.find({}) .limit(pageSize).skip(start)//Query database, limit query number, skip skip number
    res.render('admin/user',{//Pass in the data required by the template file
        users,
        page,
        total
    })

User information modification and deletion

  • The user's id needs to be passed in by get method during modification.
  • It is not allowed to change the password, but it is used to verify whether the user information can be modified.
  • It shares an editing user interface with new users, so short circuit operation is used to process user data to be rendered.
  • Optimize the error handling middleware (you can't write the error information to death, but use the method of judgment to use for the information except path... In to display in the error message)
//Error handling Middleware
app.use((err,req,res,next) => {
    const result = JSON.parse(err)
    let params = []
    for(let attr in result){
        if(attr != 'path'){
            params.push(attr + '=' +result[attr])
        }
    }
    res.redirect(`${result.path}?${params.join('&')}`)
})
  • To delete a user, you also need an id, but there is no relevant form on the page, so you need to add a hidden field (that is, input tag, type = "hidden")

2. Background - article management

Article list routing settings

  • Implement switching article management (write corresponding route in a tag)
  • The module selected for implementation has a corresponding selection style (attach an ID in req.app.locals.currentLink). When switching to the corresponding page, a request will be sent. You can judge this data to decide which module's title to render.

Create a collection of articles

  • The author's data in the article collection should be associated with the User (type:mongoose.Schema.Types.ObjectId,ref: 'User')

Add article

  • Because the article can upload the cover page, it involves uploading files in the form (upload with binary data, and add enctype = "multipart / form data" in the form tag).
  • Use the third-party module, formatible: parse the form, support get request parameters, post request parameters and file upload
const formidable = require('formidable')
const path = require('path')
const {Article} = require('../../model/article')//Introducing a collection of articles

module.exports= (req,res) => {
    const form = new formidable.IncomingForm()//Create a form resolution object
    form.uploadDir = path.join(__dirname,'../','../','public','uploads')  //Set file upload path
    form.keepExtensions=true//Keep file suffix
    form.parse(req, async (err, fields, files) =>{//err - error information fields - the common request parameter object files is the information object related to the uploaded file, which needs to be processed as follows
            await Article.create({
                title:fields.title,
                author:fields.author,
                releaseTime:fields.releaseTime,
                cover:files.cover.path.split('public')[1],
                content:fields.content
            })
            res.redirect('/admin/article')
      });
       
}
  • Real time preview of the pictures added in the article (using the native method FileReader)
//Preview pictures in real time
        var file = document.querySelector('#file')
        var preview = document.querySelector('.preview')
        file.onchange=function(){
            var reader = new FileReader()
            reader.readAsDataURL(this.files[0])
            reader.onload=function(){
                // console.log(reader.result)
                preview.src=reader.result
                
            }
        }
  • Using editor plug-ins in text fields
		let editor;
        ClassicEditor
                .create( document.querySelector('#editor'))
                .then(newEditor => {
                    editor = newEditor;
                    editor.setData('{{@article && article.content}}')//Display data in the database in the editor when modifying
                })
                .catch( error => {
                    console.error( error );
                });

Article list page data rendering

  • Multi set union query: set. find().populate('field of union query ')
  • Dateformat processing date format: after installation, you can use template.defaults.imports.dateFormat = dateFormat globally by importing objects as template import variables.
  • Paging third-party module (mongoose sex page)
const pagination = require('mongoose-sex-page')
module.exports=async (req,res) => {
    let page = req.query.page || 1//Declare the variable page as get parameter or 1
    req.app.locals.currentLink = 'article'//Distinguish whether the article module is selected or the user management module is selected
    let articles = await pagination(Article).find().populate('author').page(page).size(8).display(3).exec()//Paginate 8 pieces of data on each page, display the three page pagination button, and click the next page to display the data one by one.
    // res.send(articles)
    res.render('admin/article',{
        articles
    })
}

Article modification

  • The image cannot be reversed after being converted to binary data, but the path address after transcoding can be rendered at the image file address, and the image before real-time preview can still be displayed.
<div class="form-group">
   <label for="exampleInputFile">Article cover</label>
   <input name="cover" type="file" mutiple id="file" >
   <div class="thumbnail-waper">
       <img class="img-thumbnail preview" src="{{article && article.cover}}" >
   </div>
</div>
  • Content cannot appear in the editor by default, calling the setData() method of the ClassicEditor plug-in. The documentation for the plug-in is here
  • Because the data has been set to binary form data, you can also use formatible to process the modified post data.
  • Problems not handled: determine the upload path of the picture by judging whether the picture has been changed. If the change is made, the original files.cover.path.split('public')[1] will be used. The unmodified address does handle the relative address from \ uploads once, but the problem has not been solved yet. (the current method is to select a new picture every time you modify the picture, or you can select the same picture before and keep the original picture.)

Article deletion

  • Using hidden domain and passing id to implement

3. Front desk - front page and details page

Front desk basic processing

  • Processing static resource paths
  • Extract common sub template and HTML skeleton

Data rendering of blog homepage list

  • Paging
  • Implementation of template data rendering
  • Select the current page number
  • The content data of the article contains html tags. You can directly call the jquery method to process {@ $value. Content. Replace (/ [^ >] + > / g, '). Substr (0,80) +'... '}}}} in {}}} and avoid spaces in the content turning into {@}} original output.

Detail page article comment function

  • Create a collection of article comments
  • Judge whether users log in or not
    • Text field for comments only when logged in
    • Through login interception, if the login role is super administrator, you can enter the background management page, if the login role is ordinary user, you can jump to the home page (there is corresponding code in front).
{{if userInfo}}
	<form class="comment-form" action="/home/articleAdd?aid={{@article._id}}&uid={{@userInfo._id}}" method="post">
		<textarea class="comment" name="comment"></textarea>
		<div class="items">
			<input type="submit" value="Submission">
		</div>
	</form>
{{else}}
	<div><h2>Log in before commenting on the article</h2></div>
{{/if}}
  • Comment addition, comment display (add according to relevant data of the collection)
const mongoose = require('mongoose')

const commentSchema = new mongoose.Schema({
    aid:{
        type:mongoose.Schema.Types.ObjectId,
        ref:'Article',
        required:true
    },
    uid:{
        type:mongoose.Schema.Types.ObjectId,
        ref:'User',
        required:true
    },
    publishTime:{
        type:Date,
        default:Date.now
    },
    comment:{
        type:String,
        required:true
    }
})

const Comment = new mongoose.model('Comment',commentSchema)
module.exports={
    Comment
}

Posted by richo89 on Sat, 26 Oct 2019 00:26:17 -0700