《Node与Express开发》读书笔记

夔庆
2023-12-01

1.创建项目

$ mkdir myapp
$ cd myapp
$ npm init

入口文件名最好设置为 项目名.js,避免混淆。(如:我的项目叫myapp,那么我的入口文件命名为myapp.js)

2.安装express

$ npm install --save express

3.配置.gitignore规则

添加到代码库时,忽略掉的文件或文件夹

新建.gitignore文件

# filename: .gitignore
# igonre packages installed by npm
node_modules

4.项目入口文件的配置

新建myapp.js文件

// filename: myapp.js
const express = require('express')
const app = express()

// 设置监听端口
app.set('port', process.env.PORT || 3000)

// 定制路由规则
app.get('/', function(req, res) {
  res.type('text/plain')
  res.send('Hello World')
})

app.get('/about', function(req, res) {
  res.type('text/plain')
  res.send('About Hello World')
})

// 定制404页面
app.use(function(req, res) {
  res.type('text/plain')
  res.status(404)
  res.send('404 - Not Found')
})

// 定制500页面
app.use(function(err, req, res, next) {
  console.error(err.stack)
  res.type('text/plain')
  res.status(500)
  res.send('500 - Server Error')
})

// 监听端口
app.listen(app.get('port'), function() {
  console.log(`Express started on http://localhost:${app.get('port')}; press Ctrl - C to terminate`)
})

// 使用Redirect Path可以方便开发过程中查看状态码

5.添加handlebars作为模板引擎

# 安装模板引擎
$ npm install --save express3-handlebars

安装后,在项目根目录新建views目录,用于存放模板文件
views目录,新建layouts子目录,用于存放布局模板文件

myapp.js中 添加设置模板引擎为handlebars

// filename: myapp.js
// code ...
const app = express();

// 设置handlebars为模板引擎
const handlebars = require('express3-handlebars')
		.create({ defaultLayout:'main' }) // 指明默认布局使用 main.handlebars 作为模板
app.engine('handlebars', handlebars.engine)
app.set('view engine', 'handlebars')

// code ...

view/layouts/目录下创建一个main.handlebars文件,这个是布局模板

<!-- filename: main.handlebars -->
<!DOCTYPE HTML>
<html>
<head>
	<title>My App</title>
</head>
<body>
	{{{body}}}
</body>
</html>

接下来分别给首页关于404500页面创建模板,

views目录下,新建home.handlebars,内容为

<h1>我是首页</h1>

views目录下,新建about.handlebars,内容为

<h1>我是关于页面</h1>

views目录下,新建404.handlebars,内容为

<h1>404 - NOT FOUND</h1>

views目录下,新建500.handlebars,内容为

<h1>500 - Server Error</h1>

模板已经设置好了,接下来修改myapp.js的路由规则

// filename: myapp.js
// code
app.get('/', function(req, res) {
	res.render('home')
})

app.get('/about', function(req, res) {
	res.render('about')
})

// 404 catch-all 处理器(中间件)
app.use(function(req, res, next){
	res.status(404)
	res.render('404')
})

app.use(function(err, req, res, next){
	console.log(err.stack)
	res.status(500)
	res.render('500')
})

node重启一下之后,会发现已经handlebars模板生效了

6.视图模板和静态资源文件

在项目根路径新建public目录,用于存放静态资源
public下新建img目录,用于存放图片
找一张png图片,命名为logo.png,放在public/img/

在所有的路由之前,加入中间件指派静态资源目录

// filename: myapp.js
// code...

app.use(express.static(__dirname + '/public'))
app.get('/', function(req, res) {
	res.render('home')
})
// code...

为了让每个页面都有logo,这个时候需要修改views/layouts/下的main.handlebars

<!-- filename: main.handlebars-->
<!DOCTYPE HTML>
<html>
<head>
	<title>My App</title>
</head>
<body>
	<!-- 添加如下 -->
	<header><img src="/img/logo.png" alt="logo标志"></header>
	{{{body}}}
</body>
</html>

7.视图模板中的动态内容

如果要将动态的数据插入到模板当中,只需要在res.render()中传入两个参数,第一个参数是模板名,第二个参数需要传入一个对象。

// filename: myapp.js

// 定义一个用数组,用于随机输出内容
var fortunes = [
   '我用双手,成就你的梦想',
   '让我们来猎杀,那些陷入黑暗中的人',
   '你们会输的~',
   '我~~是太阳的抉择!',
   '哼,一个能打的都没有',
   '我的箭飞向真理',
   '是时候表演真正的技术了',
   '好运,不会眷顾傻瓜~'
]

