Home  >  Article  >  Web Front-end  >  A simple example analysis of how to use Node.js to implement the MVC framework

A simple example analysis of how to use Node.js to implement the MVC framework

黄舟
黄舟Original
2017-08-07 11:51:081560browse

The following editor will bring you an article on how to implement a simple MVC framework using Node.js. The editor thinks it’s pretty good, so I’ll share it with you now and give it as a reference. Let’s follow the editor and take a look.

In the article Building a static resource server using Node.js, we completed the server’s processing of static resource requests, but did not involve dynamic requests. Currently, it cannot be issued based on the client. and return personalized content for different requests. Static resources alone cannot support these complex website applications. This article will introduce how to use <span style="font-family:NSimsun">Node</span> to handle dynamic requests, and how to build a simple MVC framework. Since the previous article has introduced in detail how to respond to static resource requests, this article will skip all the static parts.

A simple example

Let’s start with a simple example to understand how to return dynamic content to the client in Node.

Suppose we have such a requirement:

When the user visits <span style="font-family:NSimsun">/actors</span>, return the actor list page

When the user Return the list of actresses when accessing <span style="font-family:NSimsun">/actresses</span>

You can use the following code to complete the function:


const http = require(&#39;http&#39;);
const url = require(&#39;url&#39;);

http.createServer((req, res) => {
  const pathName = url.parse(req.url).pathname;
  if ([&#39;/actors&#39;, &#39;/actresses&#39;].includes(pathName)) {
    res.writeHead(200, {
      &#39;Content-Type&#39;: &#39;text/html&#39;
    });
    const actors = [&#39;Leonardo DiCaprio&#39;, &#39;Brad Pitt&#39;, &#39;Johnny Depp&#39;];
    const actresses = [&#39;Jennifer Aniston&#39;, &#39;Scarlett Johansson&#39;, &#39;Kate Winslet&#39;];
    let lists = [];
    if (pathName === &#39;/actors&#39;) {
      lists = actors;
    } else {
      lists = actresses;
    }

    const content = lists.reduce((template, item, index) => {
      return template + `<p>No.${index+1} ${item}</p>`;
    }, `<h1>${pathName.slice(1)}</h1>`);
    res.end(content);
  } else {
    res.writeHead(A simple example analysis of how to use Node.js to implement the MVC framework);
    res.end(&#39;<h1>Requested page not found.</h1>&#39;)
  }
}).listen(9527);

The core of the above code is route matching. When a request arrives, it checks whether there is logical processing corresponding to its path. When the request does not match any route, A simple example analysis of how to use Node.js to implement the MVC framework is returned. The corresponding logic is processed when the match is successful.

simple request

The above code is obviously not universal, and there are only two route matching candidates (and the request method has not been distinguished), and the database and template files have not been used. Under the premise, the code is already a little tangled. So next we will build a simple MVC framework to separate data, models, and performance, and each can perform its own duties.

Build a simple MVC framework

MVC refers to:

M: Model (data)

V: View (performance)

C: Controller (logic)

In Node, the process of processing requests under the MVC architecture is as follows:

Request arrival Server

The server will hand over the request to the routing process

The routing will direct the request to the corresponding controller through path matching

The controller receives the request and asks the model for it Data

model returns the required data to the controller

controller may need to do some reprocessing of the received data

controller hands the processed data to 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 : Listen and respond to requests

router: Handle the request to the correct controller

controllers: Execute business logic, retrieve data from the model, and pass it to the view

model: Provide data

view: Provide html

Create the following directory:


-- server.js
-- lib
  -- router.js
-- views
-- controllers
-- models

server

Create server.js file:


const http = require(&#39;http&#39;);
const router = require(&#39;./lib/router&#39;)();

router.get(&#39;/actors&#39;, (req, res) => {
  res.end(&#39;Leonardo DiCaprio, Brad Pitt, Johnny Depp&#39;);
});

http.createServer(router).listen(9527, err => {
  if (err) {
    console.error(err);
    console.info(&#39;Failed to start server&#39;);
  } else {
    console.info(`Server started`);
  }
});

Regardless of the details in this file, router is the module that will be completed below. It is introduced here first and will be handed over to it after the request arrives. deal with.

router module

The router module actually only needs to complete one thing, direct the request to the correct controller for processing. Ideally, it can be used like this:


const router = require(&#39;./lib/router&#39;)();
const actorsController = require(&#39;./controllers/actors&#39;);

router.use((req, res, next) => {
  console.info(&#39;New request arrived&#39;);
  next()
});

router.get(&#39;/actors&#39;, (req, res) => {
  actorsController.fetchList();
});

router.post(&#39;/actors/:name&#39;, (req, res) => {
  actorsController.createNewActor();
});

In general, we hope that it supports 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 the request object and response object. Things that can be done in the middleware include:

Execute any code, such as adding logs and handling errors, etc.

Modify the request ( req) and response object (res), such as getting query parameters from req.url and assigning them to req.query

End response

Call the next middleware (next )

Note:

It should be noted that if the response is neither terminated nor the next method is called within a certain middleware, control will be handed over to the next one. middleware, the request will hang

__Non-routing middleware__ is added in the following way to match all requests:


router.use(fn);

For example, above Example:


router.use((req, res, next) => {
  console.info(&#39;New request arrived&#39;);
  next()
});

__routing middleware__ is added via:


router.HTTP_METHOD(path, fn)

After sorting it out, write the framework first:

/lib/router.js


const METHODS = [&#39;GET&#39;, &#39;POST&#39;, &#39;PUT&#39;, &#39;DELETE&#39;, &#39;HEAD&#39;, &#39;OPTIONS&#39;];

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 above is mainly It adds use, get, post and other methods to the router. Whenever these methods are called, a route rule is added to the routes.

Note:

A function in Javascript is a special object that can be called and also have properties and methods.

The next focus is on the router function. What it needs to do is:

Get method and pathname from the req object

依据 method、pathname 将请求与routes数组内各个 route 按它们被添加的顺序依次匹配

如果与某个route匹配成功,执行 route.handler,执行完后与下一个 route 匹配或结束流程 (后面详述)

如果匹配不成功,继续与下一个 route 匹配,重复3、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();
  };

对于非路由中间件,直接调用其 handler。对于路由中间件,只有请求方法和路径都匹配成功时,才调用其 handler。当没有匹配上的 route 时,直接与下一个route继续匹配。

需要注意的是,在某条 route 匹配成功的情况下,执行完其 handler 之后,还会不会再接着与下个 route 匹配,就要看开发者在其 handler 内有没有主动调用 next() 交出控制权了。

在__server.js__中添加一些route:


router.use((req, res, next) => {
  console.info(&#39;New request arrived&#39;);
  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 = A simple example analysis of how to use Node.js to implement the MVC framework;
  res.end();
});

每个请求抵达时,首先打印出一条 log,接着匹配其他route。当匹配上 actors 或 actresses 的 get 请求时,直接发回演员名字,并不需要继续匹配其他 route。如果都没匹配上,返回 A simple example analysis of how to use Node.js to implement the MVC framework。

在浏览器中依次访问 http://localhost:9527/erwe、http://localhost:9527/actors、http://localhost:9527/actresses 测试一下:

A simple example analysis of how to use Node.js to implement the MVC framework

<span style="font-family:NSimsun">network</span> 中观察到的结果符合预期,同时后台命令行中也打印出了三条 <span style="font-family:NSimsun">New request arrived</span>语句。

接下来继续改进 router 模块。

首先添加一个 router.all 方法,调用它即意味着为所有请求方法都添加了一条 route:


router.all = (path, fn) => {
    METHODS.forEach(item => {
      const method = item.toLowerCase();
      router[method](path, fn);
    })
  };

接着,添加错误处理。

/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(&#39;./lib/router&#39;)((err, req, res) => {
  console.error(err);
  res.statusCode = 500;
  res.end(err.stack);
});
...

默认情况下,遇到错误时会返回 500,但开发者使用 router 模块时可以传入自己的错误处理函数将其替代。

修改一下代码,测试是否能正确执行错误处理:


router.use((req, res, next) => {
  console.info(&#39;New request arrived&#39;);
  next(new Error(&#39;an error&#39;));
});

这样任何请求都应该返回 500:

error stack

继续,修改 route.path 与 pathname 的匹配规则。现在我们认为只有当两字符串相等时才让匹配通过,这没有考虑到 url 中包含路径参数的情况,比如:

localhost:9527/actors/Leonardo

router.get('/actors/:name', someRouteHandler);

这条route应该匹配成功才是。

新增一个函数用来将字符串类型的 route.path 转换成正则对象,并存入 route.pattern:


const getRoutePattern = pathname => {
 pathname = &#39;^&#39; + pathname.replace(/(\:\w+)/g, &#39;\(\[a-zA-Z0-9-\]\+\\s\)&#39;) + &#39;$&#39;;
 return new RegExp(pathname);
};

这样就可以匹配上带有路径参数的url了,并将这些路径参数存入 req.params 对象:


    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];
    }
  }
}

添加个 route 测试一下:


router.get(&#39;/actors/:year/:country&#39;, (req, res) => {
  res.end(`year: ${req.params.year} country: ${req.params.country}`);
});

访问<span style="font-family:NSimsun">http://localhost:9527/actors/1990/China</span>试试:

url parameters

router 模块就写到此,至于查询参数的格式化以及获取请求主体,比较琐碎就不试验了,需要可以直接使用 bordy-parser 等模块。

现在我们已经创建好了router模块,接下来将 route handler 内的业务逻辑都转移到 controller 中去。

修改__server.js__,引入 controller:


...
const actorsController = require(&#39;./controllers/actors&#39;);
...
router.get(&#39;/actors&#39;, (req, res) => {
  actorsController.getList(req, res);
});

router.get(&#39;/actors/:name&#39;, (req, res) => {
  actorsController.getActorByName(req, res);
});

router.get(&#39;/actors/:year/:country&#39;, (req, res) => {
  actorsController.getActorsByYearAndCountry(req, res);
});
...

新建__controllers/actors.js__:


const actorsTemplate = require(&#39;../views/actors-list&#39;);
const actorsModel = require(&#39;../models/actors&#39;);

exports.getList = (req, res) => {
  const data = actorsModel.getList();
  const htmlStr = actorsTemplate.build(data);
  res.writeHead(200, {
    &#39;Content-Type&#39;: &#39;text/html&#39;
  });
  res.end(htmlStr);
};

exports.getActorByName = (req, res) => {
  const data = actorsModel.getActorByName(req.params.name);
  const htmlStr = actorsTemplate.build(data);
  res.writeHead(200, {
    &#39;Content-Type&#39;: &#39;text/html&#39;
  });
  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, {
    &#39;Content-Type&#39;: &#39;text/html&#39;
  });
  res.end(htmlStr);
};

在 controller 中同时引入了 view 和 model, 其充当了这二者间的粘合剂。回顾下 controller 的任务:

controller 收到请求,向 model 索要数据
model 给 controller 返回其所需数据
controller 可能需要对收到的数据做一些再加工
controller 将处理好的数据交给 view

在此 controller 中,我们将调用 model 模块的方法获取演员列表,接着将数据交给 view,交由 view 生成呈现出演员列表页的 html 字符串。最后将此字符串返回给客户端,在浏览器中呈现列表。

从 model 中获取数据

通常 model 是需要跟数据库交互来获取数据的,这里我们就简化一下,将数据存放在一个 json 文件中。

/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"]
  }
]

接着就可以在 model 中定义一些方法来访问这些数据。

models/actors.js


const actors = require(&#39;./test-data&#39;);

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;
});

当 controller 从 model 中取得想要的数据后,下一步就轮到 view 发光发热了。view 层通常都会用到模板引擎,如 dust 等。同样为了简化,这里采用简单替换模板中占位符的方式获取 html,渲染得非常有限,粗略理解过程即可。

创建 /views/actors-list.js:


const actorTemplate = `
<h1>{name}</h1>
<p><em>Born: </em>{contry}, {year}</p>
<ul>{movies}</ul>
`;

exports.build = list => {
  let content = &#39;&#39;;
  list.forEach(actor => {
    content += actorTemplate.replace(&#39;{name}&#39;, actor.name)
          .replace(&#39;{contry}&#39;, actor.country)
          .replace(&#39;{year}&#39;, actor["birth year"])
          .replace(&#39;{movies}&#39;, actor.movies.reduce((moviesHTML, movieName) => {
            return moviesHTML + `<li>${movieName}</li>`
          }, &#39;&#39;));
  });
  return content;
};

在浏览器中测试一下:

test mvc

至此,就大功告成啦!

The above is the detailed content of A simple example analysis of how to use Node.js to implement the MVC framework. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn