Skip to content

Commit 4623ad6

Browse files
author
Junhui Tong
committed
init commit
0 parents  commit 4623ad6

File tree

13 files changed

+625
-0
lines changed

13 files changed

+625
-0
lines changed

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"deno.enable": true,
3+
"deno.disablePaths": [
4+
"frontend"
5+
]
6+
}

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Deno Web app
2+
3+
This is a template for a web app that uses Deno as backend and TypeScript as frontend.
4+
5+
This template also implement a feature that enumerates all windows on Windows OS and shows them in the UI to demonstrate typed-communications between frontend and backend.
6+
![screenshot](doc/screenshot.png)
7+
To run it, clone the repo and run: `run.bat` on Windows, or invoking tsc and deno manually on other platforms.
8+
9+
(pre-requisite: deno, tsc)
10+
11+
## Usage
12+
13+
You define API interfaces between the web client and the backend script in `api.ts`:
14+
15+
```typescript
16+
export type API = {
17+
checkResult: (a:number, b:number, res:number) => string,
18+
getWindows: () => {title:string, className:string}[]
19+
}
20+
export const api: Promisify<API> = {
21+
checkResult: (a, b, res) => fetchAPI('checkResult', [a, b, res]),
22+
getWindows: () => fetchAPI('getWindows', []),
23+
}
24+
```
25+
26+
Then you implement the API in `api_impl.ts`.
27+
28+
Now you can use the API as normal function in the web client, e.g.:
29+
30+
```typescript
31+
import {api} from '../api.js'
32+
...
33+
const windows = await api.getWindows()
34+
for (const w of windows) {
35+
const div = document.createElement('div');
36+
div.innerHTML = `<b>${w.title}</b> (${w.className})`;
37+
app.appendChild(div);
38+
}
39+
document.body.appendChild(app);
40+
```

api.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const api = {
2+
checkResult: async function (_a:number, _b:number, _res:number) {
3+
return await callAPI('checkResult', arguments) as string
4+
},
5+
getWindows: async function () {
6+
return await callAPI('getWindows', arguments) as {title:string, className:string}[]
7+
},
8+
launchExcel: async function () {
9+
return await callAPI('launchExcel', arguments) as string
10+
},
11+
getActiveExcelRow: async function () {
12+
return await callAPI('getActiveExcelRow', arguments) as {
13+
headings: string[],
14+
data: string[]
15+
}
16+
},
17+
closeBackend: async function () {
18+
return await callAPI('closeBackend', arguments) as string
19+
}
20+
}
21+
22+
export type BackendAPI = Omit<typeof api, 'closeBackend'>
23+
24+
async function callAPI(cmd:string, ...args:any[]){
25+
const proto = window.location.protocol
26+
const host = window.location.hostname
27+
const port = 8080
28+
const resp = await fetch(`${proto}//${host}:${port}/api?cmd=${cmd}&args=${encodeURIComponent(JSON.stringify(args))}`)
29+
return await resp.json()
30+
}

architecture.drawio.svg

Lines changed: 143 additions & 0 deletions
Loading