app.get('/', function(req, res){
	// 从数组中随机选择一个元素
	var randomThings = things[Math.floor(Math.random() * fortunes.length)]

	// 将 data 传入 home.handlebars
	res.render('home',{ data: randomThings })
})

8.使用git版本控制

如果项目还没有进行版本控制:

# 文件夹初始化
$ git init

# 将所有文件及文件夹添加到暂存区
$ git add -A

# 将单个文件或者文件夹添加到暂存区
$ git add meadowlark.js

# 将提交修改
$ git commit -m '备注'

# 拉取项目
$ git clone https://github.com/用户名/储存库名

# 回退版本
$ git checkout 版本名

# 添加远程储存库
$ git remote add origin git@github.com:用户名/储存库名.git

# 提交至远程储存库
$ git push -u origin master

9.将业务逻辑单独封装成一个模块

在上面的myapp.js中,有一个从数组中随机取一个元素的业务,将其抽离出来封装成一个模块

在项目根目录新建lib目录,用于保存模块
lib目录,新建一个fortune.js
myapp.js中相关业务部分抽离到fortune.js

const fortunes = [
	'我用双手,成就你的梦想',
	'让我们来猎杀,那些陷入黑暗中的人',
	'你们会输的~',
	'我~~是太阳的抉择!',
	'哼,一个能打的都没有',
	'我的箭飞向真理',
	'是时候表演真正的技术了',
	'好运,才不会眷顾傻瓜~'
]
// 如需要让变量在模块外可见,需要把他加载到exports上
exports.getFortune = function () {
	var idx = Math.floor(Math.random() * fortunes.length)
	return fortunes[idx]
}

myapp.js需要引入模块

// filename: myapp.js

// code...

const app = express()
const fortune = require('./lib/fortune')

// code...
app.get('/about', function(req, res) {
	res.render('about',{data: fortune.getFortune()})
})

10. 关于测试

OA技术概览

页面测试跨页测试
测试页面的表示和前端的功能,同时涉及单元测试和集成测试,会使用Mocha进行页面测试从一个页面转到另一个页面的功能的测试。这种测试涉及多个组件,一般被当成集成测试,使用Zombie.js测试
逻辑测试去毛(代码风格检查)
对逻辑域进行单元和集成测试。它只会测试JavaScript,跟所有表示功能分开去毛不是找错误,而是找潜在错误, 使用JSHintESLint进行代码风格检查
链接检查
确保网站上没有破损链接,使用LinkChecker链接检查

服务器可以使用nodemon监控文件是否改动,如果改动自动重启服务器

npm istall --save (将包放在依赖项中)
npm istall --save-dev (将包放在开发依赖项中)

页面测试

使用mocha测试框架 + Chai断言库

  1. 安装
$ npm install --save-dev mocha # mocha测试框架
$ npm install --save-dev chai  # 断言库
$ mkdir public/vendor		   # 新建 vendor 目录,用于存放第三方模块,库

拷贝到静态资源文件夹, Linux环境下使用cp ,window环境下使用copy

$ cp node_modules/mocha/mocha.js public/vendor 		# Mocha 资源放到public目录下
$ cp node_modules/mocha/mocha.css public/vendor
$ cp node_modules/chai/chai.js public/vendor		# chai 资源拷贝到public下
  1. html页面和入口文件配置mochachai

测试开启条件:

  • 默认禁用测试;
  • 传入相应参数开启测试页面;(http:127.0.0.1:3000/?test=1开启)

首先,修改myapp.js,设置中间件判断是否需要测试

// filename: myapp.js

// code..

// 在路由之前配置中间件来检测查询字符串中的条件
app.use(function(req, res, next) {
	res.locals.showTests = app.get('env') !== 'production' && req.query.test === '1' // 通过url判断是否开启测试页面
	next()
})

// 路由放在这...
// code..

然后修改views/layouts/main.handlebars,有条件地引入mocha测试框架,修改<head>部分

