note
  • Introduction
  • algorithm
    • Array
    • BinarySearch
    • Bits
    • Design
    • DFS
    • DP
    • DP_bag
    • DP_stock.md
    • Hash
    • Heap
    • NodeList
    • Number
    • SlideWindow
    • Sort
    • Stack
    • String
    • Tree
  • Backend
    • express
    • koa2
    • node
    • npm
    • npx
  • db
    • mongoDB
  • Frontend
    • CSS
      • BFC
      • flex
      • layout
      • less
      • middle
      • position
      • sass
      • selector
      • 动画相关属性
      • 响应式页面
      • 层叠上下文
      • 隐藏元素
    • JS
      • Array
      • async
      • DOM
      • ES6
      • JS-军规
      • macrotask-microtask
      • practice
      • RegExp
      • this-prototype-inherit
      • type-convert
      • 前端请求
      • 浏览器加载
      • 浏览器窗口间通信
    • TS
      • note
    • Vue
      • practice
      • Component-Communicate
      • Component
      • lifecycle
      • note
      • Pinia
      • practice
      • Vue-cli
      • Vue-Router
      • Vue-Source
      • Vue2
      • Vue3
      • Vuex
    • cross-domain
    • webpack
    • 从前端角度理解缓存
    • 前端概念
    • 页面性能分析与优化
    • 页面渲染机制
  • Linux
    • basic-command
    • docker
    • java-env
    • learn
    • manage-command
    • nginx
    • Shell
    • ssh
    • tmux
    • vi
  • chrome-devtools
  • git
  • Jenkins
Powered by GitBook
On this page
  1. Backend

koa2

1.get started

//1 npm init -y
//2 npm i koa

//3 hello world
const Koa = require('koa')
const app = new Koa()

app.use(async ctx => {
	ctx.body = `hello koa2`
}).listen(3000, _ => {
	console.log('server is starting at port 3000')
})
//4 run
node xxx.js

ctx

ctx作为上下文使用,Koa 将 node 的 request, response对象封装进一个单独对象。即ctx.request 、 ctx.response。Koa 内部又对一些常用的属性或者方法做了代理操作,使得我们可以直接通过 ctx 获取。比如,ctx.request.ur以写成ctx.url

除此之外,Koa 还约定了一个中间件的存储空间ctx.state。通过 state 可以存储一些数据,比如用户数据,版本信息等。如果你使用 webpack 打包的话,可以使用中间件,将加载资源的方法作为 ctx.state 的属性传入到 view 层,方便获取资源路径

next

next 参数的作用是将处理的控制权转交给下一个中间件

//notice the sequence of middleware
const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
	let startTime = new Date().getTime()
	await next()
	let endTime = new Date().getTime()
	console.log(`response time is:${endTime - startTime}ms`)
})

app.use(async (ctx, next) => {
	console.log('111, 然后doSomething')
	await next()
	console.log('111 end')
})

app.use(async (ctx, next) => {
	console.log('222, 然后doSomething')
	await next()
	console.log('222 end')
})

app.use(async (ctx, next) => {
	console.log('333, 然后doSomething')
	await next()
	console.log('333 end')
})

app.listen(3333, () => {
	console.log('server is running at http://localhost:3333')
})

2.koa2 特性

  • 只提供封装好 http 上下文、请求、响应,以及基于async/await的中间件容器

  • 利用 ES7 的async/await的来处理传统回调嵌套问题和代替 koa@1 的 generator,但是需要在 node.js 7.x 的 harmony 模式下才能支持async/await

  • 中间件只支持 async/await 封装的,如果要使用 koa@1 基于 generator 中间件,需要通过中间件 koa-convert 封装一下才能使用

3.koa2 中间件开发(顺序)

  1. generator 中间件,用在 koa1

  2. generator 中间件在 koa@2 中的使用,需要用 koa-convert 封装一下

    const Koa = require('koa') // koa v2
    const convert = require('koa-convert')
    const loggerGenerator = require('./middleware/logger-generator')
    const app = new Koa()
    
    app.use(convert(loggerGenerator()))
    
    app.use((ctx) => {
    	ctx.body = 'hello world!'
    })
    
    app.listen(3000)
    console.log('the server is starting at port 3000')
  3. async 中间件

    /* ./middleware/logger-async.js */
    function log(ctx) {
    	console.log(ctx.method, ctx.header.host + ctx.url)
    }
    
    module.exports = function () {
    	return async function (ctx, next) {
    		log(ctx)
    		await next()
    	}
    }
    //app.js
    const Koa = require('koa') // koa v2
    const loggerAsync = require('./middleware/logger-async')
    const app = new Koa()
    
    app.use(loggerAsync())
    
    app.use((ctx) => {
    	ctx.body = 'hello world!'
    })
    
    app.listen(3000)
    console.log('middleware demo is running at port 3000')

4.路由(koa-router)

example

const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
//npm i koa-router
const Router = require('koa-router')

let home = new Router()

// 子路由1
home.get('/', async (ctx) => {
	let html = `
    <ul>
      <li><a href="/page/helloworld">/page/helloworld</a></li>
      <li><a href="/page/404">/page/404</a></li>
    </ul>
  `
	ctx.body = html
})

// 子路由2
let page = new Router()
page
	.get('/404', async (ctx) => {
		ctx.body = '404 page!'
	})
	.get('/helloworld', async (ctx) => {
		ctx.body = 'helloworld page!'
	})

