Initialize the project structure
Create the directory first and initialize it (if you want to put it on GitHub, create a repository on GitHub and clone it) : You can enter it all the way to initialization, if you don’t mind.
mkdir Instagrammy && cd Instagrammy
npm init
Copy the code
Install express
npm i express
Copy the code
The resulting package.json file is as follows:
{" name ":" instagrammy ", "version" : "1.0.0", "description" : ""," main ":" index. Js ", "scripts" : {" test ", "echo \" Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^ 4.17.1}}"Copy the code
Then create the entry file server.js in the project root directory
const express = require('express'); app = express(); app.set('port', process.env.PORT || 3000); app.get('/', function(req, res) { res.send('Hello World! '); }) app.listen(app.get('port'), function() { console.log(`Server is running on http://localhost:${app.get('port')}`) })Copy the code
Run Nodeserver.js and visit http://localhost:3000 in your browser to see Hello World! Returned from the server on the page.
Here we add a startup command to package.json to make it easier to run the project later:
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
Copy the code
Configuring middleware
Express is a very concise Web framework on its own, but with middleware as a design pattern, it can achieve very rich functionality. An Express middleware is essentially a function:
function someMiddleware(req, res, next) {}
Copy the code
The req argument is an express.Request object that encapsulates the user Request. The res argument is an express.Response object that encapsulates the Response to be returned to the user. Next is the function that triggers the next middleware after all the logic has been executed.
The code for adding middleware is quite simple:
app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);
Copy the code
Middleware A, B, and C will execute sequentially on each request (which also means that the order of the middleware is very important). Next we will add the following basic middleware (which is also used by almost all applications) :
- Morgan: Middleware for logging, useful for development debugging and production monitoring;
- BodyParser: Middleware for parsing client requests, including HTML forms and JSON requests;
- MethodOverride: REST request compatibility support for older browsers;
- CookieParser: Used for sending and receiving cookies;
- ErrorHandler: Used to print the call stack in case of an error, only for development purposes;
- Handlebars: A template engine for rendering user interfaces, more on this later.
We install these middleware via NPM:
npm install express-handlebars body-parser cookie-parser morgan method-override errorhandler
Copy the code
Create a server directory in the root directory of the project, and create a configure.js file in the server directory to configure all middleware. The file content is as follows:
const path = require('path'); const exphbs = require('express-handlebars'); const express = require('express'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const morgan = require('morgan'); const methodOverride = require('method-override'); const errorHandler = require('errorhandler'); module.exports = function(app) { app.use(morgan('dev')); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(methodOverride()); app.use(cookieParser('secret-value')); app.use('/public/', express.static(path.join(__dirname, '.. /public'))); if (app.get('env') === 'development') { app.use(errorHandler()) } return app; }Copy the code
Express. static is the static resource middleware of Express. It is used to send static files, such as images and CSS, to clients. Finally, we use the env variable to determine if we are in a development environment, and if so add errorHandler to make it easier to debug the code.
Call the code that just configured the middleware in server.js:
const express = require('express'); const configure = require('./server/configure'); app = express(); app = configure(app); app.set('port', process.env.PORT || 3000); app.get('/', function(req, res) { res.send('Hello World! '); }) app.listen(app.get('port'), function() { console.log(`Server is running on http://localhost:${app.get('port')}`) })Copy the code
Set up routes and controllers
In the previous step we configured the basic middleware, but only the home page (/) was accessible. Now we implement the following routing:
- GET / : Website home page.
- GET /images/image_id: show a single image.
- POST /images: Uploads images.
- POST /images/image_id/like: Like the image.
- POST /images/image_id/comment: Comments on images.
We use the MVC pattern most familiar to the front end to build the following.
Create a home.js file that contains controllers and define the index controller as follows:
module.exports = { index: function(req, res) { res.send('The home:index controller'); }};Copy the code
Each controller is essentially an Express middleware (except that the next function is not required because it is the last middleware). Here we temporarily send a text with res.send to indicate that the controller has been implemented.
Create image.js in the controllers directory.
module.exports = { index: function(req, res) { res.send('The image:index controller ' + req.params.image_id); }, create: function(req, res) { res.send('The image:create POST controller'); }, like: function(req, res) { res.send('The image:like POST controller'); }, comment: function(req, res) { res.send('The image:comment POST controller'); }};Copy the code
Then create the routing module routes.js under the server directory to establish the mapping between URL and controller:
const express = require('express'); const router = express.Router(); const home = require('.. /controllers/home'); const image = require('.. /controllers/image'); module.exports = function(app) { router.get('/', home.index); router.get('/images/:image_id', image.index); router.post('/images', image.create); router.post('/images/:image_id/like', image.like); router.post('/images/:image_id/comment', image.comment); app.use(router); };Copy the code
Here, we use the Router class of Express, which can easily define routes. Besides, the Router itself is also a middleware, which can be configured directly through app.use.
The routing module is then called in the server/configure.js module.
const path = require('path'); const exphbs = require('express-handlebars'); const express = require('express'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const morgan = require('morgan'); const methodOverride = require('method-override'); const errorHandler = require('errorhandler'); const routes = require('./routes'); module.exports = function(app) { app.use(morgan('dev')); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(methodOverride()); app.use(cookieParser('secret-value')); app.use('/public/', express.static(path.join(__dirname, '.. /public'))); if (app.get('env') === 'development') { app.use(errorHandler()); } routes(app); return app; };Copy the code
Finally, we removed the original home page route from server.js.
const express = require('express');
const configure = require('./server/configure');
app = express();
app = configure(app);
app.set('port', process.env.PORT || 3000);
app.listen(app.get('port'), function() {
console.log(`Server is running on http://localhost:${app.get('port')}`);
});
Copy the code
Now we run the server
npm start
Copy the code
Open The browser and visit localhost:3000 to see The Home: Index Controller displayed on The page. Access localhost: 3000 / images/test123, is The image: The index controller test123.
Configure the Handlebars template engine
In this step, we will realize the interface display, and the effect of the home page is shown as follows:
The image details page looks like this:
Although the separation of the front and back ends is the trend these days, there is also a place for using template engines to render pages on the server side, especially for developing simple applications quickly. Handlebars and Pug are the best of the template engines. Since Handlebars are almost identical to a normal HTML document and easy to get started with, we’ll use Handlebars and Bootstrap styles for this tutorial.
Compared with normal HTML documents, templates provide data access. Handlebars, for example, can fill in any data between double curly braces {{}}, and when the server renders the page, it just needs to pass in the data to render the corresponding content. In addition, Handlebars supports advanced features like conditional syntax, looping syntax, and template nesting, which are described in more detail below.
We create a views directory to hold all the template code. The structure of the views directory is as follows:
Handlebars and index.handlebars are layouts/main.handlebars, or layouts/main.handlebars. Such as comments, data and so on.
Handlebars /main.handlebars
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial - scale = 1.0 "> < title > Instagrammy < / title > < link rel =" stylesheet" Href = "http://netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" > < / head > < body > < div class = "page - the header" > <div class="container"> <div class="col-md-6"> <h1><a href="/">Instagrammy</a></h1> </div> </div> </div> <div class="container"> <div class="row"> <div class="col-sm-8">{{{body}}}</div> </div> </div> </body> </html>Copy the code
Main. handlebars itself is a complete HTML document, including the head and body sections. In the head part, we defined some metadata of the website and added the CDN link of Bootstrap. In the body section, there are two containers: the web site header and the custom content for each page (that is, {{{body}}}).
Index. Handlebars:
<h1>Index Page</h1>
Copy the code
Picture details page content:
<h1>Image Page</h1>
Copy the code
The contents of index.handlebars and image.handlebars will replace the {{{body}}} sections in the layout template. When a user visits a page, page content = layout template + page template.
After the template is written, we modify the corresponding code of controller/home.js to render the template using the res.render function:
module.exports = { index: function(req, res) { res.render('index'); }};Copy the code
The Render function takes a string argument, the name of the page template. For example, the name of index.handlebars is index.
Similarly, we modify the code for the image controllers/image.js:
module.exports = { index: function(req, res) { res.render('image'); }, create: function(req, res) { res.send('The image:create POST controller'); }, like: function(req, res) { res.send('The image:like POST controller'); }, comment: function(req, res) { res.send('The image:comment POST controller'); }};Copy the code
Configure handlebars middleware in server/configure.js:
const path = require('path'); const exphbs = require('express-handlebars'); const express = require('express'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const morgan = require('morgan'); const methodOverride = require('method-override'); const errorHandler = require('errorhandler'); const routes = require('./routes'); module.exports = function(app) { app.engine('handlebars', exphbs()); app.set('view engine', 'handlebars'); app.use(morgan('dev')); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(methodOverride()); app.use(cookieParser('secret-value')); app.use('/public/', express.static(path.join(__dirname, '.. /public'))); if (app.get('env') === 'development') { app.use(errorHandler()); } routes(app); return app; };Copy the code
Here we can finish the this step, NPM start to run the server now, visit the home page localhost: 3000 and image details page localhost: 3000 / images/test can see the corresponding page, although there is no data.
Improve the page code
Start by adding a form to upload images and a container to display the latest images in index.handlebars
{{! -- views/index.handlebars --}} <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title"> </h3> </div> <form action="/images" method="post" encType ="multipart/form-data"> <div class="panel-body Horizontal "> <div class="form-group col-MD-12 "> <label for="file" class=" col-SM-2 control-label"> View :</label> <div class="col-md-10"> <input type="file" class="form-control" name="file" id="file"> </div> </div> <div class="form-group Col-md-12 "> <label for="title" class=" col-MD-2 control-label" </label> <div class=" col-MD-10 "> <input type="text" class="form-control" name="title"> </div> </div> <div class="form-group col-md-12"> <label for="description" Class =" col-MD-2 control-label"> </label> <div class=" col-MD-10 "> <textarea name="description" rows="2" class="form-control"></textarea> </div> </div> <div class="form-group col-md-12"> <div class="col-md-12 text-right"> <button type="submit" id="login-btn" class=" BTN btn-success"> < I class="fa fa-cloud-upload"> </ I > </button> </div> </div> </div> </form> </div> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> Latest image </h3> </div> <div class="panel-body"> {{#each images}} <div class="col-md-4 text-center" style="padding-bottom: 1em;" > <a href="/images/{{uniqueId}}"> <img src="/public/upload/{{filename}}" alt="{{title}}" style="width: 175px; height: 175px;" class="img-thumbnail"> </a> </div> {{/each}} </div> </div>Copy the code
We use the loop syntax provided by Handlebars (lines 46 to 53) to show the latest images. The data object images passed into the template are traversed, and all attributes of a single image, such as uniqueId, and so on, can be accessed in each loop.
I then refined the contents of image.Handlebars, including details to display images, a form to post comments, and a container to display all comments.
{{! -- views/image.handlebars --}} <div class="panel panel-primary"> <div class="panel-heading"> <h2 class="panel-title">{{image.title}}</h2> </div> <div class="panel-body"> <p>{{image.description}}</p> <div class="col-md-12 text-center"> <img src="/public/upload/{{image.filename}}" alt="{{image.title}}" class="thumbnail"> </div> </div> <div class="panel-footer"> <div class="row"> <div class="col-md-8"> <button class="btn btn-success" Id ="btn-like" data-id="{{image.uniqueId}}"> < I class="fa fa-heart"> </ I > </button> <strong class="likes-count">{{image.likes}}</strong> - <i class="fa fa-eye"></i> <strong>{{image.views}}</strong> - Published in: <em class="text-muted">{{image.timestamp}}</em> </div> <div class="col-md-4 text-right"> <button class="btn btn-danger" id="btn-delete" data-id="{{image.uniqueId}}"> <i class="fa fa-times"></i> </button> </div> </div> </div> </div> <div class="panel panel-default"> <div class="panel-heading"> <div class="row"> <div class="col-md-8"> <strong Class ="panel-title"> </strong> </div> <div class=" col-mD-4 text-right"> <button class=" BTN btn-default bTN-sm" Id ="btn-comment" data-id="{{image.uniqueId}}"> < I class="fa fa-comments-o"> </i> </button> </div> </div> </div> <div class="panel-body"> <blockquote id="post-comment"> <div class="row"> <form action="/images/{{image.uniqueId}}/comment" method="post"> <div class="form-group col-sm-12"> <label for="name" </label> <div class="col-sm-10"><input type="text" class="form-control" name="name"></div> </div> <div class="form-group col-sm-12"> <label for="email" class="col-sm-2 control-label">Email:</label> <div class="col-sm-10"><input type="text" class="form-control" name="email"></div> </div> <div class="form-group col-sm-12"> <label for="comment" class="col-sm-2 control-label" class="col-sm-10"> <textarea name="comment" class="form-control" rows="2"></textarea> </div> </div> <div class="form-group col-sm-12"> <div class="col-sm-12 text-right"> <button class="btn btn-success" id="comment-btn" Type = "button" > < I class = "fa fa - the comment" > < / I > published < / button > < / div > < / div > < / form > < / div > < / blockquote > < ul class="media-list"> {{#each comments}} <li class="media"> <a href="#" class="pull-left"> <img src="http://www.gravatar.com/avatar/{{gravatar}}?d=monsterid&s=45" class="media-object img-circle"> </a> <div class="media-body"> {{comment}} <br/> <strong class="media-heading">{{name}}</strong> <small class="text-muted">{{timestamp}}</small> </div> </li> {{/each}} </ul> </div> </div>Copy the code
In the code that displays all the comments, we also use handlebars loop syntax, which is very handy.
Then, we’ll implement the statistics, most popular images, and latest comments components in the right sidebar of the site, respectively.
The first is the statistics component template:
{{! -- views/partials/stats.handlebars --}} <div class="panel panel-default"> <div class="panel-heading"> <h3 Class ="panel-title"> statistics </h3> </div> <div class="panel-body"> <div class="row"> <div class=" col-MD-4" </div> <div class=" col-mD-8 text-right">{{sidebar. Stats. Images}}</div> </div> <div class="row"> <div Class =" col-mD-4 text-left"> comments :</div> <div class=" col-mD-8 text-right">{{sidebar.stats.comments}}</div> </div Class ="row"> <div class="col-md-4 text-left"> </div> <div class="col-md-8 text-right">{{sidebar. Stats. Views}}</div> </div> <div class="row"> <div class="col-md-4 text-left"> Like :</div> <div class=" col-mD-8 text-right">{{sidebar.stats.likes}}</div> </div> </div> </div>Copy the code
Handlebars:
{{! -- views/partials/popular.handlebars --}} <div class="panel panel-default"> <div class="panel-heading"> <h3 <div class="panel-title"> </h3> </div> <div class="panel-body"> {{#each sidebar text-center" style="padding-bottom: .5em;" > <a href="/images/{{uniqueId}}"> <img src="/public/upload/{{filename}}" style="width: 75px; height: 75px;" class="img-thumbnail"> </a> </div> {{/each}} </div> </div>Copy the code
Latest comments component (Comments.handlebars) :
{{! -- views/partials/comments.handlebars --}} <div class="panel panel-default"> <div class="panel-heading"> <h3 Class ="panel-title"> </h3> </div> <div class="panel-body"> <ul class="media-list"> {{#each sidebar.comments}} <li class="media"> <a href="/images/{{image.uniqueId}}" class="pull-left"> <img src="/public/upload/{{image.filename}}" class="media-object" height="45" width="45"> </a> <div class="media-body"> {{comment}}<br/> <strong class="media-heading">{{name}}</strong> <small class="text-muted">{{timestamp}}</small> </div> </li> {{/each}} </ul> </div> </div>Copy the code
Finally, we add all component templates in our layouts/main.handlebars layout template with the syntax {{> Component this}}. On top of that, since we used some small ICONS, we added a link to font-awesome.
{{! -- views/layouts/main.handlebars --}} <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Instagrammy</title> <link Href = "http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel = "stylesheet" > < link Href = "http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css" rel = "stylesheet" > < / head > < body > < div class="page-header"> <div class="container"> <div class="col-md-6"> <h1><a href="/">Instagrammy</a></h1> </div> </div> </div> <div class="container"> <div class="row"> <div class="col-sm-8">{{{body}}}</div> <div class="col-sm-4"> {{> stats this}} {{> popular this}} {{> comments this}} </div> </div> </div> </body> </html>Copy the code
Pass the data into the template view
If no data is passed in, the corresponding data portion of the template will be all blank. In this step, we’ll use some dummy data to demonstrate how to pass data from the controller into the template view.
We first construct a viewModel object in the Home controller and pass it in as the second argument in the Render function. You can see that the viewModel object is exactly the same as the data interface in the template.
{{! -- controllers/home.js --}} module.exports = { index: function(req, res) { const viewModel = { images: [ { uniqueId: 1, title: 'sample1.jpg', description: ', filename: 'sample1.jpg', views: 0, likes: 0, timestamp: date.now (),}, {uniqueId: 2, title: 'sample image 2', description: ', filename: 'sample2.jpg', views: 0, likes: 0, timestamp: date.now (),}, {uniqueId: 3, title: 'sample3.jpg', description: ', filename: 'sample3.jpg', views: 0, likes: 0, timestamp: date.now (),},],}; res.render('index', viewModel); }};Copy the code
{{! -- controllers/image.js --}} module.exports = { index: function(req, res) { const viewModel = { image: { uniqueId: 1, title: 'sample image 1', description: 'This is a test image ', filename: 'sample1.jpg', views: 0, likes: 0, timestamp: Date.now(), }, comments: [ { image_id: 1, email: '[email protected]', name: 'Test Tester', comment: 'Test 1', timestamp: Date.now(), }, { image_id: 1, email: '[email protected]', name: 'Test Tester', comment: 'Test 2', timestamp: Date.now(), }, ], }; res.render('image', viewModel); }, create: function(req, res) { res.send('The image:create POST controller'); }, like: function(req, res) { res.send('The image:like POST controller'); }, comment: function(req, res) { res.send('The image:comment POST controller'); }};Copy the code
When passing in data, we can customize some helper functions to use in the template. For example, timestamp, date.now () returns a string of numbers, which is obviously not user friendly, so we need to convert it to a user-friendly time, such as “seconds ago” or “two hours ago”. Here we use JavaScript’s most popular handling time library moment.js and install it via NPM:
npm i moment
Copy the code
Then configure the handlebars helper function timeago in server/configure.js:
{{! -- server/configure.js --}} const path = require('path'); const exphbs = require('express-handlebars'); const express = require('express'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const morgan = require('morgan'); const methodOverride = require('method-override'); const errorHandler = require('errorhandler'); const moment = require('moment'); const routes = require('./routes'); Module. exports = function(app) {// exports = function(app) {// exports ('zh-cn'); app.engine( 'handlebars', exphbs.create({ helpers: { timeago: function(timestamp) { return moment(timestamp).startOf('minute').fromNow(); }, }, }).engine, ); app.set('view engine', 'handlebars'); app.use(morgan('dev')); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(methodOverride()); app.use(cookieParser('secret-value')); app.use('/public/', express.static(path.join(__dirname, '.. /public'))); if (app.get('env') === 'development') { app.use(errorHandler()); } routes(app); return app; };Copy the code
Then add the timeago function where the timestamp is used:
views/image.handlebars
views/partials/comments.handlebars
Here we have realized the view and controller of the project and completed the first part. In the next part, we will access the MongoDB database to realize the functions of uploading, liking, deleting, commenting and so on.
Original address Turing Community.