@maskedeng-tom/ssrsx
Version:
server side renderer with tsx
840 lines (664 loc) • 17.6 kB
Markdown
# Server Side Renderer with tsx
[](https://badge.fury.io/js/%40maskedeng-tom%2Fssrsx)
[](https://opensource.org/licenses/MIT)
-----
## Table of Contents
- [Basic Usage with Koa](#basic-usage-with-koa)
- [install](#install)
- [tsconfig.json](#tsconfigjson)
- [server side](#server-side)
- [start application](#start-application)
- [with express](#with-express)
- [with Router](#with-router)
- [with Client script](#with-client-script)
- [with jQuery](#with-jquery)
- [with jQuery from CDN](#with-jquery-from-cdn)
- [use POST method](#use-post-method)
- [express body parser](#express-body-parser)
- [use session](#use-session)
- [with CSP (Content Security Policy)](#with-csp-content-security-policy)
- [CSP with Koa](#csp-with-koa)
- [CSP with express](#csp-with-express)
- [user Context](#user-context)
- [for Koa](#for-koa)
- [for Express](#for-express)
- [async Component](#async-component)
- [Contributing](#contributing)
- [Credits](#credits)
- [Authors](#authors)
- [Show your support](#show-your-support)
- [License](#license)
-----
## Basic Usage with Koa
### install
```bash
npm install @maskedeng-tom/ssrsx
npm install koa @types/koa
```
### tsconfig.json
- change `jsx` and `jsxImportSource` to `react-jsx` and `jsx` respectively.
```json
// tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"jsx": "react-jsx", // !important
"jsxImportSource": "ssrsxjsx", // !important
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
```
### server side
```tsx
// index.tsx
import Koa from 'koa';
import { ssrsxKoa } from '@maskedeng-tom/ssrsx';
const App = () => {
return <>
<div>
Hello Ssrsx world !
</div>
</>;
};
const app = new Koa();
app.use(ssrsxKoa({
development: true,
app: <App/>
}));
app.listen(3000);
```
### start application
```bash
npm install
npm run start
```
and access to [http://localhost:3000/](http://localhost:3000/)
-----
## with express
```bash
npm install @maskedeng-tom/ssrsx
npm install express @types/express
```
```json
// tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"jsx": "react-jsx", // !important
"jsxImportSource": "jsx", // !important
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
```
```tsx
// index.tsx
import express from 'express';
import { ssrsxExpress } from '@maskedeng-tom/ssrsx';
const App = () => {
return <>
<div>
Hello Ssrsx world !
</div>
</>;
};
const app = express();
app.use(ssrsxExpress({
development: true,
app: <App/>
}));
app.listen(3000);
```
-----
## with Router
```tsx
// index.tsx
import Koa from 'koa';
import { ssrsxKoa, Router, Routes, Route, Link } from '@maskedeng-tom/ssrsx';
const Page1 = () => {
return <div>
<div>Page1</div>
<div><Link to="/page2">Link to Page2</Link></div>
<div><Link to="/">Top</Link></div>
</div>;
};
const Page2 = () => {
return <div>
<div>Page2</div>
<div><Link to="/page1">Link to Page1</Link></div>
<div><Link to="/">Top</Link></div>
</div>;
};
const App = () => {
return <html lang="en">
<head>
<meta charSet="UTF-8"/>
<title>Ssrsx</title>
</head>
<body>
<div>
<Router>
<Routes>
<Route path="/">
<div>
<div>Hello Ssrsx world !</div>
<div><Link to="/page1">Link to Page1</Link></div>
<div><Link to="/page2">Link to Page2</Link></div>
</div>
</Route>
<Route path="page1"><Page1/></Route>
<Route path="page2"><Page2/></Route>
</Routes>
</Router>
</div>
</body>
</html>;
};
const app = new Koa();
app.use(ssrsxKoa({
development: true,
app: <App/>
}));
app.listen(3000);
```
-----
## with Client script
### create client script folder
```bash
mkdir src
mkdir src/client
```
### client script
- `[client module name].js` is a client script file.
```ts
// src/client/test.js
const onClick = (e: Event) => {
alert('from js://onClick@test');
};
export { onClick }; // need export! important!
```
### server side
- `js://` is a protocol to call client script function from ssrsx.
`js://[exported function name]@[client module name]`
```tsx
// index.tsx
import Koa from 'koa';
import { ssrsxKoa, useGlobalStyle } from '@maskedeng-tom/ssrsx';
const App = () => {
useGlobalStyle({
'.clickable': {
cursor: 'pointer',
color: 'blue',
textDecoration: 'underline',
},
});
return <html lang="en">
<head>
<meta charSet="UTF-8"/>
<title>Ssrsx</title>
</head>
<body>
<div>
<div onClick="alert('inline js')" className="clickable">
Run inline js
</div>
<div onClick="js://onClick@test" className="clickable" >
Run outside client js (src/client/test.client.js -> onClick)
</div>
</div>
</body>
</html>;
};
const app = new Koa();
app.use(ssrsxKoa({
development: true,
clientRoot: 'src/client', // client script root
app: <App/>
}));
app.listen(3000);
```
-----
## with jQuery
This is a sample using jQuery. External libraries are loaded using requirejs. Specify the root folder of the client script in clientRoot and the path to place modules such as jQuery in requireJsRoot.
- [requirejs](https://requirejs.org/)
- [requirejs with jQuery](https://requirejs.org/docs/jquery.html)
### install jQuery and create client script folder
```bash
npm install @maskedeng-tom/ssrsx
npm install koa @types/koa
npm install jquery @types/jquery
mkdir src
mkdir src/client
```
### copy jQuery script to client script folder
```bash
cp node_modules/jquery/dist/jquery.min.js src/client
```
### client script with jQuery
```ts
// src/client/test.client.js
import $ from 'jquery';
const onClick = (e: Event) => {
const input = $('#username');
alert(input.val());
};
export { onClick };
```
### server side with jQuery
```tsx
// index.tsx
import Koa from 'koa';
import { ssrsxKoa } from '@maskedeng-tom/ssrsx';
const App = () => {
return <html lang="en">
<head>
<meta charSet="UTF-8"/>
<title>Ssrsx</title>
</head>
<body>
<div>
<div>
<input type="text" id="username" name="username" value="foo"/>
</div>
<button type="text" onClick="js://test.onClick">
Show input tag value!
</button>
</div>
</body>
</html>;
};
const app = new Koa();
app.use(ssrsxKoa({
development: true,
clientRoot: 'src/client',
requireJsRoot: 'src/client', // for requirejs
requireJsPaths: { // for requirejs.config paths
'jquery': 'jquery.min', // define for jquery (cut '.js' extension)
},
app: <App/>
}));
app.listen(3000);
```
-----
## with jQuery from CDN
If you want to use a CDN, specify the same version of jQuery as the one you installed with `npm install jquery`.
```tsx
// index.tsx
...
app.use(ssrsxKoa({
development: true,
clientRoot: 'src/client',
requireJsRoot: 'src/client',
requireJsPaths: {
'jquery': 'https://code.jquery.com/jquery-3.7.1.min',
},
app: <App/>
}));
...
```
-----
## use POST method
You can get the data sent by the POST method by using the `useBody` function.
and You need to add a `body parser` to get the data sent by the POST method.
```tsx
// index.tsx
import Koa from 'koa';
import bodyParser from 'koa-bodyparser'; // add body parser
import { ssrsxKoa, Router, Routes, Route, Link, useBody } from '../';
////////////////////////////////////////////////////////////////////////////////
const LoginCheck = () => {
// post body data
const body = useBody<{username: string, password: string}>();
//
return <>
<h1>Login Post Result</h1>
<div>
<div>Username: {body.username}</div>
<div>Password: {body.password}</div>
</div>
<Link to="/">Top</Link>
</>;
};
const LoginForm = () => {
return <>
<h1>Login Form</h1>
<form method="post" action="/login">
<div>
<label>
username: <input type="text" name="username" />
</label>
</div>
<div>
<label>
password: <input type="password" name="password" />
</label>
</div>
<button type="submit">Login</button>
</form>
</>;
};
const App = () => {
return <html lang="en">
<head>
<meta charSet="utf-8"/>
<title>Ssrsx</title>
</head>
<body>
<Router>
<Routes>
<Route path="/"><LoginForm /></Route>
<Route path="/login"><LoginCheck /></Route>
</Routes>
</Router>
</body>
</html>;
};
const app = new Koa();
// body parser
app.use(bodyParser());
app.use(ssrsxKoa({
development: true,
clientRoot: 'test/client',
app: <App/>
}));
app.listen(3000);
```
### express body parser
```tsx
// index.tsx
...
// body parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
...
```
-----
## use session
```tsx
// index.tsx
import Koa from 'koa';
import bodyParser from 'koa-bodyparser'; // add body parser
import session from 'koa-session'; // add session
//
import { ssrsxKoa, ssrsxExpress, useSearch } from '../';
import { Router, Routes, Route, Link, Navigate, useBody, useSession } from '../';
////////////////////////////////////////////////////////////////////////////////
interface SessionContext {
username?: string;
}
const Authorized = () => {
// session
const session = useSession<SessionContext>();
if(!session.username){
// not authorized
return <Navigate to="/"/>;
}
// authorized
return <>
<h1>Authorized</h1>
<div>
<div>Authorized Username: {session.username}</div>
</div>
<Link to="/logout">Logout</Link>
</>;
};
const Login = () => {
// session
const session = useSession<SessionContext>();
// post body data
const body = useBody<{username: string, password: string}>();
// check username and password
if(body.username === 'admin' && body.password === 'admin'){
session.username = body.username;
return <Navigate to="/authorized"/>;
}
// authorization failed
return <Navigate to="/?message=authorization_failed"/>;
};
const Logout = () => {
// set session
const session = useSession<SessionContext>();
// session clear
session.username = undefined;
// redirect to top
return <Navigate to="/"/>;
};
const LoginForm = () => {
// get search(query parameter) data (?message=...)
const search = useSearch<{message: string}>();
// set session
const session = useSession<SessionContext>();
if(session.username){
// already authorized
return <Navigate to="/authorized"/>;
}
// login form
return <>
<h1>Login Form</h1>
<form method="post" action="/login">
<div>
<label>
username: <input type="text" name="username" />
</label>
</div>
<div>
<label>
password: <input type="password" name="password" />
</label>
</div>
{
search.message && <div>{search.message}</div>
}
<button type="submit">Login</button>
</form>
</>;
};
const App = () => {
return <html lang="en">
<head>
<meta charSet="utf-8"/>
<title>Ssrsx</title>
</head>
<body>
<Router>
<Routes>
<Route path="/"><LoginForm /></Route>
<Route path="/login"><Login /></Route>
<Route path="/logout"><Logout /></Route>
<Route path="/authorized"><Authorized /></Route>
</Routes>
</Router>
</body>
</html>;
};
const app = new Koa();
app.keys = ['your custom secret']; // session key
app.use(session(app)); // add session
app.use(bodyParser());
app.use(ssrsxKoa({
development: true,
clientRoot: 'test/client',
app: <App/>
}));
app.listen(3000);
```
-----
## with CSP (Content Security Policy)
Setting CSP (Content Security Policy) requires allowing `ws://` to communicate with WebSocket for ssrsx HotReload (`development: true`).
Also, because inline scripts are used internally, you need to allow `'unsafe-inline'` or `'nonce-${nonce}'`.
### CSP with Koa
```tsx
// index.tsx
import helmet from 'koa-helmet';
import crypto from 'crypto';
...
if(process.env.NODE_ENV === 'production'){
app.use(helmet());
app.use((ctx, next) => {
// set nonce to state
ctx.state.nonce = crypto.randomBytes(16).toString('base64');
//
return helmet.contentSecurityPolicy({ directives: {
defaultSrc: ['\'self\'','ws'], // add 'ws'
connectSrc: ['\'self\'','ws://*:*'], // add 'ws://*:*'
scriptSrc: [
'\'self\'',
`'nonce-${ctx.state.nonce}'`, // add 'nonce-??' or 'unsafe-inline'
],
}})(ctx, next) as Koa.Middleware;
});
}
```
### CSP with express
```tsx
// index.tsx
import helmet from 'helmet';
import crypto from 'crypto';
...
if(process.env.NODE_ENV === 'production'){
app.use(helmet());
app.use((req, res, next) => {
// set nonce to locals
(res as express.Response).locals.nonce = crypto.randomBytes(16).toString('base64');
//
helmet.contentSecurityPolicy({ directives: {
defaultSrc: ['\'self\'','ws'], // add 'ws'
connectSrc: ['\'self\'','ws://*:*'], // add 'ws://*:*'
scriptSrc: [
'\'self\'',
// add 'nonce-??' or 'unsafe-inline'
(req, res) => `'nonce-${(res as express.Response).locals?.nonce}'`,
],
}})(req, res, next);
});
}
...
```
-----
## User Context
User context can be set with the `context` property of the `ssrsx(Koa or Express)` function.
The `context` function is called every time it is rendered.
You can use the `useContext` function to get the user context value.
### for Koa
```tsx
interface UserContext {
lang: string;
}
const App = () => {
const user = useContext<{UserContext}>();
return <div>Lang: {user.lang}</div>;
};
...
app.use(ssrsxKoa({
...
context: (server): UserContext => {
return {
lang: 'en',
};
},
}));
```
### for Express
```tsx
interface UserContext {
lang: string;
}
const App = () => {
const user = useContext<{UserContext}>();
return <div>Lang: {user.lang}</div>;
};
...
app.use(ssrsxExpress({
...
context: (server): UserContext => {
return {
lang: 'en',
};
},
}));
```
-----
## async Component
In Ssrsx, you cannot perform asynchronous processing using `useState` or `useEffect` etc. , but you can create asynchronous components.
The ssrsx `context` function is called every time it is rendered, so when specifying something like a database instance, create the instance externally and specify it in the `context` function.
```tsx
import { Redis } from 'ioredis';
interface UserContext {
redis: Redis;
}
const isAuthorized = async (username: string, password: string) => {
const context = useContext<UserContext>();
const value = await context.redis.get(username);
return value === password;
};
// async component
const LoginCheck = async () => {
// post body data
const body = useBody<{username: string, password: string}>();
// check login
const isLogin = await isAuthorized(body.username, body.password);
if(!isLogin){
return <Navigate to="/"/>;
}
return <>
<h1>Login OK !</h1>
<div>
<div>Username: {body.username}</div>
<div>Password: {body.password}</div>
</div>
<Link to="/">Top</Link>
</>;
};
...
// create redis instance
const redis = new Redis();
app.use(ssrsxKoa({
...
context: (ctx, next): UserContext => {
return {
redis,
};
},
}));
```
-----
## Contributing
[CONTRIBUTING.md](CONTRIBUTING.md)をお読みください。ここには行動規範やプルリクエストの提出手順が詳細に記載されています。
1. フォークする
2. フィーチャーブランチを作成する:`git checkout -b my-new-feature`
3. 変更を追加:`git add .`
4. 変更をコミット:`git commit -am 'Add some feature'`
5. ブランチをプッシュ:`git push origin my-new-feature`
6. プルリクエストを提出 :sunglasses:
> Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
>
> 1. Fork it!
> 2. Create your feature branch: `git checkout -b my-new-feature`
> 3. Add your changes: `git add .`
> 4. Commit your changes: `git commit -am 'Add some feature'`
> 5. Push to the branch: `git push origin my-new-feature`
> 6. Submit a pull request :sunglasses:
-----
## Credits
昨今の複雑化していく開発現場にシンプルな力を! :muscle:
> Simplify the complex development landscape of today! :muscle:
-----
## Authors
**Maskedeng Tom** - *Initial work* - [Maskedeng Tom](https://github.com/maskedeng-tom)
:smile: [プロジェクト貢献者リスト](https://github.com/maskedeng-tom/ssrsx/contributors) :smile:
> See also the list of [contributors](https://github.com/maskedeng-tom/ssrsx/contributors) who participated in this project.
-----
## Show your support
お役に立った場合はぜひ :star: を!
> Please :star: this repository if this project helped you!
-----
## License
[MIT License](https://github.com/maskedeng-tom/ssrsx/blob/main/LICENSE.txt) © Maskedeng Tom
## TODOs