// 装载所有子路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())

// 加载路由中间件
app.use(router.routes()).use(router.allowedMethods())

app.listen(3000, () => {
	console.log('koa-router-demo is starting at port 3000')
})

router.all

// 同时命中
router.get('/', async (ctx, next) => {
	ctx.response.body = `<h1>index page</h1>`
	await next()
})
router.all('/', async (ctx, next) => {
	console.log('match "all" method')
	await next()
})

命名路由

router.get('user', '/users/:id', function (ctx, next) {
	// ...
})

router.url('user', 3)
// => 生成路由 "/users/3"

router.url('user', { id: 3 })
// => 生成路由 "/users/3"

router.use(function (ctx, next) {
	// 重定向到路由名称为 “sign-in” 的页面
	ctx.redirect(ctx.router.url('sign-in'))
})

多中间件

router.get(
	'/users/:id',
	function (ctx, next) {
		return User.findOne(ctx.params.id).then(function (user) {
			// 首先读取用户的信息,异步操作
			ctx.user = user
			next()
		})
	},
	function (ctx) {
		console.log(ctx.user)
		// 在这个中间件中再对用户信息做一些处理
		// => { id: 17, name: "Alex" }
	}
)

嵌套路由

var forums = new Router();
var posts = new Router();

posts.get('/', function (ctx, next) {...});
posts.get('/:pid', function (ctx, next) {...});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());

// 可以匹配到的路由为 "/forums/123/posts" 或者 "/forums/123/posts/123"
app.use(forums.routes());

路由前缀(前缀是一个固定的字符串,不能添加动态参数)

var router = new Router({
  prefix: '/users'
});

router.get('/', ...); // 匹配路由 "/users"
router.get('/:id', ...); // 匹配路由 "/users/:id"

URL 参数

router.get('/:category/:title', function (ctx, next) {
	console.log(ctx.params)
	// => { category: 'programming', title: 'how-to-node' }
})

5.请求数据获取

get

const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
	let url = ctx.url
	// 从上下文的request对象中获取
	let request = ctx.request
	let req_query = request.query
	let req_querystring = request.querystring
	// 从上下文中直接获取
	let ctx_query = ctx.query
	let ctx_querystring = ctx.querystring
	ctx.body = {
		url,
		req_query,
		req_querystring,
		ctx_query,
		ctx_querystring,
	}
})
app.listen(3000, () => {
	console.log('parameter demo is starting at port 3000')
})
router.get('/data/:id', async (ctx, next) => {
	// 也从ctx.params对象获取数据
	let data = ctx.params
	ctx.body = data
})

post

注意:ctx.request是context经过封装的请求对象,ctx.req是context提供的node.js原生 HTTP 请求对象,同理ctx.response是context经过封装的响应对象,ctx.res是context提供的node.js原生 HTTP 请求对象

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
	if (ctx.url === '/' && ctx.method === 'GET') {
		// 当GET请求时候返回表单页面
		let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <p>email</p>
        <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `
		ctx.body = html
	} else if (ctx.url === '/' && ctx.method === 'POST') {
		// 当POST请求的时候,解析POST表单里的数据,并显示出来
		let postData = await parsePostData(ctx)
		ctx.body = postData
	} else {
		// 其他请求显示404
		ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
	}
})

// 解析上下文里node原生请求的POST参数
function parsePostData(ctx) {
	return new Promise((resolve, reject) => {
		try {
			let postData = ''
			ctx.req.addListener('data', (data) => {
				postData += data
			})
			ctx.req.addListener('end', function () {
				let parseData = parseQueryStr(postData)
				resolve(parseData)
			})
		} catch (err) {
			reject(err)
		}
	})
}

// 将POST请求参数字符串解析成JSON
function parseQueryStr(queryStr) {
	let queryData = {}
	let queryStrList = queryStr.split('&')
	console.log(queryStrList)
	for (let [index, queryStr] of queryStrList.entries()) {
		let itemList = queryStr.split('=')
		queryData[itemList[0]] = decodeURIComponent(itemList[1])
	}
	return queryData
}

app.listen(3000, () => {
	console.log('request post demo is starting at port 3000')
})

koa-bodyparser

const Koa = require('koa')
const app = new Koa()
//npm i koa-bodyparser
const bodyParser = require('koa-bodyparser')

app.use(bodyParser())

app.use(async (ctx) => {
	if (ctx.url === '/' && ctx.method === 'GET') {
		// 当GET请求时候返回表单页面
		let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <p>email</p>
        <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `
		ctx.body = html
	} else if (ctx.url === '/' && ctx.method === 'POST') {
		// 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来
		let postData = ctx.request.body
		ctx.body = postData
	} else {
		// 其他请求显示404
		ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
	}
})

app.listen(3000, () => {
	console.log('koa-bodyparser demo is starting at port 3000')
})

6.静态资源加载

一个 http 请求访问 web 服务静态资源,一般响应结果有三种情况

  • 访问文本,例如 js,css,png,jpg,gif

  • 访问静态目录

  • 找不到资源,抛出 404 错误

原生实现

//index.js
const Koa = require('koa')
const path = require('path')
const content = require('./util/content')
const mimes = require('./util/mimes')

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

// 解析资源类型
function parseMime(url) {
	let extName = path.extname(url)
	extName = extName ? extName.slice(1) : 'unknown'
	return mimes[extName]
}

