Javascript服务器端开发(二)express运行初探

您所在的位置:网站首页 在线运行js文件 Javascript服务器端开发(二)express运行初探

Javascript服务器端开发(二)express运行初探

2023-03-25 04:31| 来源: 网络整理| 查看: 265

上一篇:Javascript服务器端开发(一)开发环境搭建

完成上一篇的学习后,我们成功创建了expressdemo应用的框架代码,并且使用set DEBUT=expressdemo:* & npm start命令启动了服务器进程,并通过浏览器验证了相关程序是否能正常运行。现在我们就来分析一下为什么我们会获得一个在页面上显示Express等字样的页面。如果我们想在Express后显示一个Hello world,又需要完成哪些工作呢?

对于工具生成的代码,有必要逐一分析以下相关源码,才能理解express框架的运行原理。对于一些不理解的代码,可以暂时搁置,随着学习过程的深入,一些问题可能慢慢就自己解决了。当然也可以求助搜索引擎解决问题,你遇到的问题绝大部分情况下已经有人有解决方案了,因此你的问题一般都能通过搜索引擎解决。当然也可以在这一系列的文章下评论留言,笔者也乐于解答。

1、为什么是npm start

我们刚刚创建的expressdemo应用也是一个标准的npm包,了解一个标准的npm包,可以从文件pakcage.json开始:

