简易的服务端模型
description: 编写一个简单的服务端框架,具备的功能展示html页面,静态资源,展示文件上传和下载 tags:
- 服务端
本文将介绍如何编写一个简单的服务端框架,具备以下功能:
- 展示 HTML 页面
- 提供静态资源
- 支持文件上传和下载
- Nodejs
- Nodejs http 模块
- art-template 提供html 模板语法
- TypeScript 编写
项目使用node http 模块作为服务端启动,当收到 对应的url 请求时候转发到对应处理的中间件函数,中间件函数用来定义可以处理的请求链接访问类型来进行逻辑处理的操作,这些处理包含 静态资源处理、文件上传下载的处理、页面展示的处理、页面使用模板语法的处理
.
├── nodemon.json
├── package-lock.json
├── package.json
├── src
│ ├── index.ts
│ ├── middle
│ │ ├── aboutPage.ts
│ │ ├── errorPage.ts
│ │ ├── index.ts
│ │ └── indexPage.ts
│ ├── public
│ │ ├── css
│ │ │ └── index.css
│ │ └── image
│ │ └── 12.png
│ ├── router.ts
│ ├── setting
│ │ └── index.ts
│ ├── types
│ │ └── glohal.d.ts
│ └── utils
│ └── index.ts
├── views
│ ├── about.html
│ ├── error.html
│ └── index.html
└── tsconfig.json
router.ts 作为路由文件作用是让路由和中间件进行匹配,做到分发的作用
import { indexPage, errorPage, aboutPage } from './middle'
/**
* @description: 导出路由配置,
*
*/
export default {
'/': {
page: 'index.html', // 渲染html
serverRequest: indexPage, // 中间层
},
'/about': {
page: 'about.html',
serverRequest: aboutPage,
},
error: {
page: 'error.html',
serverRequest: errorPage,
},
}
http.createServer 创建了node 服务监听访问端口,用来获取请求信息,例如请求地址,用来将响应信息发送会用户客户端
在这里我们进行路由匹配,配置在 router.ts 存在的地址我们将调用他的 serverRequest 指定中间层的方法,如果访问过来的是我们指定暴露的静态文件资源的地址,将作为静态资源获取后返回,如果地址不存在或者程序异常将会分配到特定的页面1
import http from 'http'
import router from './router'
import { STATIC } from './setting'
import { renderStatic } from './utils'
const server = http.createServer((req, res) => {
try {
const { url } = req
if (url && Reflect.has(router, url)) {
// 路由符合页面的存在
const { serverRequest, page } = Reflect.get(router, url)
serverRequest(req, res, page)
} else if (url && url.indexOf(`/${STATIC}/`) === 0) {
// 静态文件
renderStatic(url).pipe(res)
} else {
// 路由不存在的话 跳转错误页面
const { serverRequest, page } = router['error']
serverRequest(req, res, page)
}
} catch {
const { serverRequest, page } = router['error']
serverRequest(req, res, page)
}
})
server.listen('8080', () => {
console.log('启动服务')
})
中间层用来处理对应连接逻辑这里用 middle/aboutPage.ts 为例接受了 处理请求的对象 处理响应的对象 和 router.ts 中映射路由对应的html 的page字段作为参数
注意这里有个 renderArtTemplate 方法用来读取我们在 views 文件映射的 html 内容将最后读取完毕的内容回调后内容我们使用了 artTemplate 模板语法进行了代码渲染
再用处理响应的对象end 方法将内容发送到客户端
import http from 'http'
import { renderArtTemplate } from '../utils'
import artTemplate from 'art-template'
/**
* @name:aboutPage
* @description: url /about 路由映射
*
*/
const aboutPage = (
req: http.IncomingMessage,
res: http.ServerResponse,
pageSrc: string
) => {
const { method } = req
console.log(method)
if (method === 'GET') {
// 将读取的html 写入到返回的响应
renderArtTemplate(pageSrc, (data) => {
const html = artTemplate.render(data.toString(), {
name: 'Jack',
age: 18,
province: 'dl',
hobbies: ['写代码', '唱歌', '打游戏'],
title: '个人信息',
})
res.end(html)
})
}
}
export default aboutPage
view/about.html 为例写好了 artTemplate 的模板语法,只要读取后渲染即可
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{{ title }}</title>
</head>
<body>
<p>大家好,我叫:{{ name }}</p>
<p>我今年 {{ age }} 岁了</p>
<h1>我来自 {{ province }}</h1>
<p>我喜欢:{{each hobbies}} {{ $value }} {{/each}}</p>
<script>
var foo = '{{ title }}'
</script>
</body>
</html>
提供了读取文件的形式,使用几种方式去读取指定的静态文件资源
import { createReadStream, readFileSync } from 'fs'
import path from 'path'
import { PAGE_VIEW_ROOT, ROOT } from '../setting'
const rootPath = process.cwd()
/**
* @name: renderHtml
* @description: 读取html
*
*/
export function renderHtml(src: string) {
const fullPath = path.join(rootPath, ROOT, PAGE_VIEW_ROOT, src)
return createReadStream(fullPath)
}
/**
* @name: renderArtTemplate
* @description: 读取html art 渲染
*
*/
export function renderArtTemplate(
src: string,
callback: (data: Buffer) => void
) {
const data: Buffer[] = []
const rh = renderHtml(src)
rh.on('data', (chunk) => {
data.push(chunk as Buffer)
})
rh.on('end', () => {
callback?.(Buffer.concat(data))
})
}
/**
* @name: asyncRenderArtTemplate
* @description: 同步读取html
*
*/
export async function asyncRenderArtTemplate(src: string) {
const fullPath = path.join(rootPath, ROOT, PAGE_VIEW_ROOT, src)
return await readFileSync(fullPath)
}
/**
* @name: renderStatic
* @description: image css js 等其他静态文件
*
*/
export function renderStatic(src: string) {
const fullPath = path.join(rootPath, ROOT, src)
return createReadStream(fullPath)
}
-
使用 FormData 格式进行上传和下载。这是最常用的方法,可以方便地将文件和其他表单数据一起发送到服务器,并且可以使用 fetch API 进行异步请求。
-
使用 Base64 编码或二进制数据将文件包含在 JSON 对象中进行上传和下载。这种方法可能会导致数据大小增加,并且可能需要更多的处理步骤。
-
对于下载,可以使用 Blob 格式来接收响应数据。在这种情况下,响应数据可以是任何格式,例如 JSON、XML 或文本。但是,如果要下载的文件非常大,可能需要使用流式传输来避免内存问题。
-
使用 WebSocket 进行上传和下载。这种方法可以实现实时上传和下载,并且可以在上传和下载过程中进行流式处理。
-
使用 WebRTC 进行上传和下载。这种方法可以实现点对点的文件传输,可以在不同的浏览器之间进行文件传输。但是,这种方法需要建立一个对等连接,并且需要处理 NAT 穿透等问题。
这里我们选用的 FormData 形式来编写,在 view/upload 页面增加一个上传按钮页面,在路由映射位置 这个页面并且 增加一个接受上传逻辑的映射
{
'/upload': {
page: 'upload.html',
serverRequest: uploadPage,
},
'/upload/file': {
// page: 'upload.html',
serverRequest: uploadFile,
},
}
主要看一下 middle/uploadFile 这个逻辑处理,通过获取请求头中的数据,将格式通过二进制读取出来,获取到了 fromData 格式,我们在这个规律的格式中获取图片信息写入到服务器中
import http from 'http'
import fs from 'fs'
/**
* @name:uploadPage
* @description: url /uploadPage 路由映射
*
*/
const uploadFile = (
req: http.IncomingMessage,
res: http.ServerResponse,
pageSrc: string
) => {
const { method } = req
console.log(method)
if (method === 'POST') {
// 读取图片
req.setEncoding('binary') // 二进制将数据读取出来
const boundary = req.headers['content-type']
?.split('; ')[1]
.replace('boundary=', '')
let formData = ''
// 获取请求 体中的数据
req.on('data', (chunk) => {
formData += chunk
})
// 处理formdata 格式数据
req.on('end', () => {
// 1.截图从image/jpeg位置开始后面所有的数据
const imgType = 'image/jpeg'
const imageTypePosition = formData.indexOf(imgType) + imgType.length
let imageData = formData.substring(imageTypePosition)
console.log(123)
// 2.imageData开始位置会有两个空格
imageData = imageData.replace(/^\s\s*/, '')
// 3.替换最后的boundary
imageData = imageData.substring(0, imageData.indexOf(`--${boundary}--`))
// 4.将imageData的数据存储到文件中
fs.writeFile('./bar.png', imageData, 'binary', () => {
console.log('文件存储成功')
res.end('文件上传成功~')
})
})
}
}
export default uploadFile