app.use(async (ctx) => {
	// 静态资源目录在本地的绝对路径
	let fullStaticPath = path.join(__dirname, staticPath)

	// 获取静态资源内容,有可能是文件内容,目录,或404
	let _content = await content(ctx, fullStaticPath)

	// 解析请求内容的类型
	let _mime = parseMime(ctx.url)

	// 如果有对应的文件类型,就配置上下文的类型
	if (_mime) {
		ctx.type = _mime
	}

	// 输出静态资源内容
	if (_mime && _mime.indexOf('image/') >= 0) {
		// 如果是图片,则用node原生res,输出二进制数据
		ctx.res.writeHead(200)
		ctx.res.write(_content, 'binary')
		ctx.res.end()
	} else {
		// 其他则输出文本
		ctx.body = _content
	}
})

app.listen(3000)
console.log('static-server demo is starting at port 3000')
//util/content.js
const path = require('path')
const fs = require('fs')

// 封装读取目录内容方法
const dir = require('./dir')

// 封装读取文件内容方法
const file = require('./file')

/**
 * 获取静态资源内容
 * @param  {object} ctx koa上下文
 * @param  {string} 静态资源目录在本地的绝对路径
 * @return  {string} 请求获取到的本地内容
 */
async function content(ctx, fullStaticPath) {
	// 封装请求资源的完绝对径
	let reqPath = path.join(fullStaticPath, ctx.url)

	// 判断请求路径是否为存在目录或者文件
	let exist = fs.existsSync(reqPath)

	// 返回请求内容, 默认为空
	let content = ''

	if (!exist) {
		//如果请求路径不存在,返回404
		content = '404 Not Found! o(╯□╰)o!'
	} else {
		//判断访问地址是文件夹还是文件
		let stat = fs.statSync(reqPath)

		if (stat.isDirectory()) {
			//如果为目录,则渲读取目录内容
			content = dir(ctx.url, reqPath)
		} else {
			// 如果请求为文件,则读取文件内容
			content = await file(reqPath)
		}
	}

	return content
}

module.exports = content
//util/dir.js
const url = require('url')
const fs = require('fs')
const path = require('path')

// 遍历读取目录内容方法
const walk = require('./walk')

/**
 * 封装目录内容
 * @param  {string} url 当前请求的上下文中的url,即ctx.url
 * @param  {string} reqPath 请求静态资源的完整本地路径
 * @return {string} 返回目录内容,封装成HTML
 */
function dir(url, reqPath) {
	// 遍历读取当前目录下的文件、子目录
	let contentList = walk(reqPath)

	let html = `<ul>`
	for (let [index, item] of contentList.entries()) {
		html = `${html}<li><a href="${url === '/' ? '' : url}/${item}">${item}</a>`
	}
	html = `${html}</ul>`

	return html
}

module.exports = dir
//util/file.js
const fs = require('fs')

/**
 * 读取文件方法
 * @param  {string} 文件本地的绝对路径
 * @return {string|binary}
 */
function file(filePath) {
	let content = fs.readFileSync(filePath, 'binary')
	return content
}

module.exports = file
//util/walk.js
const fs = require('fs')
const mimes = require('./mimes')

/**
 * 遍历读取目录内容(子目录,文件名)
 * @param  {string} reqPath 请求资源的绝对路径
 * @return {array} 目录内容列表
 */
function walk(reqPath) {
	let files = fs.readdirSync(reqPath)

	let dirList = [],
		fileList = []
	for (let i = 0, len = files.length; i < len; i++) {
		let item = files[i]
		let itemArr = item.split('.')
		let itemMime =
			itemArr.length > 1 ? itemArr[itemArr.length - 1] : 'undefined'

		if (typeof mimes[itemMime] === 'undefined') {
			dirList.push(files[i])
		} else {
			fileList.push(files[i])
		}
	}

	let result = dirList.concat(fileList)

	return result
}

module.exports = walk
//util/mime.js
let mimes = {
	css: 'text/css',
	less: 'text/css',
	gif: 'image/gif',
	html: 'text/html',
	ico: 'image/x-icon',
	jpeg: 'image/jpeg',
	jpg: 'image/jpeg',
	js: 'text/javascript',
	json: 'application/json',
	pdf: 'application/pdf',
	png: 'image/png',
	svg: 'image/svg+xml',
	swf: 'application/x-shockwave-flash',
	tiff: 'image/tiff',
	txt: 'text/plain',
	wav: 'audio/x-wav',
	wma: 'audio/x-ms-wma',
	wmv: 'video/x-ms-wmv',
	xml: 'text/xml',
}

module.exports = mimes

koa-static

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

app.use(static(path.join(__dirname, staticPath)))

// app.use( async ( ctx ) => {
//   ctx.body = 'hello world'
// })

app.listen(3000, () => {
	console.log('koa-static demo is starting at port 3000')
})

7.cookie/session

koa2 使用 cookie

koa 提供了从上下文直接读取、写入 cookie 的方法

  • ctx.cookies.get(name, [options]) 读取上下文请求中的 cookie

  • ctx.cookies.set(name, value, [options]) 在上下文中写入 cookie