{ "name": "expressdemo", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", "http-errors": "~1.6.3", "jade": "~1.11.0", "morgan": "~1.9.1" } }

这里scripts属性的值是一个json对象,start属性的值是node ./bin/www。也就是说执行npm start等同于执行node ./bin/www命令。贼node就是我们说的nodejs虚拟机,./bin/www就是执行的js程序文件。用vscode打开目录D:\devtools\jssrc\expressdemo,然后在vscode左侧找到bin目录下的www文件:

#!/usr/bin/env node /** * Module dependencies. */ var app = require('../app'); var debug = require('debug')('expressdemo:server'); var http = require('http'); /** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ var server = http.createServer(app); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on('error', onError); server.on('listening', onListening); /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { var port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== 'listen') { throw error; } var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); }

这个www文件也是一个js代码文件,只是省去了.js文件扩展名。对于一个稍微大型点的应用,一般都会将程序切分为不同的模块,这些模块可以是逻辑概念,也可以是物理概念。nodejs 遵循 CommonJS 规范,该规范的核心是通过 require函数来加载其他依赖的模块。对于www文件的第一行代码var app =require('../app');,可以理解成www程序需要依赖父目录下下的第一方模块app.js模块。尽管app.js是express generator生成的,但后续我们可能会根据实际需求对这个文件进行个性化改造,因此这个app.js就是第一方(我的)模块。通过require函数,将依赖的模块实例化成app变量,后续如果要调用模块中的函数或对象,可以用http://app.xxx的形式调用,比如后续代码app.setPort(),设置http服务的监听端口。

代码var debug =require('debug')('expressdemo:server')解释如下:require('debug')部分和上面的require('../app')作用类似,加载依赖包,只是这个依赖包不是第一方的包(因为名称前没有指定在哪个路径下,默认就是D:\devtools\jssrc\expressdemo\node_modules目录下的对应子目录),而且是一个叫debug的第三方包。后面的('expressdemo:server')表示是一个函数调用并且传递给调用函数的参数值是字符串expressdemo:server。换句话说:require('debug')加载的是一个函数,require('debug')('expressdemo:server')是对加载的函数立即调用,调用时传递给函数的参数值是字符串expressdemo:server。var debug =require('debug')('expressdemo:server')就是把最终调用的函数的返回值赋值给变量debug,后续对debug的对象的函数或属性可采用http://debug.xxx的形式调用。debug模块的作用就是在执行相关程序时,会打印较多的模块运行日志,便于在程序出现问题时开发人员根据日志内容分析问题原因。

对于这些第三方包,我们这个阶段做到:知道他们的用途是什么以及怎么使用即可,如果知道他们具体是如何实现这些功能的,等后续自己学有余力的时候再去分析源码也不迟。毕竟我们现在是需要快速开发一个我们自己的服务器端应用,对吧?

后续的代码就是一些函数定义与函数调用,利用之前学习到的知识基本可以分析代码的意义。这里需要注意的是:函数调用时参数中有||的情况,||的意思是当第一个值不存在时使用||后的常量作为参数值去调用函数。var port =normalizePort(process.env.PORT||'3000') 和 app.set('port', port)两行代码说明了为什么我们的服务进程监听端口是3000(就是在浏览器地址栏目中输入的:后的时3000)。大家可以思考一个问题:如果这里的3000被修改成立8080,然后执行npm start命令,再次访问应用时,浏览器地址栏应该如何变化呢?

2、app.js文件

服务器进程启动时要依赖app.js模块,这个模块的源码如下:

var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/users', usersRouter); // catch 404 and forward to error handler app.use(function(req, res, next) { next(createError(404)); }); // error handler app.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app;

这是一个典型的nodejs的模块定义语法文件,也是加载各种依赖包,然后对相关模块进行初始化设置。express是一个服务器端MVC设计模式的框架,因此需要对服务器端路由、视图模板路径等进行初始化配置。

通过分析,npm start命令其实就是利用nodejs虚拟机执行了bin目录下的www文件和app.js文件,初始化了服务器进程,并让进程监听3000端口,完成初始化后,就等待浏览器连接服务器进程,然后调用相关程序。

3、http://localhost:3000是怎么回事

vscode打开目录D:\devtools\jssrc\expressdemo,然后在vscode终端(如果没有终端窗口,请点击主菜单”终端-》新建终端“菜单项):

点击新建终端打开终端窗口

然后在终端中执行命令:set DEBUT=expressdemo:* ; npm start(注意这里命令分隔符和cmd有区别,cmd命令分隔符是&,PowerShell分割符是;),然后打开浏览器,在地址栏中输入http://localhost:3000后回车,浏览器将显示Express字样的文字。

那么这是怎么做到的呢?看一下app.js中的关键代码:

//…… var indexRouter = require('./routes/index'); var usersRouter = require('./routes/users'); //…… app.use('/', indexRouter); app.use('/users', usersRouter); //……

地址栏目输入地址后,浏览器进程向服务器进程发起了一个HTTP协议的GET请求,请求资源/,在app.js中定义了资源/对应服务器程序是./routes/index的文件:这是因为app.use('/', indexRouter)代码指定了资源/由变量indexRouter来处理,而变量indexRouter又是由代码var indexRouter =require('./routes/index')决定的。之前说过require函数的作用是加载模块。由于模块定义中有 . 这样的相对路径,说明这个模块由我们自己定义。打开routes目录下的index.js文件:

var express = require('express');//加载express模块 var router = express.Router();//调用模块方法 /* GET home page. * 第一个参数:请求资源 * 第二个参数:匿名函数,当浏览器发起对资源的调用请求后,会执行这个匿名函数 */ router.get('/', function(req, res, next) { //在转到模板处理前的逻辑写在这里,也就是为生成结果数据的处理逻辑写在这里 res.render('index', { title: 'Express' });//逻辑处理结束,交由视图层代码继续处理 });//定义资源/对应的服务器端处理逻辑 module.exports = router;//将路由作为模块导出已备app.js中的var indexRouter = require('./routes/index');调用

router.get函数是定义/资源由一个匿名函数来处理:get函数第二个参数function(req, res, next) {……}是一个匿名函数,这个/资源对应的逻辑代码笔者在源码中做了说明。由于这个服务器进程在接收到/资源请求时,没有做任何的逻辑处理,接着调用了res.render函数,调用视图层的代码index继续服务器端处理,在调用视图层的代码时,使用了一个JSON对象错误render的第二个参数,这就相当于把业务处理结果的数据传递给了视图层代码。那么这个index有对应了那个代码文件呢?看app.js的关键代码:

//…… app.set('views', path.join(__dirname, 'views'));//视图层代码路径 app.set('view engine', 'jade');//视图层模板是jade模板 //……

__dirname是一个固定写法,总是指向被执行 js 文件的所在的目录的绝对路径,当前指向的是D:\devtools\jssrc\expressdemo目录。因此routes\index.js文件中代码调用res.render函数的第一个参数指向的文件就是D:\devtools\jssrc\expressdemo\views\index.jade文件。打开views\index.jade文件:

//-继承layout模板 extends layout //-模板内容 block content h1= title //-转换为HTML后Express p Welcome to #{title}//-转换为HTML后

Welcome to Express

这就是模板文件,作用就是将代码中的#{}用数据对象中对应的值替换,然后转换为标准的HTML代码输出。这里h1= title中的title,就是routes\index.js文件中代码res.render('index',{ title:'Express'}),res.render函数的第二个参数JSON对象的title。再打开views\layout.jade文件:

html head title= title link(rel='stylesheet', href='/stylesheets/style.css')//-引入CSS body //-…… block content //-运行期被继承模块内容替换

extends是jade模板的复用方式,layout代码的关键点在源码中也进行了说明

关于jade的模板语法,可参考官方网站或者通过搜索引擎学习。要理解jade运行后的结果,就需要借助浏览器开发者工具,查看/返回的标准HTML代码如下:

Express Express Welcome to Express

我们结合服务器进程jade模板引擎处理后的标准HTML输出,慢慢就可以理解”模板文件+数据===》HTML字符串“的具体含义了。

现在我们的需求是要在Express后添加一个中文字符串:你好,世界。

我们采用用反向思维的方式来解决这个问题:

1、要在页面上显示”Express,你好,世界!“,浏览器就要获得类似Express。你好,世界!这样的HTML。

2、要生成类似Express。你好,世界!这样的HTML,可以修改views\index.jade文件内容如下:

//修改前 h1= title //修改后 h1 #{title}。你好,世界!

回到浏览器并刷新页面,看到页面输出的变化了吧:

修改views\index.jade文件后的运行结果

现在我们采用正向思维的方式解决这个问题:

views\index.jade在h1标签中输出了title属性的值,那么我们在title属性的值中添加”。你好,世界!“的字符串是不是也可以解决这个问题?修改routes\index.js文件如下:

//修改前 res.render('index', { title: 'Express' }); //修改后 res.render('index', { title: 'Express。你好,世界!' });

再次刷新页面,发现结果并不是预期的结果。但理论分析,这个修改应该是可行的啊。

我们重启一次服务进程:在终端中点击组合键”Ctrl+C“组合键,根据提示输入y终止服务器进程,然后点击向上箭头,显示之前执行的命令后,再次点击回车按键,启动服务器进程。再次回到浏览器,刷新页面,看看是否达到了预期的效果。同时也请注意一下这个结果和之前修改jade文件的结果有什么不同,也请分析一下为什么会有这么多的不同结果产生:

修改routs\index.js的运行结果

我们用一副图来说明整个程序的运行过程:

浏览器进程与服务器进程间程序执行过程

理解这个程序的执行过程很重要,只有理解了这个过程,一旦程序出现错误,我们就可以分析到底哪里的程序出了问题,应该如何解决这些问题,通过分析问题,解决问题,我们就可以慢慢提升我们的编程能力了。这也是为社么说学习编程可以锻炼开发人员的逻辑思维能力的原因,而逻辑思维能力是编程中很重要的一种能力。

4、修改代码后自动重启服务器进程

之前的学习过程中,我们修改routes目录下的文件后,需要重启服务器进程才能看到修改后的效果,如果后续频繁修改文件,需要频繁启动服务器进程,这且不是很耗费时间?下面我们就使用supervisor模块来启动自动重启服务器进程,该工具可自动感知程序变化然后自动重启服务进程。

在vscode的终端中终止服务进程,执行npm install -g supervisor命令安装supervisor包,安装重构后,执行supervisor ./bin/www命令再次启动服务进程,然后在浏览器中刷新页面。再次修改routes\index.js文件:修改title的值:

//修改后代码 res.render('index', { title: 'Express。你好,世界!修改文件后是否需要自动重启了?' });

再次回到浏览器,刷新页面,发现修改内容已经是最新的内容了:

supervistor自动重启服务进程

至此,完成了expressdemo应用运行过程的分析,于是可以思考:如果需要完成服务器版参数游戏



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3