@maskedeng-tom/ssrsx
v0.5.10
Published
server side renderer with tsx
Downloads
33
Maintainers
Readme
Server Side Renderer with tsx
Table of Contents
- Basic Usage with Koa
- with express
- with Router
- with Client script
- with jQuery
- with jQuery from CDN
- use POST method
- use session
- with CSP (Content Security Policy)
- user Context
- async Component
- Contributing
- Credits
- Authors
- Show your support
- License
Basic Usage with Koa
install
npm install @maskedeng-tom/ssrsx
npm install koa @types/koa
tsconfig.json
- change
jsx
andjsxImportSource
toreact-jsx
andjsx
respectively.
// tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"jsx": "react-jsx", // !important
"jsxImportSource": "ssrsxjsx", // !important
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
server side
// 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
npm install
npm run start
and access to http://localhost:3000/
with express
npm install @maskedeng-tom/ssrsx
npm install express @types/express
// tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"jsx": "react-jsx", // !important
"jsxImportSource": "jsx", // !important
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
// 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
// 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
mkdir src
mkdir src/client
client script
[client module name].js
is a client script file.
// 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]
// 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.
install jQuery and create client script folder
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
cp node_modules/jquery/dist/jquery.min.js src/client
client script with jQuery
// src/client/test.client.js
import $ from 'jquery';
const onClick = (e: Event) => {
const input = $('#username');
alert(input.val());
};
export { onClick };
server side with jQuery
// 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
.
// 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.
// 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
// index.tsx
...
// body parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
...
use session
// 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
// 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
// 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
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
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.
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をお読みください。ここには行動規範やプルリクエストの提出手順が詳細に記載されています。
- フォークする
- フィーチャーブランチを作成する:
git checkout -b my-new-feature
- 変更を追加:
git add .
- 変更をコミット:
git commit -am 'Add some feature'
- ブランチをプッシュ:
git push origin my-new-feature
- プルリクエストを提出 :sunglasses:
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
- Fork it!
- Create your feature branch:
git checkout -b my-new-feature
- Add your changes:
git add .
- Commit your changes:
git commit -am 'Add some feature'
- Push to the branch:
git push origin my-new-feature
- Submit a pull request :sunglasses:
Credits
昨今の複雑化していく開発現場にシンプルな力を! :muscle:
Simplify the complex development landscape of today! :muscle:
Authors
Maskedeng Tom - Initial work - Maskedeng Tom
:smile: プロジェクト貢献者リスト :smile:
See also the list of contributors who participated in this project.
Show your support
お役に立った場合はぜひ :star: を!
Please :star: this repository if this project helped you!
License
MIT License © Maskedeng Tom