UNPKG

@maskedeng-tom/ssrsx

Version:
840 lines (664 loc) 17.6 kB
# Server Side Renderer with tsx [![npm version](https://badge.fury.io/js/%40maskedeng-tom%2Fssrsx.svg)](https://badge.fury.io/js/%40maskedeng-tom%2Fssrsx) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 -&gt; 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