导航
导航
文章目录
  1. JavaScript Modules
  2. Express Middleware
    1. Express Middleware 类型
      1. Application-level middleware
      2. Router-level middleware
      3. Error-handling middleware
      4. Built-in middleware
      5. Third-party middleware
  3. 项目实践
    1. app.js
    2. controllers
      1. index.js
      2. test.js
    3. models 模块
    4. router
  4. Architecture
  5. Summary
    1. Reference

学习手记 —— JavaScript Modules 和 Express Middleware

今天仍然是以学习公司后端项目代码为主,学习了有关 JavaScript Module 和 Express Middleware 的相关内容。诚如 9 月 19 日学习手记所言,由于后端代码模块化非常深,所以我这个渣渣学习起来比较费时间。不过,通过今天的学习,对于项目的架构有了更深入的认识,接下来先介绍下 JavaScript Module 和 Express Middleware 两个概念。

JavaScript Modules

In JavaScript, the word “modules” refers to small units of independent, reusable code.
JavaScript Modules

A module encapsulates related code into a single unit of code.
Understanding module.exports and exports in Node.js

从上面的描述可以了解,对于 JavaScript 而言,模块 Module 其实就是一段代码,而这段代码可以是对象、函数、HTML 模版或是 CSS 样式。有关更多模块化的内容可以想见本次推送的另一篇佳作《从 include到 require - 论 node require 设计》,这里不做更多讨论。

Express Middleware

Express is a routing and middleware web framework that has minimal functionality of its own: An Express application is essentially a series of middleware function calls.

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next.

Express 是一个路由和中间件 Web 框架,其自身只具有最低程度的功能:Express 应用程序基本上是一系列中间件函数调用。
中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。

Express Middleware 类型

Application-level middleware

使用 app.use() 和 app.METHOD() 函数将应用层中间件绑定到应用程序对象的实例,其中 METHOD 是中间件函数处理的请求的小写 HTTP 方法(例如 GET、PUT 或 POST)。比如,下面就是利用 app 的应用层中间件 use 函数。

var app = Express();
var router = require('./router');
var models = require('./models');
var controllers = require('./controllers');
app.use(BodyParser.json());
app.use(router);

Router-level middleware

路由器层中间件的工作方式与应用层中间件基本相同,差异之处在于它绑定到 express.Router() 的实例。

var router = express.Router();

Error-handling middleware

错误处理中间件函数的定义方式与其他中间件函数基本相同,差别在于错误处理函数有四个自变量而不是三个,专门具有特征符 (err, req, res, next).

app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});

Built-in middleware

Express 内置中间件包括:

  • express.static 提供静态资源服务,例如 HTML 文件、图片等。
  • express.json 解析收到的 JSON 负载请求。NOTE: Express 4.16.0+ 可用。
  • express.urlencoded 解析收到的 URL 编码负载的请求。 NOTE: Express 4.16.0+ 可用。
app.use(express.static('public'));
app.use(express.static('uploads'));
app.use(express.static('files'));

Third-party middleware

使用第三方中间件向 Express 应用程序添加功能。

var Router = require('express-promise-router');
var c = require('../controllers');
var r = Router();
/// test
r.get('/', c.test.hello);
module.exports = r;

项目实践

我比着公司项目的葫芦画了个小葫芦,项目文件目录如下。

.
|____bin
| |____www
|____models
| |____user.js
| |____index.js
|____public
| |____images
| |____javascripts
| |____stylesheets
| | |____style.css
|____package.json
|____errors.js
|____helpers.js
|____controllers
| |____test.js
| |____index.js
|____views
| |____error.jade
| |____index.jade
| |____layout.jade
|____app.js
|____router
| |____index.js