koa2 中操作的 cookies 是使用了 npm 的 cookies 模块,所以在读写 cookie 的使用参数与该模块的使用一致

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
	if (ctx.url === '/index') {
		ctx.cookies.set('cid', 'hello world', {
			domain: 'localhost', // 写cookie所在的域名
			path: '/index', // 写cookie所在的路径
			maxAge: 10 * 60 * 1000, // cookie有效时长
			expires: new Date('2017-02-15'), // cookie失效时间
			httpOnly: false, // 是否只用于http请求中获取
			overwrite: false, // 是否允许重写
		})
		ctx.body = 'cookie is ok'
	} else {
		ctx.body = 'hello world'
	}
})

app.listen(3000, () => {
	console.log('cookie demo is starting at port 3000')
})

koa2 使用 session

  • 如果 session 数据量很小,可以直接存在内存中

  • 如果 session 数据量很大,则需要存储介质存放 session 数据

  • koa-session-minimal适用于 koa2 的 session 中间件,提供存储介质的读写接口

  • koa-mysql-session为koa-session-minimal中间件提供 MySQL 数据库的 session 数据读写操作

const Koa = require('koa')
//npm i koa-session-minimal
const session = require('koa-session-minimal')
//npm i koa-mysql-session
const MysqlSession = require('koa-mysql-session')

const app = new Koa()

// 配置存储session信息的mysql
let store = new MysqlSession({
	user: 'root',
	password: 'root',
	database: 'koa_demo',
	host: '127.0.0.1',
})

// 存放sessionId的cookie配置
let cookie = {
	maxAge: '', // cookie有效时长
	expires: '', // cookie失效时间
	path: '', // 写cookie所在的路径
	domain: '', // 写cookie所在的域名
	httpOnly: '', // 是否只用于http请求中获取
	overwrite: '', // 是否允许重写
	secure: '',
	sameSite: '',
	signed: '',
}

// 使用session中间件
app.use(
	session({
		key: 'SESSION_ID',
		store: store,
		cookie: cookie,
	})
)

app.use(async (ctx) => {
	// 设置session
	if (ctx.url === '/set') {
		ctx.session = {
			user_id: Math.random().toString(36).substr(2),
			count: 0,
		}
		ctx.body = ctx.session
	} else if (ctx.url === '/') {
		// 读取session信息
		ctx.session.count = ctx.session.count + 1
		ctx.body = ctx.session
	}
})

app.listen(3000)
console.log('session demo is starting at port 3000')

8.常用中间件

  • reify

  • koa-conditional-get

  • koa-etag

  • koa-bodyparser

  • koa-static

  • koa-views

  • koa-mount 通过 URL 挂载,将其他 Koa 实例挂在到一个主实例中

  • koa-connect 兼容 Express 中间件可以在 Koa 中使用

9.view

ejs

  1. 安装 koa 模板使用中间件npm i koa-views

//index.js
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
//入口文件同级存在view目录
app.use(
	views(path.join(__dirname, './view'), {
		extension: 'ejs',
	})
)

app.use(async (ctx) => {
	let title = 'hello koa2 template engine'
	await ctx.render('index', {
		title,
	})
})

app.listen(3000)
console.log('ejs view demo is starting at port 3000')
<!--./view/index.ejs -->
<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>

Nunjucks

//npm i koa-nunjucks-2
app.use(
	nunjucks({
		ext: 'html',
		path: path.join(__dirname, 'views'), // 指定视图目录
		nunjucksConfig: {
			trimBlocks: true, // 开启转义 防Xss
		},
	})
)

grammar

  • variable {{ username }} {{ foo.bar }} {{ foo["bar"] }} 如果变量的值为 undefined 或 null 不显示

  • filter {{ foo | title }} {{ foo | join(",") }} {{ foo | replace("foo", "bar") | capitalize }}

  • if

    <div data-gb-custom-block data-tag="if">
    
      It is true
    

- for

var items = [{ title: "foo", id: 1 }, { title: "bar", id: 2}]

Posts

```

  • macro

{{ field('user') }} {{ field('pass', type='password') }}


- ### 继承功能

//layout.html

  </head>
  <body>
    
  </body>
</html>

//home.html

```

 //最终的 home.html
 <html>
    <head>
      <link href="home.css">
    </head>
    <body>
      <h1>this is header</h1>

      <h1>home 页面内容</h1>

      <h1>this is footer</h1>

      <script src="home.js"></script>
    </body>
  </html>

10.upload

busboby

//npm i busboy用来解析POST请求,node原生req中的文件流
const inspect = require('util').inspect
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')

// req 为node原生请求
const busboy = new Busboy({ headers: req.headers })

// ...

// 监听文件解析事件
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
	console.log(`File [${fieldname}]: filename: ${filename}`)

	// 文件保存到特定路径
	file.pipe(fs.createWriteStream('./upload'))

	// 开始解析文件流
	file.on('data', function (data) {
		console.log(`File [${fieldname}] got ${data.length} bytes`)
	})

	// 解析文件结束
	file.on('end', function () {
		console.log(`File [${fieldname}] Finished`)
	})
})

// 监听请求中的字段
busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated) {
	console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
})

// 监听结束事件
busboy.on('finish', function () {
	console.log('Done parsing form!')
	res.writeHead(303, { Connection: 'close', Location: '/' })
	res.end()
})
req.pipe(busboy)

上传文件简单实现

//npm i busboy
const inspect = require('util').inspect
const path = require('path')
const os = require('os')
const fs = require('fs')
const Busboy = require('busboy')

