-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add server and client im project
- Loading branch information
Showing
15 changed files
with
336 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { Context } from '@/app/types/index'; | ||
|
||
export default class UserController { | ||
async login(ctx: Context): Promise<void> { | ||
try { | ||
const { username, password } = ctx.request.body; | ||
console.log('login:', username, password); | ||
} catch (error) { | ||
console.log('login:', error); | ||
// ctx.errorHandler({ error }); | ||
} | ||
} | ||
|
||
async list(ctx: Context): Promise<void> { | ||
try { | ||
const { limit, offset } = ctx.request.query; | ||
console.log('list:', limit, offset); | ||
} catch (error) { | ||
console.log('register:', error); | ||
// ctx.errorHandler({ error }); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,62 @@ | ||
import fs from 'node:fs'; | ||
import { createServer } from 'node:http'; | ||
import path, { resolve } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
import type { IncomingMessage, ServerResponse } from 'node:http'; | ||
import { readController, routing } from '@/app/routes'; | ||
import { noop } from 'ranuts'; | ||
import { routing } from '@/app/routes'; | ||
import { createContext, createController } from '@/app/lib/context'; | ||
import { LOCAL_URL, MIME_TYPES, PORT } from '@/app/lib/constant'; | ||
import type { Context } from '@/app/types/index'; | ||
|
||
readController().then((controller) => { | ||
const server = createServer((req: IncomingMessage, res: ServerResponse) => { | ||
const ctx: Context = { | ||
req, | ||
res, | ||
controller, | ||
path: req.url || '', | ||
}; | ||
routing(ctx); | ||
// if (req.url === '/home') { | ||
// res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
// res.end(JSON.stringify({ message: 'Hello, JSON!' })); | ||
// } else { | ||
// res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||
// res.end('404 Not Found\n'); | ||
// } | ||
}); | ||
// 静态文件目录 | ||
const createStaticDir = () => { | ||
const __filename = fileURLToPath(import.meta.url); | ||
const __dirname = path.dirname(__filename); | ||
return resolve(__dirname, 'public'); | ||
}; | ||
// 静态服务 | ||
const createStaticServer = | ||
(dirStatic: string) => | ||
(ctx: Context, callback = noop): void => { | ||
const { req, res } = ctx; | ||
const filePath = path.join(dirStatic, req.url || '/'); | ||
const extname = String(path.extname(filePath)).toLowerCase(); | ||
const contentType = MIME_TYPES[extname] || 'application/octet-stream'; | ||
fs.readFile(filePath, (error, content) => { | ||
if (error) { | ||
if (error.code === 'ENOENT') { | ||
callback(ctx); | ||
} else { | ||
res.writeHead(500); | ||
res.end(`Server Error: ${error.code}`); | ||
} | ||
} else { | ||
res.writeHead(200, { 'Content-Type': contentType }); | ||
res.end(content, 'utf-8'); | ||
} | ||
}); | ||
}; | ||
// controller 目录 | ||
const createControllerDir = () => { | ||
const __filename = fileURLToPath(import.meta.url); | ||
const __dirname = path.dirname(__filename); | ||
return resolve(__dirname, 'controllers'); | ||
}; | ||
|
||
const PORT = 3000; | ||
const dirController = createControllerDir(); | ||
|
||
createController(dirController).then((controller) => { | ||
const server = createServer((req: IncomingMessage, res: ServerResponse) => { | ||
createContext(req, res, controller).then((ctx: Context) => { | ||
// 匹配静态服务 | ||
createStaticServer(createStaticDir())(ctx, () => { | ||
// 没有静态服务再匹配路由 | ||
routing(ctx); | ||
}); | ||
}); | ||
}); | ||
server.listen(PORT, () => { | ||
console.log(`Server is running at http://localhost:${PORT}/`); | ||
console.log(`Server is running at ${LOCAL_URL}`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import fs from 'node:fs'; | ||
import type { IncomingMessage, ServerResponse } from 'node:http'; | ||
import type { Context, Controller, RequestBody } from '@/app/types/index'; | ||
import { TYPE_FUNCTION, TYPE_OPJECT } from '@/app/lib/constant'; | ||
import { handlerQueryParams } from '@/app/lib/request'; | ||
|
||
// 在 ctx 上挂载 controller | ||
export const createController = async (dir: string): Promise<Controller> => { | ||
const controller: Controller = {}; | ||
const fileList = fs.readdirSync(dir); | ||
for (const file of fileList) { | ||
if (file.length > 0 && file.startsWith('.')) continue; | ||
const { default: route } = await import(`${dir}/${file}`); | ||
const type = Reflect.toString.call(route); | ||
const [key, _] = file.split('.'); | ||
if (type === TYPE_OPJECT) controller[key] = route; | ||
if (type === TYPE_FUNCTION) controller[key] = new route(); | ||
} | ||
return controller; | ||
}; | ||
// 处理请求的 body 数据 | ||
export const createRequestBody = (ctx: Context): Promise<RequestBody> => { | ||
const { req } = ctx; | ||
return new Promise((resolve) => { | ||
let body = ''; | ||
req.on('data', (chunk) => { | ||
body += chunk.toString(); | ||
}); | ||
// 监听 end 事件,当数据接收完毕时触发 | ||
req.on('end', () => { | ||
// 解析 JSON 数据(如果请求体是 JSON 格式) | ||
if (!body) return resolve({}); | ||
try { | ||
const parsedBody = JSON.parse(body); | ||
ctx.request.body = parsedBody; | ||
resolve(parsedBody); | ||
} catch (error) { | ||
console.error('Error parsing JSON:', error); | ||
resolve({}); | ||
} | ||
}); | ||
}); | ||
}; | ||
// 初始化 ctx | ||
export const createContext = (req: IncomingMessage, res: ServerResponse, controller: Controller): Promise<Context> => { | ||
const { url = '' } = req; | ||
const [path] = url.split('?'); | ||
// 根据 url 处理请求的 params 和 query | ||
const { params, query } = handlerQueryParams(req); | ||
const request = { | ||
path, | ||
url, | ||
query, | ||
body: {}, | ||
params, | ||
}; | ||
const response = { | ||
setHeader: (name: string, value: string | number | readonly string[]): ServerResponse<IncomingMessage> => { | ||
return res.setHeader(name, value); | ||
}, | ||
write: (chunk: any, callback?: (error: Error | null | undefined) => void): boolean => { | ||
return res.write(chunk, callback); | ||
}, | ||
type: '', | ||
}; | ||
// 初始化 ctx | ||
const ctx: Context = { | ||
req, | ||
res, | ||
controller, | ||
request, | ||
response, | ||
}; | ||
return new Promise((resolve) => { | ||
// 处理请求的 body 数据 | ||
createRequestBody(ctx).then((body) => { | ||
ctx.request.body = body; | ||
resolve(ctx); | ||
}); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import url from 'node:url'; | ||
import type { UrlWithParsedQuery } from 'node:url'; | ||
import type { IncomingMessage } from 'node:http'; | ||
|
||
export function parseURL(requestUrl: string): { query: UrlWithParsedQuery['query']; params: Record<string, string> } { | ||
const parsedUrl = url.parse(requestUrl, true); | ||
const pathname = parsedUrl.pathname || ''; | ||
const query = parsedUrl.query; | ||
|
||
const params = extractParams(pathname, requestUrl); | ||
|
||
return { query, params }; | ||
} | ||
|
||
export function extractParams(pathname: string, pathPattern: string): Record<string, string> { | ||
const pathParts = pathname.split('/').filter(Boolean); | ||
const patternParts = pathPattern.split('/').filter(Boolean); | ||
const params: Record<string, string> = {}; | ||
|
||
for (let i = 0; i < patternParts.length; i++) { | ||
if (patternParts[i].startsWith(':')) { | ||
const paramName = patternParts[i].slice(1); | ||
params[paramName] = pathParts[i]; | ||
} | ||
} | ||
return params; | ||
} | ||
|
||
// // 示例使用 | ||
// const requestUrl = '/user/123?name=John'; | ||
// const pathPattern = '/user/:id'; | ||
|
||
// const result = parseURL(requestUrl, pathPattern); | ||
// console.log(result); | ||
|
||
// // 输出: | ||
// // { query: { name: 'John' }, params: { id: '123' } } | ||
|
||
export const handlerQueryParams = ( | ||
req: IncomingMessage, | ||
): { query: UrlWithParsedQuery['query']; params: Record<string, string> } => { | ||
const url = req.url || ''; | ||
if (url) { | ||
// 根据 url 处理请求的 params 和 query | ||
return parseURL(url); | ||
} | ||
return { params: {}, query: {} }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import type { Context } from '@/app/types/index'; | ||
// 没有匹配到的响应 | ||
export const notMatchResponse = (ctx: Context): void => { | ||
const { res } = ctx; | ||
res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||
res.end('404 Not Found\n'); | ||
}; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,27 @@ | ||
import path, { resolve } from 'node:path'; | ||
import fs from 'node:fs'; | ||
import { fileURLToPath } from 'node:url'; | ||
import { noop } from 'ranuts'; | ||
import routes from '@/app/router.config'; | ||
import type { Context, Controller } from '@/app/types/index'; | ||
import type { Context } from '@/app/types/index'; | ||
import { notMatchResponse } from '@/app/lib/response'; | ||
|
||
const regex = /#\/(.*?)\/#/; | ||
|
||
const __filename = fileURLToPath(import.meta.url); | ||
// 根据 url 处理请求的 params 和 query | ||
|
||
const __dirname = path.dirname(__filename); | ||
|
||
const dir = resolve(__dirname, 'controllers'); | ||
|
||
export const readController = async (): Promise<Controller> => { | ||
const controller: Controller = {}; | ||
const fileList = fs.readdirSync(dir); | ||
for (const file of fileList) { | ||
if (file.length > 0 && file.startsWith('.')) continue; | ||
const { default: route } = await import(`${dir}/${file}`); | ||
const type = Reflect.toString.call(route); | ||
const [key, _] = file.split('.'); | ||
if (type === '[object Object]') controller[key] = route; | ||
if (type === '[object Function]') controller[key] = new route(); | ||
} | ||
return controller; | ||
}; | ||
|
||
export const routing = async (ctx: Context): Promise<void> => { | ||
export const routing = (ctx: Context): void => { | ||
const { req, controller } = ctx; | ||
for (const [key, value] of Object.entries(routes)) { | ||
const [method, path] = key.split('=>'); | ||
// 验证路由是否合法 | ||
const [_, url] = new RegExp(regex).exec(path) || []; | ||
// 路由匹配 controller | ||
if (req.method === method.toUpperCase() && req.url && new RegExp(url).test(req.url)) { | ||
const [name, func] = value.split('#'); | ||
const handler = controller[name][func] || noop; | ||
handler(ctx); | ||
return; | ||
} else { | ||
// 处理匹配不上的情况 | ||
notMatchResponse(ctx); | ||
} | ||
} | ||
}; |
Oops, something went wrong.