Use Node. js to implement the simple MVC framework.
When using Node. in the article about setting up a static resource server in js, we have completed the server's processing of static resource requests, but it does not involve dynamic requests. Currently, we cannot return personalized content based on different requests sent by the client. Static resources alone can support these complex website applications. This article will introduce how to useNodeProcess dynamic requests and build a simple MVC framework. Because the previous article has detailed how to respond to static resource requests, this article will skip all static parts.
First, let's start with a simple example to understand how to return dynamic content to the client in Node.
const http = require('http');const url = require('url');http.createServer((req, res) => { const pathName = url.parse(req.url).pathname; if (['/actors', '/actresses'].includes(pathName)) { res.writeHead(200, { 'Content-Type': 'text/html' }); const actors = ['Leonardo DiCaprio', 'Brad Pitt', 'Johnny Depp']; const actresses = ['Jennifer Aniston', 'Scarlett Johansson', 'Kate Winslet']; let lists = []; if (pathName === '/actors') { lists = actors; } else { lists = actresses; } const content = lists.reduce((template, item, index) => { return template + `<p>No.${index+1} ${item}</p>`; }, `
The core of the above Code is route matching. When a request arrives, check whether there is logic processing corresponding to its path. When the request does not match any route, 404 is returned. The corresponding logic is processed when the matching is successful.
The above code is obviously not universal, and only has two kinds of route matching candidates (and the request method is not differentiated), and the database and template file are not used yet, the Code is a bit tangled. Therefore, we will build a simple MVC Framework to separate data, models, and performance and perform their respective duties.
Build a simple MVC Framework
MVC respectively refers:
M: Model (data)
V: View (performance)
C: Controller (logic)
In Node, the request processing process in the MVC Architecture is as follows:
Request to arrive at Server
The server submits the request to the route for processing.
Route requests are directed to the corresponding controller through Path Matching.
The controller receives a request to request data from the model.
The model returns the required data to the controller.
The controller may need to reprocess the received data.
The controller sends the processed data to the view.
View generates response Content Based on Data and templates
The server returns this content to the client.
Based on this, we need to prepare the following modules:
Server: listener and Response Request
Router: submit the request to the correct controller.
Controllers: executes the business logic, extracts data from the model, and passes the data to the view.
Model: provides data
View: provides html
Create the following directory:
-- server.js-- lib -- router.js-- views-- controllers-- models
Server
Create a server. js file:
const http = require('http');const router = require('./lib/router')();router.get('/actors', (req, res) => { res.end('Leonardo DiCaprio, Brad Pitt, Johnny Depp');});http.createServer(router).listen(9527, err => { if (err) { console.error(err); console.info('Failed to start server'); } else { console.info(`Server started`); }});
Regardless of the details in the file, the router is the module to be completed below. It is introduced here, And the request will be processed by it upon arrival.
Router Module
The router module only needs to do one thing and direct requests to the correct controller for processing. Ideally, it can be used like this:
const router = require('./lib/router')();const actorsController = require('./controllers/actors');router.use((req, res, next) => { console.info('New request arrived'); next()});router.get('/actors', (req, res) => { actorsController.fetchList();});router.post('/actors/:name', (req, res) => { actorsController.createNewActor();});
In general, we want it to support both routing middleware and non-middleware. After the request arrives, the router will hand it over to the matching middleware for processing. Middleware is a function that can access request objects and response objects. What can be done in middleware include:
Execute any code, such as adding logs and handling errors.
Modify the request (req) and response object (res). For example, obtain query parameters from req. url and assign values to req. query.
End response
Call next middleware (next)
Note:
It should be noted that if no response is terminated in a middleware or the next method is called to give control to the next middleware, the request will be suspended.
_ Non-routing middleware _ add in the following way to match all requests:
router.use(fn);
For example:
router.use((req, res, next) => { console.info('New request arrived'); next()});
_ Routing middleware _ add using the following method to precisely match the request method with the path:
router.HTTP_METHOD(path, fn)
After finishing the sorting, write out the framework:
/Lib/router. js
const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'];module.exports = () => { const routes = []; const router = (req, res) => { }; router.use = (fn) => { routes.push({ method: null, path: null, handler: fn }); }; METHODS.forEach(item => { const method = item.toLowerCase(); router[method] = (path, fn) => { routes.push({ method, path, handler: fn }); }; });};
The preceding section mainly adds use, get, post, and other methods to the router. Each time you call these methods, add a route rule to routes.
Note:
Functions in Javascript are special objects that can be called and have attributes and methods.
The following focuses on the router function. what it needs to do is:
Obtain method and pathname from req object
Match the requests with the route entries in the routes array in the order they are added based on method and pathname.
If a route match is successful, execute route. handler. After the execution, match with the next route or end the process (detailed later)
If the match fails, continue to match with the next route. Repeat Steps 3 and 4.
const router = (req, res) => { const pathname = decodeURI(url.parse(req.url).pathname); const method = req.method.toLowerCase(); let i = 0; const next = () => { route = routes[i++]; if (!route) return; const routeForAllRequest = !route.method && !route.path; if (routeForAllRequest || (route.method === method && pathname === route.path)) { route.handler(req, res, next); } else { next(); } } next(); };
For non-routing middleware, directly call its handler. For routing middleware, handler is called only when the request method and path match successfully. If there is no matched route, it directly matches with the next route.
It should be noted that, when a route matches successfully, will the next route match be performed after the handler is executed, it depends on whether the developer has actively called next () in the handler to hand over control.
Add some route in _ server. js:
router.use((req, res, next) => { console.info('New request arrived'); next()});router.get('/actors', (req, res) => { res.end('Leonardo DiCaprio, Brad Pitt, Johnny Depp');});router.get('/actresses', (req, res) => { res.end('Jennifer Aniston, Scarlett Johansson, Kate Winslet');});router.use((req, res, next) => { res.statusCode = 404; res.end();});
When each request arrives, a log is printed, and other route is matched. When the get request matches actors or actresses, the actor name is directly sent back, and other route matches are not required. If none match, 404 is returned.
Access http: // localhost: 9527/erwe, http: // localhost: 9527/actors, http: // localhost: 9527/actresses in the browser to test:
networkThe observed results are as expected, and threeNew request arrivedStatement.
Next, we will continue to improve the router module.
First, add a router. all method. Calling it means that a route is added to all request methods:
router.all = (path, fn) => { METHODS.forEach(item => { const method = item.toLowerCase(); router[method](path, fn); }) };
Next, add error handling.
/Lib/router. js
const defaultErrorHander = (err, req, res) => { res.statusCode = 500; res.end();};module.exports = (errorHander) => { const routes = []; const router = (req, res) => { ... errorHander = errorHander || defaultErrorHander; const next = (err) => { if (err) return errorHander(err, req, res); ... } next(); };
Server. js
...const router = require('./lib/router')((err, req, res) => { console.error(err); res.statusCode = 500; res.end(err.stack);});...
By default, 500 is returned when an error occurs. However, when using the router module, developers can input their own error handler function to replace it.
Modify the code to test whether error handling can be correctly executed:
router.use((req, res, next) => { console.info('New request arrived'); next(new Error('an error'));});
In this case, any request should return 500:
Continue. Modify the matching rules of route. path and pathname. Now we think that the matching is passed only when the two strings are equal, which does not take into account the url containing path parameters, such:
localhost:9527/actors/Leonardo
And
router.get('/actors/:name', someRouteHandler);
This route should be matched successfully.
Add a function to convert the route. path of the string type into a regular object and store it in route. pattern:
const getRoutePattern = pathname => { pathname = '^' + pathname.replace(/(\:\w+)/g, '\(\[a-zA-Z0-9-\]\+\\s\)') + '$'; return new RegExp(pathname);};
In this way, you can match the URLs with path parameters and save these path parameters to the req. params object:
const matchedResults = pathname.match(route.pattern); if (route.method === method && matchedResults) { addParamsToRequest(req, route.path, matchedResults); route.handler(req, res, next); } else { next(); }
const addParamsToRequest = (req, routePath, matchedResults) => { req.params = {}; let urlParameterNames = routePath.match(/:(\w+)/g); if (urlParameterNames) { for (let i=0; i < urlParameterNames.length; i++) { req.params[urlParameterNames[i].slice(1)] = matchedResults[i + 1]; } }}
Run the following command to add a route:
router.get('/actors/:year/:country', (req, res) => { res.end(`year: ${req.params.year} country: ${req.params.country}`);});
Accesshttp://localhost:9527/actors/1990/ChinaTry:
The router module writes this article. As for formatting query parameters and obtaining request bodies, it is trivial and will not be tested. You can directly use bordy-parser and other modules.
Now that we have created the router module, we will transfer the business logic in route handler to the controller.
Modify _ server. js __and introduce controller:
...const actorsController = require('./controllers/actors');...router.get('/actors', (req, res) => { actorsController.getList(req, res);});router.get('/actors/:name', (req, res) => { actorsController.getActorByName(req, res);});router.get('/actors/:year/:country', (req, res) => { actorsController.getActorsByYearAndCountry(req, res);});...
Create _ controllers/actors. js __:
const actorsTemplate = require('../views/actors-list');const actorsModel = require('../models/actors');exports.getList = (req, res) => { const data = actorsModel.getList(); const htmlStr = actorsTemplate.build(data); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(htmlStr);};exports.getActorByName = (req, res) => { const data = actorsModel.getActorByName(req.params.name); const htmlStr = actorsTemplate.build(data); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(htmlStr);};exports.getActorsByYearAndCountry = (req, res) => { const data = actorsModel.getActorsByYearAndCountry(req.params.year, req.params.country); const htmlStr = actorsTemplate.build(data); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(htmlStr);};
The view and model are introduced in the controller, which act as the adhesive between the two. Review the controller task:
The controller receives a request to request data from the model.
The model returns the required data to the controller.
The controller may need to reprocess the received data.
The controller sends the processed data to the view.
In this controller, we will call the method of the model module to obtain the list of actors, and then hand the data to the view to generate an html string showing the list of actors. Finally, return the string to the client to display the list in the browser.
Get data from model
Generally, the model needs to interact with the database to obtain data. Here we will simplify the process and store the data in a json file.
/Models/test-data.json
[ { "name": "Leonardo DiCaprio", "birth year": 1974, "country": "US", "movies": ["Titanic", "The Revenant", "Inception"] }, { "name": "Brad Pitt", "birth year": 1963, "country": "US", "movies": ["Fight Club", "Inglourious Basterd", "Mr. & Mrs. Smith"] }, { "name": "Johnny Depp", "birth year": 1963, "country": "US", "movies": ["Edward Scissorhands", "Black Mass", "The Lone Ranger"] }]
Then you can define some methods in the model to access the data.
Models/actors. js
const actors = require('./test-data');exports.getList = () => actors;exports.getActorByName = (name) => actors.filter(actor => { return actor.name == name;});exports.getActorsByYearAndCountry = (year, country) => actors.filter(actor => { return actor["birth year"] == year && actor.country == country;});
When the controller obtains the desired data from the model, the next step is the view glow. The view layer usually uses the template engine, such as dust. Similarly, to simplify the process, here we use the simple replacement of placeholders in the template to obtain html, rendering is very limited, just a rough understanding of the process.
Creating/views/actors-list.js:
const actorTemplate = `
Test in the browser:
So far, this is a success!
The above method for implementing the simple MVC framework using Node. js is all the content shared by Alibaba Cloud xiaobian. I hope you can give us a reference and support for the customer's house.