/**
 * 同步创建文件目录
 * @param  {string} dirname 目录绝对地址
 * @return {boolean}        创建目录结果
 */
function mkdirsSync(dirname) {
	if (fs.existsSync(dirname)) {
		return true
	} else {
		if (mkdirsSync(path.dirname(dirname))) {
			fs.mkdirSync(dirname)
			return true
		}
	}
}

/**
 * 获取上传文件的后缀名
 * @param  {string} fileName 获取上传文件的后缀名
 * @return {string}          文件后缀名
 */
function getSuffixName(fileName) {
	let nameList = fileName.split('.')
	return nameList[nameList.length - 1]
}

/**
 * 上传文件
 * @param  {object} ctx     koa上下文
 * @param  {object} options 文件上传参数 fileType文件类型, path文件存放路径
 * @return {promise}
 */
function uploadFile(ctx, options) {
	let req = ctx.req
	let res = ctx.res
	let busboy = new Busboy({ headers: req.headers })

	// 获取类型
	let fileType = options.fileType || 'common'
	let filePath = path.join(options.path, fileType)
	let mkdirResult = mkdirsSync(filePath)

	return new Promise((resolve, reject) => {
		console.log('文件上传中...')
		let result = {
			success: false,
			formData: {},
		}

		// 解析请求文件事件
		busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
			let fileName =
				Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
			let _uploadFilePath = path.join(filePath, fileName)
			let saveTo = path.join(_uploadFilePath)

			// 文件保存到制定路径
			file.pipe(fs.createWriteStream(saveTo))

			// 文件写入事件结束
			file.on('end', function () {
				result.success = true
				result.message = '文件上传成功'

				console.log('文件上传成功!')
				resolve(result)
			})
		})

		// 解析表单中其他字段信息
		busboy.on(
			'field',
			function (
				fieldname,
				val,
				fieldnameTruncated,
				valTruncated,
				encoding,
				mimetype
			) {
				console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val))
				result.formData[fieldname] = inspect(val)
			}
		)

		// 解析结束事件
		busboy.on('finish', function () {
			console.log('文件上结束')
			resolve(result)
		})

		// 解析错误事件
		busboy.on('error', function (err) {
			console.log('文件上出错')
			reject(result)
		})

		req.pipe(busboy)
	})
}

module.exports = {
	uploadFile,
}
//index.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
// const bodyParser = require('koa-bodyparser')

const { uploadFile } = require('./util/upload')

// app.use(bodyParser())

app.use(async (ctx) => {
	if (ctx.url === '/' && ctx.method === 'GET') {
		// 当GET请求时候返回表单页面
		let html = `
      <h1>koa2 upload demo</h1>
      <form method="POST" action="/upload.json" enctype="multipart/form-data">
        <p>file upload</p>
        <span>picName:</span><input name="picName" type="text" /><br/>
        <input name="file" type="file" /><br/><br/>
        <button type="submit">submit</button>
      </form>
    `
		ctx.body = html
	} else if (ctx.url === '/upload.json' && ctx.method === 'POST') {
		// 上传文件请求处理
		let result = { success: false }
		let serverFilePath = path.join(__dirname, 'upload-files')

		// 上传文件事件
		result = await uploadFile(ctx, {
			fileType: 'album', // common or album
			path: serverFilePath,
		})

		ctx.body = result
	} else {
		// 其他请求显示404
		ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
	}
})

app.listen(3000, () => {
	console.log('upload-simple demo is starting at port 3000')
})

异步上传图片实现

//后端代码
//入口文件 demo/upload-async/index.js
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const convert = require('koa-convert')
const static = require('koa-static')
const { uploadFile } = require('./util/upload')

const app = new Koa()

app.use(
	views(path.join(__dirname, './view'), {
		extension: 'ejs',
	})
)

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

app.use(static(path.join(__dirname, staticPath)))

app.use(async (ctx) => {
	if (ctx.method === 'GET') {
		let title = 'upload pic async'
		await ctx.render('index', {
			title,
		})
	} else if (ctx.url === '/api/picture/upload.json' && ctx.method === 'POST') {
		// 上传文件请求处理
		let result = { success: false }
		let serverFilePath = path.join(__dirname, 'static/image')

		// 上传文件事件
		result = await uploadFile(ctx, {
			fileType: 'album',
			path: serverFilePath,
		})
		ctx.body = result
	} else {
		// 其他请求显示404
		ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
	}
})

app.listen(3000, () => {
	console.log('upload-pic-async demo is starting at port 3000')
})
//后端上传图片流写操作 入口文件 demo/upload-async/util/upload.js
const inspect = require('util').inspect
const path = require('path')
const os = require('os')
const fs = require('fs')
const Busboy = require('busboy')

/**
 * 同步创建文件目录
 * @param  {string} dirname 目录绝对地址
 * @return {boolean}        创建目录结果
 */
function mkdirsSync(dirname) {
	if (fs.existsSync(dirname)) {
		return true
	} else {
		if (mkdirsSync(path.dirname(dirname))) {
			fs.mkdirSync(dirname)
			return true
		}
	}
}

/**
 * 获取上传文件的后缀名
 * @param  {string} fileName 获取上传文件的后缀名
 * @return {string}          文件后缀名
 */
function getSuffixName(fileName) {
	let nameList = fileName.split('.')
	return nameList[nameList.length - 1]
}