其中,与 Node Express Modules 编程的相关目录/文件是 models/ 、 controllers/ 、 router/* 、 helpers.js 和 app.js 等文件。

app.js

其中 app.js 文件主要进行模块的组装,将拆分后的 Model、 Controller、 Router 等模块组装在一起。

var Express = require('express');
var BodyParser = require('body-parser');
var PromiseRouter = require('express-promise-router');
var helpers = require('./helpers');
Object.assign(Express.request, helpers.Request);
Object.assign(Express.response, helpers.Response);
var app = Express();
var router = require('./router');
var models = require('./models');
var controllers = require('./controllers');
app.use(BodyParser.json());
app.use(router);
module.exports = app;

controllers

index.js

index.js 是 controllers 模块的入口文件,主要功能是动态的加载模块,有关动态加载模块欢迎阅读另一篇由土豪金大佬写的文章。

var fs = require('fs');
var c = {};
fs.readdirSync(__dirname)
.filter(f => f.match(/\.js$/) && !f.match(/^_/) && f !== 'index.js')
.forEach(f => {
c[f.replace(/\.js$/, '')] = require('./' + f);
});
module.exports = c;

上面这段代码主要的功能是,创建一个对象 c ,并利用 fs 将 controllers 目录下的 js 程序文件都动态地导入进来,然后将导入的程序模块作为一个 c 的实例属性,这样就可以直接用模块名访问了。这段代码真的被惊艳到了,惊叹于 js 的动态性如此灵活。

test.js

test.js 是 controllers 模块中其中一个测试的模块程序文件,如果 controllers 有多个这样的程序模块文件,入口文件 index.js 种的程序都会将这些程序文件作为不同字模块导入到控制器 c 的实例中。

exports.hello = async function(req, res) {
res.send('hello ' + req.protocol);
};

上面这段代码,就是利用 Express 的 Response 对象发送 HTTP 响应结果,并输出 “hello” 和 Express 的 Request 对象的网络协议。

models 模块

models 模块在上一篇手机中简单介绍了 Sequelize 这个 ORM 框架,本文不过多介绍。models 的主要用途就是将程序数据模型,映射到数据库中的关系数据中。 models 模块的模块导入方法和 controllers 是一样的,这里也不做详细介绍了。

router

router 的入口文件 index.js 主要是做路由的绑定,利用三方的路由模块,将用户访问的路由绑定到相应的 controllers 的模块程序调用。

var Router = require('express-promise-router');
var c = require('../controllers');
var r = Router();
/// test
r.get('/', c.test.hello);
module.exports = r;

这段代码将 / 路由绑定到了 c.test.hello 程序调用上,这样当访问根路由的时候,网页就能显示 “hello” 和 Express 的 Request 对象的网络协议了。
以上代码都编写完成后,就可以到程序根目录执行 npm start 查看程序运行效果。

Architecture

在上面的介绍中,我们已经基本学习了 JavaScript Modules 和 Express Middleware,并编写代码实践了 JavaScript 模块化编程。根据本文所提供的代码案例,我们可以得到一个简单的项目架构, Router 和 Controllers 进行绑定,用户访问路由并调用 Controllers 中相应的模块方法,而在 Controllers 的模块方法中,可以调用 Models 的模块方法,最终由 Models 层对数据库数据进行更新等操作。模块化编程的好处是将路由、控制层和模型层的代码分离,这样使程序架构变得更加清晰。

在本次分享的《从 include 到 require - 论 node require 设计》文章中,更加详细的比较了 JavaScript ES6 的标准模块导入 import 和 Nodejs 提供的 require 方式的优缺点。在这篇文章中有如下的一些描述:

而 require 则完全是相反的路子,依赖完全是动态加载的。就像是事物的两面,require 解决了静态依赖的痛点,但同时动态化也使得 ruby 的语法提示变得困难。打包器也不得不以来于一个独立的 Gemfile 来描述依赖管理。

但是同样是 require ,Ryan 却从 ruby 里充分的吸取了历史教训:node require 会返回一个变量而不是修改本地名字空间。

另外 node require 还做了一个极具前瞻性的事情,就是把依赖安装到了项目目录。

就像是我上面说的,动态方案的最大缺陷就在于无法被简单的静态分析。当浏览器加载页面如果需要反复的暂停,等待网络请求,的确会极大的影响用户体验。这个从表明上来说貌似是一个非常“合理”的理由。我们暂且认为这个理由是合理的。但 es 标准化委员会在 import 的设计中犯的低级错误简直无法忍受。

如果去掉 webpack 的,让浏览器直接 import,完全就是不切实际的做法。一个网页通常只有上百个图标。但一个 lodash 都有 631 个文件。如果按照 100ms 的延迟、10 个线程并发、启用 tcp fast open、禁用 https,在这么极端的情况下仍然需要 6.4 秒。这仅仅是一个 lodash。未经压缩的 jQuery 有多少文件,未压缩的 d3 有多少文件?一个页面到时候 10 分钟加载够吗?

以上这几段文字,是我摘录的动态导入模块 require 和 ES6 标准制定的 import 模块导入的相关对比描述。从本文之前的介绍中,我们可以了解到 require 可以动态的加载模块,这样对于程序而言就非常灵活,动态性发挥到极致,并不用将模块方法写死到代码中,甚至线上部署的时候可以利用变量来控制模块的加载。而 import 则只能静态导入模块,并且要将全部的程序模块文件一次性导入,这样做会大大浪费网络请求的时间,从而降低用户体验。

但除此之外,扩展去看,解释型语言和编译型语言相比,性能确实不如编译型语言。比如,我们一个 iOS App 当经过编译打包之后,就可以安装到手机上,当程序加载到内存中后就可以直接运行了。而对于 Web App 我们要先下载 HTML、JavaScript 等相关资源,然后,等 JS 的资源下载完成后浏览器的 JS 引擎才开始进行程序的解释并运行。从这个方面看,客户端开发应该还暂时不会丢饭碗。但如果未来 5G 网络速度又有了更大提升呢?以后网络速度优化更好,说不定真的就不需要 Client 了。

Summary

本文简单介绍了 JavaScript Modules 和 Express Middleware,并进行了编码时间,对模块编程比较深入的学习。

Reference

赞赏
感谢支持~ 😘