<head>
	<title>My App</title>
	{{#if showTests}} <!--这个showTests是从myapp.js里面传过来的-->
		<link rel="stylesheet" href="/vendor/mocha.css">
	{{/if}}
	<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>

</body>之前

	{{#if showTests}}
		<div id="mocha"></div>
		<script src="/vendor/mocha.js"></script>
		<script src="/vendor/chai.js"></script>
		<script type="text/javascript">
			mocha.ui('tdd') // TDD(测试驱动开发),如果不设置默认是BDD(行为驱动开发)
			var assert = chai.assert
		</script>
		<script src="/qa/tests-global.js"></script>
		{{#if pageTestScript}}
			<script src="{{pageTestScript}}"></script>
		{{/if}}

之后,在public目录下,新建qa目录,在qa目录下新建tests-global.js

suite('所有页面测试',function() {
	// 配置测试规则
	test('页面具有有效标题', function() {
		// 断言
		assert(document.title && document.title.match(/\S/) && document.title.toUpperCase() !== 'TODO')
	})
})

以上配置是在所有页面上都进行的测试

如果需要为特定的页面,配置特定的测试规则,需要在上述配置之外在特定页面的路由中传入参数
首先,在qa目录下新建tests-about.js

// filename:tests-about.js
// 配置规则:如页面上至少有一个链接指向`/contact`
suite('about页面测试', function() {
	test('含有“联系我们”入口', function() {
		assert($('a[href="/contact"]').length)
	})
})

然后,需要在路由中指明视图使用哪个页面测试文件,在myapp.js中修改 /about的路由

// filename: myapp.js
app.get('/about', function(req, res) {
 res.render('about',{
 	fortune: fortune.getFortune(),
 	pageTestScript: '/qa/tests-about.js'
 })
})

如此访问页面的时候带上参数?test=1即可开启测试

跨页测试

如:记录用户是通过哪个推广链接,来到被推广页面

首先,在views目录新建一个adsense目录,并在adsense目录下新建一个baidu.handlebars

<h1>假装我是百度</h1>
<a class="targetPage" href="/adsense/target">目标活动页面</a>

然后,在views/adsense目录新建target.handlebars,使用隐藏域将访问来源进行跟踪

<h1>欢迎访问目标活动页面</h1>
<form>
	<input type="hidden" name="referrer">
	<p>姓名:<input type="text" name="name" id="fieldName"></p>
	<p>邮箱:<input type="email" name="email"></p>
	<p><input type="submit" value="Submit"></p>
</form>
<script>
	$(document).ready(function() {
		$('input[name="referrer"]').val(document.referrer)
	})
</script>

接着,在myapp.js中为上面两个页面配置路由:

// filename: myapp.js
// code ...

app.get('/adsense/baidu', function(req, res) {
	res.render('adsense/baidu')
})

app.get('/adsense/target', function(req, res) {
	res.render('adsense/target')
})

// code ...

使用mocha配合无头浏览器(ZombieSeleniumPhantomJS)进行跨页测试
本次使用Zombie + mocha

$ npm install --save-dev zombie
$ npm install -g mocha 				# 如果没有全局安装mocha的话,安装一下

在项目根目录新建qa目录,用于存发测试文件,并在qa目录下新建test-crosspage.js

 // filename: test-crosspage.js
 // 引入 Zombie 无头浏览器模块,以及chai断言库
 var Browser = require('zombie')
 var assert = require('chai').assert
 var browser
 
 // 配置规则
 suite('跨页测试', function() {
 	
 	// 在测试框架运行前执行
 	browser.setup(function() {
 		// 每次测试都使用一个新的浏览器
 		browser = new Browser() 
 	})
 
 	// 第一条规则
	test('经由baidu页面来到推广页面', function(done) {
		let referrer = 'http://127.0.0.1:3000/adsense/baidu'
	 	browser.visit(referrer, function() {
			browser.clickLink('.targetPage', function() {
				// 浏览器模拟点击链接之后,开始断言
				assert(browser.field('referrer').value === referrer)
				done()
			})
		})
	})

	//第二条规则
	test('经由google页面来到推广页面', function(done) {
		let referrer = 'http://127.0.0.1:3000/adsense/google'
	 	browser.visit(referrer, function() {
			browser.clickLink('.targetPage', function() {
				// 浏览器模拟点击链接之后,开始断言
				assert(browser.field('referrer').value === referrer)
				done()
			})
		})
	})

	//第三条规则
	test('直接来到推广页面的流量', function(done) {
	 	browser.visit('http://127.0.0.1:3000/adsense/target', function() {
			assert(browser.field('referrer').value === '')
			done()
		})
	})

})

这个时候我们的服务器在运行中,所以我们需要打开另外一个命令行,并来到根目录下

# mocha -u TDD模式 输出日志为 spec 测试脚本路径
$ mocha -u tdd -R spec qa/tests-crosspage.js
逻辑测试

使用Mocha + chai断言库

qa目录新建tests-unit.js测试文件,用于测试fortune.js模块

// filename: tests-unit.js
var fortune = require('../lib/fortune')
var expect = require('chai').expect

// 配置测试规则
suite('Fortune 模块测试', function () {
	test('getFortune()函数应该返回一个fortune值', function () {
		expect(typeof fortune.getFortune() === 'string')
	})
})

最后,在保证服务器运行的情况下,打开另外一个命令行,切换到项目根目录

$ mocha -u tdd -R spec qa/tests-unit.js
去毛

使用JSHint或者ESLint

第一种:JSHint
通过npm安装JSHint

$ npm install -g jshint

运行比较简单,指定文件名就调用它即可

$ jshint myapp.js

如果不符合规范的话,会报错,一些比较小的问题,可以添加参数,自动修复

$ jshint myapp.js --fix

第二种:ESlint
通过npm安装ESlint

$ npm install -g eslint

之后再项目根目录下,初始化ESlint

$ eslint --init

根据提示操作即可
使用命令可以调用,--fix自动修复

$ eslint myapp.js --fix

具体操作配置等,可见文档

使用grunt/gulp实现自动化

Grunt

$ npm install -g grunt-cli # 全局安装 grunt命令行
$ npm install --save-dev grunt # 将grunt 加入项目开发依赖

这里我们需要用到grunt的mocha插件以及ESLint还有可以执行CMD命令行exec插件
分别使用命令先查询一下插件名

$ npm search grunt-mocha
$ npm search grunt-eslint
$ npm search grunt-exec

找到你觉得合适的包
这里使用
grunt-cafe-mochagrunt-eslint,还有grunt-exec

$ npm install --save-dev grunt-mocha grunt-eslint grunt-exec # 将用到的模块加入到项目开发依赖

接下来在项目根目录下新建Gruntfile.js,用以编辑grunt自动化任务

// filename: Gruntfile.js
module.exports = function(grunt) {
	// 加载插件
	['grunt-cafe-mocha', 'grunt-eslint', 'grunt-exec'].foreach(function(task) {grunt.loadNpmTasks(task)})

	// 配置插件
	grunt.initConfig({
		cafemocha: {all: {src: 'qa/tests-*.js', options: {ui: 'tdd'}}},
		eslint: {
			// 这里将eslint需要检测文件分成两部分
			// 第一部分是入口文件,以及封装好的模块
			app: ['myapp.js', 'public/js/**/*.js', 'lib/**/*.js'],
			// 第二部分是测试文件
			qa: ['Gruntfile.js', 'public/qa/**/*.js', 'qa/**/*.js'],
		},
		exec: {
			// 由于链接检查所使用到的测试工具,没办法通过npm安装使用,只能用命令行调用了
			linchecker: {cmd: 'linkchecker http://127.0.0.1:3000'}
		}
	}),
	
	// 注册任务
	grunt.registerTask('default', ['cafemocha', 'eslint', 'exec'])
}

如此就完成grunt的任务配置,在命令行中,切换到项目根目录

$ grunt            # 开始执行任务 ,如果没有传入任务名,默认按照注册任务的顺序执行任务
$ grunt eslint   # 执行eslint任务,如果代码有不符合eslint风格的话,就会报错,可以使用 --force 参数,让其强制执行

更多配置,参考文档

Gulp
gulpgrunt差不多

$ npm install -g gulp-cli                   # 全局安装gulp-cli
$ npm install --save-dev gulp               # 安装gulp

通过npm搜索相关插件

$ npm search gulp-mocha
$ npm search gulp-eslint
$ npm search gulp-exec

找到合适的插件包,就开始安装

$ npm install --save-dev gulp-mocha
$ npm install --save-dev gulp-eslint
$ npm install --save-dev gulp-exec

安装后,在项目根目录创建gulpfile.js

// filename: gulpfile.js
// 先将需要用到的包加载进来
var gulp = require('gulp')
var mocha = require('gulp-mocha')
var eslint = require('gulp-eslint')
var exec = require('gulpexec')
var { assert } = require('chai')

gulp.task('mocha', () => gulp.src(['qa/tests-*.js', 'public/qa/tests-*.js'], { read: false })
 .pipe(mocha({
   reporter: 'spec',
   globals: {
     assert,
   },
 })))
})

gulp.task('eslint', () => gulp.src(['myapp.js', 'lib/**/*.js', 'qa/tests-*.js', 'public/qa/**/*.js'])
	.pipe(eslint({
		configFile: './.eslintrc.js', 	// eslint配置文件
		reportOutputDir: './report' 	// 输出日志文件夹
	}))
	.pipe(eslint.format('checkstyle', fs.createWriteStream('./report/1.xml'))) // 输出日志
)

gruntgulp 使用其中之一即可

11.请求和响应对象

11.1 URL的组成

https://www.xxx.com/home/a.html?name=xxx&age=xxx#test=1

协议名 http/https
域名www.xxx.com
路径/home/
文件名a.html
参数 name=xxx&age=xxx
锚点#test=1

协议名 : //域名(或者ip)/路径/文件名?参数名=参数值&参数名=参数值#锚点

11.2 http请求方法

比较常见的有 GETPOST方法

11.3 请求报头

有例如useragentcookie

11.4 响应报头

express中会在响应头中显示X-Powered-By信息,可以使用下面代码禁用

app.disable('x-powered-by')
11.5 互联网媒体类型

浏览器接收到来自服务端的响应,根据响应头中的互联网媒体类型(Content-Type)做出渲染
Content-TypeInternet media typeMIME type是可以互换的
一般是text/html;charset=UTF-8

11.6 请求体
一般GET请求没有主题内容
POST主体内容有三种
POST类型描述
1multipart/form-data相对更为复杂的格式,支持文件上传
2application/x-www-form-urlencoded键值对集合的简单编码,用&分隔
3application/jsonAJAX请求
11.7 请求对象

req(请求对象)的生命周期始于Node的一个核心对象http.IncomingMessage的实例。
如下是req中最有用的属性和方法

来自Node的自带方法或属性 req.headers req.url

Express添加的方法或属性

属性(方法)描述
req.params一个数组,包含已命名过的路由参数
req.params(name)返回命名的路由参数,或者GET请求或POST请求参数
req.query一个对象,包含以键值对存放的查询字符串参数,GET请求参数
req.body一个对象,包含POST请求参数,需要使用中间件才能解析请求中的正文信息
req.route关于当前匹配路由的信息。主要用于路由调试
req.cookies/res.singnedCookies一个对象,包含从客户端传递过来的cookies
req.headers从客户端接收到的请求头
req.accepts([types])一个简便的方法,用来确定客户端是否接受一个或一组指定的类型(可选类型可以是单个的MIME类型,如application/json、一个逗号分隔集合或是一个数组)
req.ip客户端的IP地址
req.path请求路径(不包含协议主机端口查询字符串)
req.host一个简便的方法,用来返回客户端所报告的主机名,这些信息可以伪造,所以不应该用于安全目的
req.xhr一个渐变的属性,如果请求由Ajax发起,将会返回true
req.protocol用于标识请求的协议(httpshttp)
req.secure一个简便的属性,如果连接是安全的,将放回true,相当于req.protocol === https
req.url/req.originalUrl返回路径和查询字符串(不包含协议、主机名和端口);req.orginalUrl旨在保留原始请求查询字符串
req.acceptedLanguages一个简便的方法,用来返回客户端首选的一组语言(不同国家的语言,EN-US,ZH-CN )
11.8 响应对象

res(响应对象)的生命周期始于Node的一个核心对象http.ServerResponse的实例。

属性(方法)描述
res.status设置HTTP状态码。默认为200,可以使用它设置404(页面不存在),500(服务器内部错误),重定向 (301302303307)可使用redirect方法
res.set(name, value)设置响应头,通常不用手动设置
res.cookie(name, value, [options])设置客户端cookies值,需要中间件支持
res.clearCookie(name, value, [options])删除客户端cookies值,需要中间件支持
res.rediect([status], url)重定向浏览器,默认302(建立),建议少用,除非永久重定向301(永久重定向)
res.send(body)res.send(status, body)向客户端发送响应以及可选状态码,Express默认内容类型是text/html
res.json(json)res.json(status, json)发送JSON以及可选状态码
res.jsonp(jsonp)res.json(status, jsonp)发送JSONP以及可选状态码
res.type(type)用于设置Content-Type信息
res.format(object)允许根据接收请求头发送不同的内容,如res.format({'text/plain': 'hi there', 'text/html': '<b>hi there</b>'})
res.attachment([filename])Content-Type设置为attachment,浏览器就会选择下载而不是展示内容
res.download(path, [filename], [callback])Content-Type设置为attachment,并且可以指定下载的文件
res.sendFile(path, [option], [callback])根据路径读取文件,并将内容发送到客户端
res.links(links)设置响应头
res.locals一个对象,包含用于渲染视图的默认上下文
res.render(view, [locals], callback)使用配置的模板引擎渲染视图()
 类似资料: