Skip to content

Commit

Permalink
feat: add ssr and sse project
Browse files Browse the repository at this point in the history
  • Loading branch information
chaxus committed Dec 23, 2024
1 parent e3c624b commit f54053b
Show file tree
Hide file tree
Showing 27 changed files with 912 additions and 0 deletions.
60 changes: 60 additions & 0 deletions packages/im/app/controllers/home.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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';

const env = getEnv();

const __dirname = path.dirname(fileURLToPath(import.meta.url));

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 { render } = await ssr.ssrLoadModule(path.resolve(__dirname, RENDER_PATH_MAP[env]));
const writable = new HtmlWritable();
const stream = render(ctx, {
onShellReady() {
stream.pipe(writable);
},
onShellError() {
ctx.res.write('<h1>Something went wrong</h1>');
},
onError(error: Error) {
ctx.errorHandler({ error });
},
onAllReady() {
const type = getMime(ctx.url);
if (type) {
ctx.type = type;
} else {
ctx.res.setHeader('Content-Type', 'text/html');
ctx.res.setHeader('Transfer-Encoding', 'chunked');
}
},
});
const writeableFinish = (): Promise<{ success: true; data: string }> => {
return new Promise((resolve, reject) => {
writable.on('finish', () => {
const stream = writable.getHtml();
const html = template.replace(TEMPLATE_REPLACE, stream);
resolve({ success: true, data: html });
});
writable.on('error', (error) => {
reject({ success: false, data: error });
});
});
};
const result = await writeableFinish();
ctx.res.end(result.data);
} catch (error) {
console.log('home:', error);
// ctx.errorHandler({ error });
}
}
}
29 changes: 29 additions & 0 deletions packages/im/app/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createServer } from 'node:http';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { readController, routing } from '@/app/routes';
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 PORT = 3000;

server.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}/`);
});
});
26 changes: 26 additions & 0 deletions packages/im/app/lib/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// transmitnotice
export const DDTOKEN = '4e6add99ad420323a61b0ad4caa4940ec2806670a20ff00d32adbe84b5346b6c';
export const ADMINISTRATOR = 1;
// serverRenderServer
const HTML_PATH_DEV = '../../views/index.html';
export const FORMAT = 'utf-8';
export const TEMPLATE_REPLACE = '<!--ssr-outlet-->';
const RENDER_PATH_DEV = '/client/server.tsx';
const HTML_PATH_PROD = '../../dist/client/views/index.html';
const RENDER_PATH_PROD = '../../dist/server/server.js';
export const HTML_PATH_MAP: Record<string, string> = {
dev: HTML_PATH_DEV,
prod: HTML_PATH_PROD,
};
export const RENDER_PATH_MAP: Record<string, string> = {
dev: RENDER_PATH_DEV,
prod: RENDER_PATH_PROD,
};
// serverRender
export const PRODUCTION = 'production';
// static server dir
export const PRODUCTION_DIR = '../../dist/';
export const DEVELOPMENT_DIR = '../../dist/views/';

export const PORT = 30102;
export const LOCAL_URL = `http://localhost:${PORT}`;
54 changes: 54 additions & 0 deletions packages/im/app/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Writable } from 'node:stream';

export const noop = (): void => {};

export class HtmlWritable extends Writable {
chunks: Uint8Array[];
html: string;
constructor() {
super();
this.chunks = [];
this.html = '';
}

getHtml(): string {
return this.html;
}

getFragment(): string {
return Buffer.concat(this.chunks).toString();
}

_write(chunk: Uint8Array, _: string, callback = noop): void {
this.chunks.push(chunk);
callback();
}

_final(callback = noop): void {
this.html = Buffer.concat(this.chunks).toString();
callback();
}
}

/**
* @description: Gets the current environment configuration
* @return {string}
*/
export const getEnv = (): string => {
const env = process.env.NODE_ENV;
switch (env) {
case 'development':
case 'dev':
case 'local':
return 'dev';
case 'test':
return 'test';
case 'staging':
return 'staging';
case 'production':
case 'prod':
return 'prod';
default:
return 'prod';
}
};
10 changes: 10 additions & 0 deletions packages/im/app/lib/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createServer } from 'vite';

const viteServer = async () => {
return await createServer({
server: { middlewareMode: true },
appType: 'custom',
});
};

export const ssr = await viteServer();
16 changes: 16 additions & 0 deletions packages/im/app/router.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* 路由配置: '${method}=>${path}: ${controller}#${controllerMethod}#${env}'
* method: 请求方法 GET|POST
* path: 请求链接 /api/index
* controller: 本次请求对应的处理 controller
* controllerMethod: 本次请求对应的 controller 中的具体的处理方法
* env: 该路由在什么环境中生效 local|test|staging|prod 不同环境的路由可以放到一起
*/
const serverRender = {
// 获取主页
'get=>#/^(?!/api).*$/#': 'home#index',
};

export default {
...serverRender,
};
43 changes: 43 additions & 0 deletions packages/im/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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';

const regex = /#\/(.*?)\/#/;

const __filename = fileURLToPath(import.meta.url);

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> => {
const { req, controller } = ctx;
for (const [key, value] of Object.entries(routes)) {
const [method, path] = key.split('=>');
// 验证路由是否合法
const [_, url] = new RegExp(regex).exec(path) || [];
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;
}
}
};
10 changes: 10 additions & 0 deletions packages/im/app/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { IncomingMessage, ServerResponse } from 'node:http';

export type Controller = Record<string, Record<string, Function>>;

export interface Context {
req: IncomingMessage;
res: ServerResponse<IncomingMessage>;
controller: Controller;
path: string;
}
6 changes: 6 additions & 0 deletions packages/im/bin/dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@


bin=./node_modules/.bin
$bin/tsx ./app/index.ts


26 changes: 26 additions & 0 deletions packages/im/client/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { JSX } from 'react';
import React, { createContext, useState } from 'react';
import { useRoutes } from 'react-router-dom';
import routes from '@/client/router';
import '@/client/assets/base.css';

const Context = createContext({});

const { Provider } = Context;

const Noop = () => <></>;

function RoutesContent(): JSX.Element {
return useRoutes(routes) || <Noop />;
}

const App = (): JSX.Element => {
const [state, setState] = useState({});
return (
<Provider value={{ state, setState }}>
<RoutesContent />
</Provider>
);
};

export default App;
Loading

0 comments on commit f54053b

Please sign in to comment.