diff --git a/examples/client-mock/backend/.gitignore b/examples/client-mock/backend/.gitignore new file mode 100644 index 0000000..d84f0da --- /dev/null +++ b/examples/client-mock/backend/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.DS_STORE \ No newline at end of file diff --git a/examples/client-mock/backend/.vscode/launch.json b/examples/client-mock/backend/.vscode/launch.json new file mode 100644 index 0000000..9ba4218 --- /dev/null +++ b/examples/client-mock/backend/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "mocha current file", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "${file}" + ], + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}" + }, + { + "type": "node", + "request": "launch", + "name": "ts-node current file", + "protocol": "inspector", + "args": [ + "${relativeFile}" + ], + "cwd": "${workspaceRoot}", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "internalConsoleOptions": "openOnSessionStart" + } + ] +} \ No newline at end of file diff --git a/examples/client-mock/backend/.vscode/settings.json b/examples/client-mock/backend/.vscode/settings.json new file mode 100644 index 0000000..00ad71f --- /dev/null +++ b/examples/client-mock/backend/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/examples/client-mock/backend/README.md b/examples/client-mock/backend/README.md new file mode 100644 index 0000000..7492c22 --- /dev/null +++ b/examples/client-mock/backend/README.md @@ -0,0 +1,35 @@ +# TSRPC Server + +## Run +### Local Dev Server +``` +npm run dev +``` + + + +### Build +``` +npm run build +``` + +--- + +## Files +### Generate ServiceProto +``` +npm run proto +``` + +### Generate API templates +``` +npm run api +``` + +### Sync shared code to client + +``` +npm run sync +``` + +> If you chose symlink when using `create-tsrpc-app`, it would re-create the symlink instead of copy files. diff --git a/examples/client-mock/backend/package.json b/examples/client-mock/backend/package.json new file mode 100644 index 0000000..d9e35bd --- /dev/null +++ b/examples/client-mock/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "client-mock-backend", + "version": "0.1.0", + "main": "index.js", + "private": true, + "scripts": { + "proto": "tsrpc proto -i src/shared/protocols -o src/shared/protocols/serviceProto.ts", + "sync": "tsrpc sync --from src/shared --to ../frontend/src/shared", + "api": "tsrpc api -i src/shared/protocols/serviceProto.ts -o src/api", + "dev": "onchange \"src/**/*.ts\" -i -k -- ts-node \"src/index.ts\"", + "build": "tsrpc build" + }, + "devDependencies": { + "@types/node": "^15.12.5", + "onchange": "^7.1.0", + "ts-node": "^10.0.0", + "tsrpc-cli": "^2.0.3", + "typescript": "^4.3.4" + }, + "dependencies": { + "tsrpc": "^3.0.4" + } +} diff --git a/examples/client-mock/backend/src/api/ApiAddData.ts b/examples/client-mock/backend/src/api/ApiAddData.ts new file mode 100644 index 0000000..c064789 --- /dev/null +++ b/examples/client-mock/backend/src/api/ApiAddData.ts @@ -0,0 +1,26 @@ +import { ApiCall } from "tsrpc"; +import { ReqAddData, ResAddData } from "../shared/protocols/PtlAddData"; +import { AllData } from "./ApiGetData"; + +// This is a demo code file +// Feel free to delete it + +export async function ApiAddData(call: ApiCall) { + // Error + if (call.req.content === '') { + call.error('Content is empty'); + return; + } + + let time = new Date(); + AllData.unshift({ + content: call.req.content, + time: time + }) + console.log('AllData', AllData) + + // Success + call.succ({ + time: time + }); +} \ No newline at end of file diff --git a/examples/client-mock/backend/src/api/ApiGetData.ts b/examples/client-mock/backend/src/api/ApiGetData.ts new file mode 100644 index 0000000..2996ee4 --- /dev/null +++ b/examples/client-mock/backend/src/api/ApiGetData.ts @@ -0,0 +1,13 @@ +import { ApiCall } from "tsrpc"; +import { ReqGetData, ResGetData } from "../shared/protocols/PtlGetData"; + +// This is a demo code file +// Feel free to delete it + +export async function ApiGetData(call: ApiCall) { + call.succ({ + data: AllData + }) +} + +export const AllData: { content: string, time: Date }[] = []; \ No newline at end of file diff --git a/examples/client-mock/backend/src/index.ts b/examples/client-mock/backend/src/index.ts new file mode 100644 index 0000000..7dcc249 --- /dev/null +++ b/examples/client-mock/backend/src/index.ts @@ -0,0 +1,26 @@ +import * as path from "path"; +import { HttpServer } from "tsrpc"; +import { serviceProto } from "./shared/protocols/serviceProto"; + +// Create the Server +const server = new HttpServer(serviceProto, { + port: 3000, + cors: '*' +}); + +// Entry function +async function main() { + // Auto implement APIs + await server.autoImplementApi(path.resolve(__dirname, 'api')); + + // TODO + // Prepare something... (e.g. connect the db) + + await server.start(); +}; + +main().catch(e => { + // Exit if any error during the startup + server.logger.error(e); + process.exit(-1); +}); \ No newline at end of file diff --git a/examples/client-mock/backend/src/shared/protocols/PtlAddData.ts b/examples/client-mock/backend/src/shared/protocols/PtlAddData.ts new file mode 100644 index 0000000..7f442ae --- /dev/null +++ b/examples/client-mock/backend/src/shared/protocols/PtlAddData.ts @@ -0,0 +1,10 @@ +// This is a demo code file +// Feel free to delete it + +export interface ReqAddData { + content: string; +} + +export interface ResAddData { + time: Date +} \ No newline at end of file diff --git a/examples/client-mock/backend/src/shared/protocols/PtlGetData.ts b/examples/client-mock/backend/src/shared/protocols/PtlGetData.ts new file mode 100644 index 0000000..8ac35a7 --- /dev/null +++ b/examples/client-mock/backend/src/shared/protocols/PtlGetData.ts @@ -0,0 +1,13 @@ +// This is a demo code file +// Feel free to delete it + +export interface ReqGetData { + +} + +export interface ResGetData { + data: { + content: string, + time: Date + }[] +} \ No newline at end of file diff --git a/examples/client-mock/backend/src/shared/protocols/serviceProto.ts b/examples/client-mock/backend/src/shared/protocols/serviceProto.ts new file mode 100644 index 0000000..e7207a9 --- /dev/null +++ b/examples/client-mock/backend/src/shared/protocols/serviceProto.ts @@ -0,0 +1,98 @@ +import { ServiceProto } from 'tsrpc-proto'; +import { ReqAddData, ResAddData } from './PtlAddData'; +import { ReqGetData, ResGetData } from './PtlGetData'; + +// This is a demo service proto file (auto generated) +// Feel free to delete it + +export interface ServiceType { + api: { + "AddData": { + req: ReqAddData, + res: ResAddData + }, + "GetData": { + req: ReqGetData, + res: ResGetData + } + }, + msg: { + + } +} + +export const serviceProto: ServiceProto = { + "version": 1, + "services": [ + { + "id": 0, + "name": "AddData", + "type": "api" + }, + { + "id": 1, + "name": "GetData", + "type": "api" + } + ], + "types": { + "PtlAddData/ReqAddData": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "content", + "type": { + "type": "String" + } + } + ] + }, + "PtlAddData/ResAddData": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "time", + "type": { + "type": "Date" + } + } + ] + }, + "PtlGetData/ReqGetData": { + "type": "Interface" + }, + "PtlGetData/ResGetData": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "data", + "type": { + "type": "Array", + "elementType": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "content", + "type": { + "type": "String" + } + }, + { + "id": 1, + "name": "time", + "type": { + "type": "Date" + } + } + ] + } + } + } + ] + } + } +}; \ No newline at end of file diff --git a/examples/client-mock/backend/tsconfig.json b/examples/client-mock/backend/tsconfig.json new file mode 100644 index 0000000..d18498f --- /dev/null +++ b/examples/client-mock/backend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "lib": [ + "es2018" + ], + "module": "commonjs", + "target": "es2018", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/examples/client-mock/frontend/.gitignore b/examples/client-mock/frontend/.gitignore new file mode 100644 index 0000000..ae16b5a --- /dev/null +++ b/examples/client-mock/frontend/.gitignore @@ -0,0 +1,3 @@ +.DS_STORE +node_modules +dist \ No newline at end of file diff --git a/examples/client-mock/frontend/package.json b/examples/client-mock/frontend/package.json new file mode 100644 index 0000000..e44937d --- /dev/null +++ b/examples/client-mock/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "client-mock-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "webpack serve --mode=development --open", + "build": "webpack --mode=production" + }, + "devDependencies": { + "copy-webpack-plugin": "^9.0.1", + "html-webpack-plugin": "^5.3.2", + "ts-loader": "^9.2.3", + "typescript": "^4.3.4", + "webpack": "^5.41.1", + "webpack-cli": "^4.7.2", + "webpack-dev-server": "^3.11.2" + }, + "dependencies": { + "tsrpc-browser": "^3.0.4" + }, + "browserslist": [ + "defaults" + ] +} diff --git a/examples/client-mock/frontend/public/favicon.ico b/examples/client-mock/frontend/public/favicon.ico new file mode 100644 index 0000000..361535c Binary files /dev/null and b/examples/client-mock/frontend/public/favicon.ico differ diff --git a/examples/client-mock/frontend/public/index.css b/examples/client-mock/frontend/public/index.css new file mode 100644 index 0000000..bdef476 --- /dev/null +++ b/examples/client-mock/frontend/public/index.css @@ -0,0 +1,83 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + text-align: center; +} + +body>* { + margin: 20px auto; +} + +.send, +.list { + width: 370px; +} + +.send>* { + display: block; + width: 100%; + margin: 10px auto; + padding: 10px; + border-radius: 5px; + font-size: 16px; + border: none; + outline: none; +} + +.send>textarea { + height: 80px; + background: #f7f7f7; + border: #eeeeee 1px solid; +} + +.send>textarea:focus { + background: #fff; + border-color: #ccc; +} + +.send>button { + background: #215fa4; + color: white; + cursor: pointer; +} + +.send>button:hover { + background: #4b80bb; +} + +.list { + list-style: none; + border-radius: 5px; + padding: 10px; + background: #f2f2f2; +} + +.list>li { + margin-bottom: 10px; + padding: 10px; + background: #fff; + line-height: 1.5em; + border-radius: 5px; +} + +.list>li>.content { + font-size: 14px; + text-align: left; + white-space: pre-wrap; + word-wrap: break-word; +} + +.list>li>.time { + font-size: 12px; + color: #4b80bb; + text-align: right; +} + +.list>li:last-child { + border-bottom: none; + margin-bottom: 0; +} \ No newline at end of file diff --git a/examples/client-mock/frontend/public/index.html b/examples/client-mock/frontend/public/index.html new file mode 100644 index 0000000..8ac22f5 --- /dev/null +++ b/examples/client-mock/frontend/public/index.html @@ -0,0 +1,28 @@ + + + + + + + + TSRPC Browser + + + + +