/**
 * 上传文件
 * @param  {object} ctx     koa上下文
 * @param  {object} options 文件上传参数 fileType文件类型, path文件存放路径
 * @return {promise}
 */
function uploadFile(ctx, options) {
	let req = ctx.req
	let res = ctx.res
	let busboy = new Busboy({ headers: req.headers })

	// 获取类型
	let fileType = options.fileType || 'common'
	let filePath = path.join(options.path, fileType)
	let mkdirResult = mkdirsSync(filePath)

	return new Promise((resolve, reject) => {
		console.log('文件上传中...')
		let result = {
			success: false,
			message: '',
			data: null,
		}

		// 解析请求文件事件
		busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
			let fileName =
				Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
			let _uploadFilePath = path.join(filePath, fileName)
			let saveTo = path.join(_uploadFilePath)

			// 文件保存到制定路径
			file.pipe(fs.createWriteStream(saveTo))

			// 文件写入事件结束
			file.on('end', function () {
				result.success = true
				result.message = '文件上传成功'
				result.data = {
					pictureUrl: `//${ctx.host}/image/${fileType}/${fileName}`,
				}
				console.log('文件上传成功!')
				resolve(result)
			})
		})

		// 解析结束事件
		busboy.on('finish', function () {
			console.log('文件上结束')
			resolve(result)
		})

		// 解析错误事件
		busboy.on('error', function (err) {
			console.log('文件上出错')
			reject(result)
		})

		req.pipe(busboy)
	})
}

module.exports = {
	uploadFile,
}
<!-- 前端代码 -->
<button class="btn" id="J_UploadPictureBtn">上传图片</button>
<hr />
<p>上传进度<span id="J_UploadProgress">0</span>%</p>
<p>上传结果图片</p>
<div id="J_PicturePreview" class="preview-picture"></div>
<script src="/js/index.js"></script>
;(function () {
	let btn = document.getElementById('J_UploadPictureBtn')
	let progressElem = document.getElementById('J_UploadProgress')
	let previewElem = document.getElementById('J_PicturePreview')
	btn.addEventListener('click', function () {
		uploadAction({
			success: function (result) {
				console.log(result)
				if (result && result.success && result.data && result.data.pictureUrl) {
					previewElem.innerHTML =
						'<img src="' + result.data.pictureUrl + '" style="max-width: 100%">'
				}
			},
			progress: function (data) {
				if (data && data * 1 > 0) {
					progressElem.innerText = data
				}
			},
		})
	})

	/**
	 * 类型判断
	 * @type {Object}
	 */
	let UtilType = {
		isPrototype: function (data) {
			return Object.prototype.toString.call(data).toLowerCase()
		},

		isJSON: function (data) {
			return this.isPrototype(data) === '[object object]'
		},

		isFunction: function (data) {
			return this.isPrototype(data) === '[object function]'
		},
	}

	/**
	 * form表单上传请求事件
	 * @param  {object} options 请求参数
	 */
	function requestEvent(options) {
		try {
			let formData = options.formData
			let xhr = new XMLHttpRequest()
			xhr.onreadystatechange = function () {
				if (xhr.readyState === 4 && xhr.status === 200) {
					options.success(JSON.parse(xhr.responseText))
				}
			}

			xhr.upload.onprogress = function (evt) {
				let loaded = evt.loaded
				let tot = evt.total
				let per = Math.floor((100 * loaded) / tot)
				options.progress(per)
			}
			xhr.open('post', '/api/picture/upload.json')
			xhr.send(formData)
		} catch (err) {
			options.fail(err)
		}
	}

	/**
	 * 上传事件
	 * @param  {object} options 上传参数
	 */
	function uploadEvent(options) {
		let file
		let formData = new FormData()
		let input = document.createElement('input')
		input.setAttribute('type', 'file')
		input.setAttribute('name', 'files')

		input.click()
		input.onchange = function () {
			file = input.files[0]
			formData.append('files', file)

			requestEvent({
				formData,
				success: options.success,
				fail: options.fail,
				progress: options.progress,
			})
		}
	}

	/**
	 * 上传操作
	 * @param  {object} options 上传参数
	 */
	function uploadAction(options) {
		if (!UtilType.isJSON(options)) {
			console.log('upload options is null')
			return
		}
		let _options = {}
		_options.success = UtilType.isFunction(options.success)
			? options.success
			: function () {}
		_options.fail = UtilType.isFunction(options.fail)
			? options.fail
			: function () {}
		_options.progress = UtilType.isFunction(options.progress)
			? options.progress
			: function () {}

		uploadEvent(_options)
	}
})()

11.integrated with MySQL

mysql

//npm i mysql
const mysql = require('mysql')
const connection = mysql.createConnection({
	host: '127.0.0.1',
	user: 'root',
	password: 'root',
	database: 'test',
})

// 执行sql脚本对数据库进行读写
connection.query('SELECT * FROM my_table', (error, results, fields) => {
	if (error) throw error
	// connected!

	// 结束会话
	connection.release()
})

创建数据连接池

const mysql = require('mysql')

// 创建数据池
const pool = mysql.createPool({
	host: '127.0.0.1',
	user: 'root',
	password: 'root',
	database: 'test',
})

// 在数据池中进行会话操作
pool.getConnection(function (err, connection) {
	connection.query('SELECT * FROM my_table', (error, results, fields) => {
		// 结束会话
		connection.release()

		// 如果有错误就抛出
		if (error) throw error
	})
})

