Skip to content

Commit

Permalink
feat: add server and client im project
Browse files Browse the repository at this point in the history
  • Loading branch information
chaxus committed Dec 23, 2024
1 parent f54053b commit 1fa02ff
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 56 deletions.
15 changes: 8 additions & 7 deletions packages/im/app/controllers/home.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import type { Context } from 'koa';
import { getMime } from 'ranuts';
import { ssr } from '@/app/lib/vite';
import { HtmlWritable, getEnv } from '@/app/lib/index';
import { FORMAT, HTML_PATH_MAP, RENDER_PATH_MAP, TEMPLATE_REPLACE } from '@/app/lib/constant';
import type { Context } from '@/app/types/index';

const env = getEnv();

Expand All @@ -15,7 +15,7 @@ export default class ServerRender {
async index(ctx: Context): Promise<void> {
try {
const fsTemplate = fs.readFileSync(path.resolve(__dirname, HTML_PATH_MAP[env]), FORMAT);
const template = await ssr.transformIndexHtml(ctx.path, fsTemplate);
const template = await ssr.transformIndexHtml(ctx.request.path, fsTemplate);
const { render } = await ssr.ssrLoadModule(path.resolve(__dirname, RENDER_PATH_MAP[env]));
const writable = new HtmlWritable();
const stream = render(ctx, {
Expand All @@ -26,15 +26,16 @@ export default class ServerRender {
ctx.res.write('<h1>Something went wrong</h1>');
},
onError(error: Error) {
ctx.errorHandler({ error });
// ctx.errorHandler({ error });
console.log('home:', error);
},
onAllReady() {
const type = getMime(ctx.url);
const type = getMime(ctx.request.url) || 'text/html';
if (type) {
ctx.type = type;
ctx.response.setHeader('Content-Type', type);
} else {
ctx.res.setHeader('Content-Type', 'text/html');
ctx.res.setHeader('Transfer-Encoding', 'chunked');
ctx.response.setHeader('Content-Type', 'text/html');
ctx.response.setHeader('Transfer-Encoding', 'chunked');
}
},
});
Expand Down
23 changes: 23 additions & 0 deletions packages/im/app/controllers/user.ts
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 });
}
}
}
73 changes: 53 additions & 20 deletions packages/im/app/index.ts
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}`);
});
});
15 changes: 15 additions & 0 deletions packages/im/app/lib/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,18 @@ export const DEVELOPMENT_DIR = '../../dist/views/';

export const PORT = 30102;
export const LOCAL_URL = `http://localhost:${PORT}`;
export const TYPE_OPJECT = '[object Object]';
export const TYPE_FUNCTION = '[object Function]';

export const MIME_TYPES: Record<string, string> = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain',
};
81 changes: 81 additions & 0 deletions packages/im/app/lib/context.ts
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);
});
});
};
48 changes: 48 additions & 0 deletions packages/im/app/lib/request.ts
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: {} };
};
7 changes: 7 additions & 0 deletions packages/im/app/lib/response.ts
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');
};
Binary file added packages/im/app/public/chi_sim.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/im/app/router.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
const serverRender = {
// 获取主页
'get=>#/^(?!/api).*$/#': 'home#index',
// 用户
'post=>/api/user/login': 'user#login',
};

export default {
Expand Down
32 changes: 8 additions & 24 deletions packages/im/app/routes.ts
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);
}
}
};
Loading

0 comments on commit 1fa02ff

Please sign in to comment.