backend/api_impl.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as wui from "https://win32.deno.dev/0.4.1/UI.WindowsAndMessaging"
2+
import { BackendAPI } from '../api.ts'
3+
4+
let scriptProcess: Deno.ChildProcess
5+
6+
function launchScript() {
7+
const cmd = new Deno.Command('cscript.exe', {
8+
args: ['//nologo', 'excel.js'],
9+
stdout: 'inherit',
10+
stderr: 'inherit',
11+
})
12+
const p = cmd.spawn()
13+
return p
14+
}
15+
16+
export const apiImpl: BackendAPI = {
17+
checkResult: async (a: number, b: number, res: number) => {
18+
if ((a + b) == res) {
19+
return `Correct: ${a} + ${b} = ${res}`;
20+
}
21+
else {
22+
return `Incorrect: ${a} + ${b} != ${res}`;
23+
}
24+
},
25+
getWindows: async () => {
26+
const windows: { title: string, className: string }[] = []
27+
const cb = new Deno.UnsafeCallback({
28+
parameters: ['pointer', 'pointer'],
29+
result: 'bool'
30+
}, (w, lparam) => {
31+
const buf = new Uint8Array(100)
32+
const buffer = new Uint16Array(1000)
33+
wui.GetWindowTextW(w, buffer, 1000)
34+
const title = new TextDecoder('utf-16le').decode(buffer).split('\0')[0]
35+
wui.GetClassNameW(w, buffer, 1000)
36+
const className = new TextDecoder('utf-16le').decode(buffer).split('\0')[0]
37+
const tid = wui.GetWindowThreadProcessId(w, buf)
38+
const pp = Deno.UnsafePointer.of(buf)
39+
const pid = new Deno.UnsafePointerView(pp!).getInt32()
40+
const info = { title, className }
41+
console.log(w, info, title, className, tid, pid);
42+
windows.push(info)
43+
return true;
44+
})
45+
wui.EnumWindows(cb.pointer, null)
46+
await new Promise(resolve => setTimeout(resolve, 1000))
47+
return windows
48+
},
49+
launchExcel: async () => {
50+
if (scriptProcess) {
51+
try {
52+
scriptProcess.kill()
53+
} catch (_e) {
54+
// ignore
55+
}
56+
scriptProcess.kill();
57+
}
58+
scriptProcess = launchScript()
59+
return ''
60+
},
61+
getActiveExcelRow: async () => {
62+
// read output from script
63+
const s = Deno.readTextFileSync('excelrow.txt')
64+
const [hs, vs] = s.split('_@@RS@@_')
65+
return {
66+
headings: hs.split('_@@HS@@_'),
67+
data: vs.split('_@@VS@@_')
68+
}
69+
}
70+
}

doc/screenshot.png

212 KB
Loading

dwa/dwa_service.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { typeByExtension } from "https://deno.land/std/media_types/mod.ts";
2+
import { extname } from "https://deno.land/std/path/mod.ts";
3+
4+
export function startDenoWebApp(root: string, port: number, apiImpl: {[key: string]: Function}) {
5+
const corsHeaders = {
6+
"Access-Control-Allow-Origin": "*",
7+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
8+
"Access-Control-Allow-Headers": "Content-Type, Content-Length, X-Requested-With",
9+
};
10+
const handlerCORS = async (req: Request) => {
11+
const response = await handler(req);
12+
response.headers.set("Access-Control-Allow-Origin", "*");
13+
return response;
14+
}
15+
const handler = async (req: Request) => {
16+
let path = new URL(req.url).pathname;
17+
18+
// API
19+
if (path == "/api") {
20+
const cmd = new URL(req.url).searchParams.get("cmd") || ''
21+
const args = JSON.parse(decodeURI(new URL(req.url).searchParams.get("args") || "[]"))
22+
console.log('handling api', cmd, args)
23+
if (cmd in apiImpl) {
24+
const func = apiImpl[cmd as keyof typeof apiImpl]
25+
const result = await func.apply(apiImpl, args)
26+
return new Response(JSON.stringify(result), { status: 200 });
27+
} else {
28+
if (cmd === 'closeBackend') {
29+
setTimeout(() => {
30+
console.log('backend closed')
31+
Deno.exit()
32+
}, 1000)
33+
return new Response(JSON.stringify('OK'), { status: 200 });
34+
}
35+
}
36+
return new Response(`invalid command ${cmd}`, { status: 404 });
37+
}
38+
39+
if(path == "/"){
40+
path = `/index.html`;
41+
}
42+
try {
43+
console.log('serving', root + path)
44+
const file = await Deno.open(root + path);
45+
return new Response(file.readable, {
46+
headers: {
47+
"content-type" : typeByExtension(extname(path)) || "text/plain"
48+
}
49+
});
50+
} catch(ex){
51+
if(ex.code === "ENOENT"){
52+
return new Response("Not Found", { status: 404 });
53+
}
54+
return new Response("Internal Server Error", { status: 500 });
55+
}
56+
};
57+
58+
Deno.serve({ port }, handlerCORS);
59+
}
60+

0 commit comments

Comments
 (0)