async/await 封装使用 mysql

const mysql = require('mysql')
const pool = mysql.createPool({
	host: '127.0.0.1',
	user: 'root',
	password: 'root',
	database: 'test',
})

let query = function (sql, values) {
	return new Promise((resolve, reject) => {
		pool.getConnection(function (err, connection) {
			if (err) {
				reject(err)
			} else {
				connection.query(sql, values, (err, rows) => {
					if (err) {
						reject(err)
					} else {
						resolve(rows)
					}
					connection.release()
				})
			}
		})
	})
}

module.exports = { query }
const { query } = require('./async-db')
async function selectAllData() {
	let sql = 'SELECT * FROM my_table'
	let dataList = await query(sql)
	return dataList
}

async function getData() {
	let dataList = await selectAllData()
	console.log(dataList)
}

建表初始化

//数据库操作文件 ./util/db.js
const mysql = require('mysql')

const pool = mysql.createPool({
	host: '127.0.0.1',
	user: 'root',
	password: 'abc123',
	database: 'koa_demo',
})

let query = function (sql, values) {
	return new Promise((resolve, reject) => {
		pool.getConnection(function (err, connection) {
			if (err) {
				reject(err)
			} else {
				connection.query(sql, values, (err, rows) => {
					if (err) {
						reject(err)
					} else {
						resolve(rows)
					}
					connection.release()
				})
			}
		})
	})
}

module.exports = {
	query,
}
//获取所有sql脚本内容 ./util/get-sql-content-map.js
const fs = require('fs')
const getSqlMap = require('./get-sql-map')

let sqlContentMap = {}

/**
 * 读取sql文件内容
 * @param  {string} fileName 文件名称
 * @param  {string} path     文件所在的路径
 * @return {string}          脚本文件内容
 */
function getSqlContent(fileName, path) {
	let content = fs.readFileSync(path, 'binary')
	sqlContentMap[fileName] = content
}

/**
 * 封装所有sql文件脚本内容
 * @return {object}
 */
function getSqlContentMap() {
	let sqlMap = getSqlMap()
	for (let key in sqlMap) {
		getSqlContent(key, sqlMap[key])
	}

	return sqlContentMap
}

module.exports = getSqlContentMap
//获取sql目录详情 ./util/get-sql-map.js
const fs = require('fs')
const walkFile = require('./walk-file')

/**
 * 获取sql目录下的文件目录数据
 * @return {object}
 */
function getSqlMap() {
	let basePath = __dirname
	basePath = basePath.replace(/\\/g, '/')

	let pathArr = basePath.split('/')
	pathArr = pathArr.splice(0, pathArr.length - 1)
	basePath = pathArr.join('/') + '/sql/'

	let fileList = walkFile(basePath, 'sql')
	return fileList
}

module.exports = getSqlMap
//遍历目录操作 ./util/walk-file.js
const fs = require('fs')

/**
 * 遍历目录下的文件目录
 * @param  {string} pathResolve  需进行遍历的目录路径
 * @param  {string} mime         遍历文件的后缀名
 * @return {object}              返回遍历后的目录结果
 */
const walkFile = function (pathResolve, mime) {
	let files = fs.readdirSync(pathResolve)

	let fileList = {}

	for (let [i, item] of files.entries()) {
		let itemArr = item.split('.')

		let itemMime =
			itemArr.length > 1 ? itemArr[itemArr.length - 1] : 'undefined'
		let keyName = item + ''
		if (mime === itemMime) {
			fileList[item] = pathResolve + item
		}
	}

	return fileList
}

module.exports = walkFile
//入口文件 ./index.js
const fs = require('fs')
const getSqlContentMap = require('./util/get-sql-content-map')
const { query } = require('./util/db')

// 打印脚本执行日志
const eventLog = function (err, sqlFile, index) {
	if (err) {
		console.log(
			`[ERROR] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行失败 o(╯□╰)o !`
		)
	} else {
		console.log(
			`[SUCCESS] sql脚本文件: ${sqlFile} 第${
				index + 1
			}条脚本 执行成功 O(∩_∩)O !`
		)
	}
}

// 获取所有sql脚本内容
let sqlContentMap = getSqlContentMap()

// 执行建表sql脚本
const createAllTables = async () => {
	for (let key in sqlContentMap) {
		let sqlShell = sqlContentMap[key]
		let sqlShellList = sqlShell.split(';')

		for (let [i, shell] of sqlShellList.entries()) {
			if (shell.trim()) {
				let result = await query(shell)
				if (result.serverStatus * 1 === 2) {
					eventLog(null, key, i)
				} else {
					eventLog(true, key, i)
				}
			}
		}
	}
	console.log('sql脚本执行结束!')
	console.log('请按 ctrl + c 键退出!')
}