TSRPC Guestbook

+ +
+ + +
+ +
    + +
+ + + \ No newline at end of file diff --git a/examples/client-mock/frontend/src/index.ts b/examples/client-mock/frontend/src/index.ts new file mode 100644 index 0000000..6174ab3 --- /dev/null +++ b/examples/client-mock/frontend/src/index.ts @@ -0,0 +1,100 @@ +import { HttpClient, TsrpcError } from 'tsrpc-browser'; +import { serviceProto } from './shared/protocols/serviceProto'; + +// This is a demo code file +// Feel free to modify or clear it + +// Create Client +let client = new HttpClient(serviceProto, { + server: 'http://127.0.0.1:3000', + logger: console +}); + +// Mock 后端逻辑临时存储数据 +let data: { + content: string, + time: Date +}[] = []; + +// Client Mock +client.flows.preCallApiFlow.push(v => { + // 模拟网络错误:20% 网络错误概率 + if (Math.random() < 0.2) { + // 模拟返回 + v.return = { + isSucc: false, + err: new TsrpcError('模拟网络错误: ' + v.apiName, { + type: TsrpcError.Type.NetworkError + }) + } + return v; + } + + // 模拟后端逻辑, Mock 返回结果,实际不请求后端 + client.logger?.log('[MockReq]', v.apiName, v.req); + if (v.apiName === 'AddData') { + let time = new Date(); + data.unshift({ content: v.req.content, time: time }) + v.return = { + isSucc: true, + res: { time: time } + } + } + else if (v.apiName === 'GetData') { + v.return = { + isSucc: true, + res: { + data: data + } + } + } + client.logger?.log('[MockRes]', v.apiName, v.return); + + return v; +}) + +// Reload message list +async function loadList() { + let ret = await client.callApi('GetData', {}); + + // Error + if (!ret.isSucc) { + alert(ret.err.message); + return; + } + + // Success + const list = document.querySelector('.list')!; + list.innerHTML = ''; + ret.res.data.forEach(v => { + let li = document.createElement('li'); + li.innerHTML = `
`; + (li.querySelector('.content') as HTMLDivElement).innerText = v.content; + (li.querySelector('.time') as HTMLDivElement).innerText = v.time.toLocaleTimeString(); + list.appendChild(li); + }) +} + +// Send Message +async function send() { + const textarea = document.querySelector('.send>textarea') as HTMLTextAreaElement; + let ret = await client.callApi('AddData', { + content: textarea.value + }); + + // Error + if (!ret.isSucc) { + alert(ret.err.message); + return; + } + + // Success + textarea.value = ''; + loadList(); +} + +// Bind Events +(document.querySelector('.send>button') as HTMLButtonElement).onclick = send; + +// Load list after page load +loadList(); \ No newline at end of file diff --git a/examples/client-mock/frontend/src/shared/protocols/PtlAddData.ts b/examples/client-mock/frontend/src/shared/protocols/PtlAddData.ts new file mode 100644 index 0000000..7f442ae --- /dev/null +++ b/examples/client-mock/frontend/src/shared/protocols/PtlAddData.ts @@ -0,0 +1,10 @@ +// This is a demo code file +// Feel free to delete it + +export interface ReqAddData { + content: string; +} + +export interface ResAddData { + time: Date +} \ No newline at end of file diff --git a/examples/client-mock/frontend/src/shared/protocols/PtlGetData.ts b/examples/client-mock/frontend/src/shared/protocols/PtlGetData.ts new file mode 100644 index 0000000..8ac35a7 --- /dev/null +++ b/examples/client-mock/frontend/src/shared/protocols/PtlGetData.ts @@ -0,0 +1,13 @@ +// This is a demo code file +// Feel free to delete it + +export interface ReqGetData { + +} + +export interface ResGetData { + data: { + content: string, + time: Date + }[] +} \ No newline at end of file diff --git a/examples/client-mock/frontend/src/shared/protocols/serviceProto.ts b/examples/client-mock/frontend/src/shared/protocols/serviceProto.ts new file mode 100644 index 0000000..e7207a9 --- /dev/null +++ b/examples/client-mock/frontend/src/shared/protocols/serviceProto.ts @@ -0,0 +1,98 @@ +import { ServiceProto } from 'tsrpc-proto'; +import { ReqAddData, ResAddData } from './PtlAddData'; +import { ReqGetData, ResGetData } from './PtlGetData'; + +// This is a demo service proto file (auto generated) +// Feel free to delete it + +export interface ServiceType { + api: { + "AddData": { + req: ReqAddData, + res: ResAddData + }, + "GetData": { + req: ReqGetData, + res: ResGetData + } + }, + msg: { + + } +} + +export const serviceProto: ServiceProto = { + "version": 1, + "services": [ + { + "id": 0, + "name": "AddData", + "type": "api" + }, + { + "id": 1, + "name": "GetData", + "type": "api" + } + ], + "types": { + "PtlAddData/ReqAddData": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "content", + "type": { + "type": "String" + } + } + ] + }, + "PtlAddData/ResAddData": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "time", + "type": { + "type": "Date" + } + } + ] + }, + "PtlGetData/ReqGetData": { + "type": "Interface" + }, + "PtlGetData/ResGetData": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "data", + "type": { + "type": "Array", + "elementType": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "content", + "type": { + "type": "String" + } + }, + { + "id": 1, + "name": "time", + "type": { + "type": "Date" + } + } + ] + } + } + } + ] + } + } +}; \ No newline at end of file diff --git a/examples/client-mock/frontend/tsconfig.json b/examples/client-mock/frontend/tsconfig.json new file mode 100644 index 0000000..f7f1d7c --- /dev/null +++ b/examples/client-mock/frontend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "es2015" + ], + "module": "esnext", + "target": "es5", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "outDir": "dist", + "skipLibCheck": true, + "strict": true, + "jsx": "react-jsx", + "sourceMap": true, + "isolatedModules": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/examples/client-mock/frontend/webpack.config.js b/examples/client-mock/frontend/webpack.config.js new file mode 100644 index 0000000..b0d3703 --- /dev/null +++ b/examples/client-mock/frontend/webpack.config.js @@ -0,0 +1,56 @@ +const webpack = require('webpack'); +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const isProduction = process.argv.indexOf('--mode=production') > -1; + +module.exports = { + entry: './src/index.ts', + output: { + filename: 'bundle.[contenthash].js', + path: path.resolve(__dirname, 'dist'), + clean: true + }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.mjs', '.cjs'] + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: [{ + loader: 'ts-loader', + options: { + // Compile to ES5 in production mode for better compatibility + // Compile to ES2018 in development for better debugging (like async/await) + compilerOptions: !isProduction ? { + "target": "es2018", + } : undefined + } + }], + exclude: /node_modules/ + }, + ] + }, + plugins: [ + // Copy "public" to "dist" + new CopyWebpackPlugin({ + patterns: [{ + from: 'public', + to: '.', + toType: 'dir', + globOptions: { + gitignore: true, + ignore: [path.resolve(__dirname, 'public/index.html').replace(/\\/g, '/')] + }, + noErrorOnMissing: true + }] + }), + // Auto add