createAllTables()
--sql脚本文件 ./sql/data.sql
CREATE TABLE   IF NOT EXISTS  `data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `data_info` json DEFAULT NULL,
  `create_time` varchar(20) DEFAULT NULL,
  `modified_time` varchar(20) DEFAULT NULL,
  `level` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
--sql脚本文件 ./sql/user.sql
CREATE TABLE   IF NOT EXISTS  `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `nick` varchar(255) DEFAULT NULL,
  `detail_info` json DEFAULT NULL,
  `create_time` varchar(20) DEFAULT NULL,
  `modified_time` varchar(20) DEFAULT NULL,
  `level` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` set email='1@example.com', password='123456';
INSERT INTO `user` set email='2@example.com', password='123456';
INSERT INTO `user` set email='3@example.com', password='123456';

12.jsonp

原生 koa2 实现 jsonp

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
	// 如果jsonp 的请求为GET
	if (ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
		// 获取jsonp的callback
		let callbackName = ctx.query.callback || 'callback'
		let returnData = {
			success: true,
			data: {
				text: 'this is a jsonp api',
				time: new Date().getTime(),
			},
		}

		// jsonp的script字符串
		let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`

		// 用text/javascript,让请求支持跨域获取
		ctx.type = 'text/javascript'

		// 输出jsonp字符串
		ctx.body = jsonpStr
	} else {
		ctx.body = 'hello jsonp'
	}
})

app.listen(3000, () => {
	console.log('jsonp demo is starting at port 3000')
})

//同域访问JSON请求
//http://localhost:3000/getData.jsonp

//跨域访问JSONP请求
$.ajax({
	url: 'http://localhost:3000/getData.jsonp',
	type: 'get',
	dataType: 'JSONP',
	success: function (res) {
		console.log(res)
	},
})

koa-jsonp

//npm i koa-jsonp
const Koa = require('koa')
const jsonp = require('koa-jsonp')
const app = new Koa()

// 使用中间件
app.use(jsonp())

app.use(async (ctx) => {
	let returnData = {
		success: true,
		data: {
			text: 'this is a jsonp api',
			time: new Date().getTime(),
		},
	}

	// 直接输出JSON
	ctx.body = returnData
})

app.listen(3000, () => {
	console.log('jsonp demo is starting at port 3000')
})

挂载 jsonp 返回格式到 ctx

module.exports = () => {
	function render(json) {
		this.set('Content-Type', 'application/json')
		this.body = JSON.stringify(json)
	}
	return async (ctx, next) => {
		ctx.send = render.bind(ctx)
		await next()
	}
}

ctx.send({
	status: 'success',
	data: 'hello ikcmap',
})

13.unit test

  • mocha 模块是测试框架

  • chai 模块是用来进行测试结果断言库,比如一个判断 1 + 1 是否等于 2

  • supertest 模块是 http 请求测试库,用来请求 API 接口

//npm i -D mocha chai supertest
const Koa = require('koa')
const app = new Koa()

const server = async (ctx, next) => {
	let result = {
		success: true,
		data: null,
	}

	if (ctx.method === 'GET') {
		if (ctx.url === '/getString.json') {
			result.data = 'this is string data'
		} else if (ctx.url === '/getNumber.json') {
			result.data = 123456
		} else {
			result.success = false
		}
		ctx.body = result
		next && next()
	} else if (ctx.method === 'POST') {
		if (ctx.url === '/postData.json') {
			result.data = 'ok'
		} else {
			result.success = false
		}
		ctx.body = result
		next && next()
	} else {
		ctx.body = 'hello world'
		next && next()
	}
}

app.use(server)

module.exports = app

app.listen(3000, () => {
	console.log('test-unit demo is starting at port 3000')
})
//test/app.test.js
const supertest = require('supertest')
const chai = require('chai')
const app = require('./../index')

const expect = chai.expect
//服务入口加载
const request = supertest(app.listen())

// 测试套件/组
describe('开始测试demo的GET请求', () => {
	// 测试用例
	it('测试/getString.json请求', (done) => {
		request
			.get('/getString.json')
			.expect(200)
			.end((err, res) => {
				// 断言判断结果是否为object类型
				expect(res.body).to.be.an('object')
				expect(res.body.success).to.be.an('boolean')
				expect(res.body.data).to.be.an('string')
				done()
			})
	})
})

14.log4js

log4js middleware

//npm i log4js
const log4js = require('log4js')
module.exports = (options) => {
	return async (ctx, next) => {
		const start = Date.now()
		log4js.configure({
			appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
			categories: { default: { appenders: ['cheese'], level: 'info' } },
		})
		const logger = log4js.getLogger('cheese')
		await next()
		const end = Date.now()
		const responseTime = end - start
		logger.info(`响应时间为${responseTime / 1000}s`)
	}
}

挂载应用日志到 ctx 上

const log4js = require('log4js')
const methods = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark']

module.exports = () => {
	const contextLogger = {}
	log4js.configure({
		appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
		categories: { default: { appenders: ['cheese'], level: 'info' } },
	})

	const logger = log4js.getLogger('cheese')

	return async (ctx, next) => {
		// 记录请求开始的时间
		const start = Date.now()
		// 循环methods将所有方法挂载到ctx 上
		methods.forEach((method, i) => {
			contextLogger[method] = (message) => {
				logger[method](message)
			}
		})
		ctx.log = contextLogger

		await next()
		// 记录完成的时间 作差 计算响应时间
		const responseTime = Date.now() - start
		logger.info(`响应时间为${responseTime / 1000}s`)
	}
}

抽出可配置量

丰富日志信息

项目自定义内容

对日志中间件进行错误处理

15.错误处理

抽取中间件并引入

渲染页面逻辑

16.规范与部署

  • nodemon自动重启(全局本地都安装)

  • pm2Node 应用的进程管理器

    npm i pm2 -g
    pm2 start app.js
PreviousexpressNextnode

Last updated 2 years ago

安装模板引擎npm i ejs

ejs