├── .eslintrc.js ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── README.md ├── package.json ├── src └── index.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: [ 4 | 'typescript', 5 | '@typescript-eslint', 6 | ], 7 | extends: ['airbnb-base'], 8 | rules: { 9 | // allow debugger during development 10 | 'linebreak-style': 0, 11 | indent: [2, 4, { 12 | SwitchCase: 1, 13 | }], 14 | 'max-len': [2, { code: 160, ignoreUrls: true }], 15 | radix: ['error', 'as-needed'], 16 | 'object-shorthand': ['error', 'methods'], 17 | 'no-unused-expressions': ['error', { 18 | allowShortCircuit: true, 19 | }], 20 | 'no-bitwise': ['error', { 21 | allow: ['~'], 22 | }], 23 | 'import/extensions': 0, 24 | 'import/no-unresolved': 0, 25 | 'import/prefer-default-export': 0, 26 | 'import/no-dynamic-require': 0, 27 | 'object-curly-newline': 0, 28 | 'consistent-return': 0, 29 | 'no-shadow': 0, 30 | 'no-redeclare': 0, 31 | 'no-unused-vars': 0, 32 | 'no-useless-constructor': 0, 33 | 'no-empty-function': 0, 34 | 'class-methods-use-this': 0, 35 | 'import/no-extraneous-dependencies': 0, 36 | '@typescript-eslint/no-shadow': 2, 37 | '@typescript-eslint/no-redeclare': 2, 38 | '@typescript-eslint/no-unused-vars': 2, 39 | '@typescript-eslint/no-useless-constructor': 2, 40 | 'no-restricted-syntax': 0, 41 | 'no-param-reassign': 0, 42 | 'no-return-await': 0, 43 | 'no-use-before-define': 0, 44 | 'no-await-in-loop': 0, 45 | 'no-continue': 0, 46 | 'no-plusplus': 0, 47 | 'no-debugger': 0, 48 | 'no-console': 0, 49 | 'no-bitwise': 0, 50 | 'padding-line-between-statements': [ 51 | 'warn', 52 | { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, 53 | { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, 54 | { blankLine: 'always', prev: '*', next: 'return' }, 55 | { blankLine: 'always', prev: 'block-like', next: '*' }, 56 | { blankLine: 'always', prev: 'block', next: '*' }, 57 | { blankLine: 'always', prev: 'function', next: '*' }, 58 | ], 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 14 18 | - run: npm install 19 | 20 | publish-npm: 21 | needs: build 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v2 26 | with: 27 | node-version: 14 28 | registry-url: https://registry.npmjs.org/ 29 | - run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 32 | 33 | publish-gpr: 34 | needs: build 35 | runs-on: ubuntu-latest 36 | permissions: 37 | contents: read 38 | packages: write 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: actions/setup-node@v2 42 | with: 43 | node-version: 14 44 | registry-url: https://npm.pkg.github.com/ 45 | - run: npm publish 46 | env: 47 | NODE_AUTH_TOKEN: ${{secrets.GIT_TOKEN}} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | yarn.lock 4 | package-lock.json 5 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @umajs/plugin-react-ssr 2 | 3 | > 针对Umajs提供React服务端渲染模式的开发插件,插件基于服务端渲染骨架工具[Srejs](https://github.com/dazjean/srejs)开发。 4 | 5 | ## 插件介绍 6 | 7 | `plugin-react-ssr`插件扩展了`Umajs`中提供的统一返回处理`Result`对象,新增了`reactView`页面组件渲染方法,可在`controller`自由调用,使用类似传统模板引擎;也同时将方法挂载到了koa中间件中的`ctx`对象上;当一些公关的页面组件,比如404、异常提示页面、登录或者需要在中间件中拦截跳转时可以在`middleware`中调用。 8 | 9 | ## 插件安装 10 | 11 | ``` 12 | yarn add @umajs/plugin-react-ssr --save 13 | ``` 14 | 15 | ## 插件配置 16 | 17 | ```ts 18 | // plugin.config.ts 19 | export default <{ [key: string]: TPluginConfig }>{ 20 | 'react-ssr': { 21 | enable:true, 22 | options:{ 23 | rootDir:'web', // 客户端页面组件根文件夹 24 | rootNode:'app', // 客户端页面挂载根元素ID 25 | ssr: true, // 全局开启服务端渲染 26 | cache: false, // 全局使用服务端渲染缓存 开发环境设置true无效 27 | prefixCDN: '/' // 客户端代码部署CDN前缀 28 | } 29 | } 30 | }; 31 | ``` 32 | 33 | ## web 目录结构 34 | 35 | ```shell 36 | - web # rootDir配置可修改 37 | - pages # 固定目录 38 | - home #页面名称 39 | - index.tsx 40 | - index.scss 41 | ``` 42 | 43 | ## 创建 react 页面组件 44 | 45 | 页面组件开发模式支持 js ,tsx。 46 | 47 | ```ts 48 | import './home.scss' 49 | import React from 'react' 50 | type typeProps = { 51 | say: string 52 | } 53 | export default function (props: typeProps) { 54 | const { say } = props 55 | return
{say}
56 | } 57 | ``` 58 | 59 | ## 路由中使用插件 60 | 61 | ```ts 62 | import { BaseController, Path } from '@umajs/core' 63 | import { Result } from '@umajs/plugin-react-ssr' 64 | 65 | export default class Index extends BaseController { 66 | @Path('/') 67 | index() { 68 | return Result.reactView( 69 | 'home', 70 | { say: 'hi,I am a ReactView' }, 71 | { cache: true } 72 | ) 73 | } 74 | } 75 | 76 | ``` 77 | 78 | ## **[使用文档](https://umajs.gitee.io/%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%B8%B2%E6%9F%93/React-ssr.html)** 79 | 80 | ## 案例 81 | - [uma-css-module](https://github.com/dazjean/Srejs/tree/mian/example/uma-css-module) 82 | - [uma-react-redux](https://github.com/dazjean/Srejs/tree/mian/example/uma-react-redux) 83 | - [uma-useContext-useReducer](https://github.com/dazjean/Srejs/tree/mian/example/uma-useContext-useReducer) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@umajs/plugin-react-ssr", 3 | "version": "2.0.9", 4 | "keywords": [ 5 | "umajs", 6 | "umajs-plugin", 7 | "umajs-plugin-react", 8 | "umajs-plugin-react-ssr"], 9 | "description": "In umajs, React is used to develop the plug-in of SPA and MPA, which supports server-side rendering and client-side rendering", 10 | "author": "zunyi_zjj@163.com", 11 | "license": "MIT", 12 | "main": "lib/index.js", 13 | "directories": { 14 | "lib": "lib", 15 | "test": "__tests__" 16 | }, 17 | "files": [ 18 | "lib", 19 | "index.d.ts" 20 | ], 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org", 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "fix": "esw src --fix --ext .ts", 27 | "lint": "npx eslint src --ext .ts", 28 | "lint-w": "esw src --clear --color -w --ext .ts", 29 | "build-w": "tsc -w --inlineSourceMap", 30 | "start": "run-p lint-w build-w", 31 | "prebuild": "npm run lint", 32 | "build": "tsc", 33 | "prepublish": "npm run build" 34 | }, 35 | "dependencies": { 36 | "@srejs/react": "latest", 37 | "consolidate": "^0.16.0", 38 | "get-stream": "^6.0.1" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^26.0.19", 42 | "@types/node": "^12.12.9", 43 | "@typescript-eslint/eslint-plugin": "^4.20.0", 44 | "@typescript-eslint/parser": "^4.20.0", 45 | "@umajs/core": "^2.0.1", 46 | "eslint": "^7.15.0", 47 | "eslint-config-airbnb-base": "^14.2.0", 48 | "eslint-plugin-import": "^2.22.1", 49 | "eslint-plugin-node": "^11.1.0", 50 | "eslint-plugin-promise": "^4.2.1", 51 | "eslint-plugin-typescript": "^0.14.0", 52 | "eslint-watch": "^7.0.0", 53 | "npm-run-all": "^4.1.5", 54 | "typescript": "^4.2.3" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git@github.com:Umajs/plugin-react-ssr.git" 59 | }, 60 | "bugs": { 61 | "url": "https://github.com/Umajs/plugin-react-ssr" 62 | }, 63 | "homepage": "https://github.com/Umajs/plugin-react-ssr#readme" 64 | } 65 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { IContext, TPlugin, Result as R, Uma, TPluginConfig } from '@umajs/core'; 2 | import * as engineSource from 'consolidate'; 3 | import * as getStream from 'get-stream'; 4 | import Srejs from '@srejs/react'; 5 | 6 | interface TviewOptions{ 7 | ssr?: boolean, // 全局开启服务端渲染 8 | cache?: boolean, // 全局使用服务端渲染缓存 9 | useEngine?: boolean, // 渲染自定义html的页面组件时,选择性开启使用模板引擎 10 | baseName?: string, // 动态修改嵌套路由的basename 默认为页面组件名称。eg:/router 11 | layout?: boolean // 是否启用页面整体布局(默认为true), 开启后可在web/layout目录下编写布局代码. 12 | } 13 | 14 | export interface TssrPluginOptions extends TviewOptions { 15 | rootDir?:string, // 客户端页面组件根文件夹 16 | rootNode?:string, // 客户端页面挂载根元素ID 17 | defaultRouter?:boolean, // 开启默认文件路由 18 | prefixCDN?:string, // 构建后静态资源CDN地址前缀 19 | prefixRouter?:string // 默认页面路由前缀(在defaultRouter设置为true时有效) 20 | } 21 | 22 | interface IReactViewParms{ 23 | viewName :string; 24 | initProps : any, 25 | options:TviewOptions 26 | } 27 | export class Result extends R { 28 | /** 29 | * @deprecated 请使用Result.react()函数渲染页面 30 | * @param viewName 31 | * @param initProps 32 | * @param options 33 | * @returns 34 | */ 35 | static reactView(viewName: string, initProps?: any, options?:TviewOptions) { 36 | return new Result({ 37 | type: 'reactView', 38 | data: { 39 | viewName, 40 | initProps, 41 | options, 42 | }, 43 | }); 44 | } 45 | 46 | /** 47 | * 48 | * @param viewName 页面组件名称 49 | * @param initProps react页面组件初始化props 50 | * @param options 页面运行配置参数 51 | * @returns 52 | */ 53 | static react(viewName: string, initProps?: any, options?:TviewOptions) { 54 | return new Result({ 55 | type: 'reactView', 56 | data: { 57 | viewName, 58 | initProps, 59 | options, 60 | }, 61 | }); 62 | } 63 | } 64 | const NODE_ENV = (process.env && process.env.NODE_ENV) || 'development'; 65 | let SrejsInstance; 66 | 67 | /** 插件配置读取放到了@srejs/react框架中进行兼容,在生产环境部署前构建阶段不会执行插件 */ 68 | let opt:TssrPluginOptions = Uma.config?.ssr || {}; // ssr.config.ts 69 | const reactSsrPlugin = Uma.config?.plugin?.react || Uma.config?.plugin['react-ssr']; 70 | 71 | if (reactSsrPlugin?.options) { 72 | opt = reactSsrPlugin.options; 73 | } 74 | 75 | let defaultRouter = false; 76 | 77 | // eslint-disable-next-line no-prototype-builtins 78 | if (opt.hasOwnProperty('defaultRouter')) { 79 | defaultRouter = opt.defaultRouter; 80 | } 81 | 82 | try { 83 | SrejsInstance = new Srejs(Uma.app, NODE_ENV === 'development', defaultRouter, opt); 84 | } catch (error) { 85 | console.error(error); 86 | } 87 | 88 | const renderDom = async (ctx:IContext, viewName:string, initProps?:any, options?:TviewOptions) => { 89 | const mergeProps = Object.assign(ctx.state || {}, initProps); 90 | let html = await SrejsInstance.render(ctx, viewName, mergeProps, options); 91 | 92 | const viewPlugin = Uma.config?.plugin.views; // use @umajs/plugin-views 93 | const ssrConfig = Uma.config?.plugin['react-ssr']; 94 | 95 | let useEngine = false; 96 | 97 | if (typeof (options?.useEngine) === 'boolean') { 98 | useEngine = options?.useEngine; 99 | } else { 100 | useEngine = ssrConfig?.options?.useEngine; 101 | } 102 | 103 | if (viewPlugin?.enable && useEngine) { 104 | const { opts } = viewPlugin.options; 105 | const { map } = opts; 106 | const engineName = map?.html; 107 | 108 | console.assert(engineName, '@umajs/plugin-views must be setting; eg====> map:{html:"nunjucks"}'); 109 | const engine = engineSource[engineName]; 110 | const state = { ...options, ...mergeProps }; 111 | 112 | if (typeof html === 'object' && html.readable && options.cache) { 113 | // when cache model ,html return a file stream 114 | html = await getStream(html); 115 | } 116 | 117 | // 在SSR模式中, 将__SSR_DATA_匹配出来, 避免其中内容被模板引擎执行, 避免注入类攻击 118 | const ssrReg = new RegExp(/]*>window.__SSR_DATA__=([\s\S]*?)<\/script>/); 119 | const placeholderStr = ''; 120 | const ssrScriptStr = html.match?.(ssrReg)?.[0] || ''; 121 | const renderHtml = html.replace(ssrReg, placeholderStr); // without ssr script 122 | 123 | // engine rendering 124 | html = await engine.render(renderHtml, state); 125 | // final result 126 | html = html.replace(placeholderStr, ssrScriptStr); 127 | } 128 | 129 | return html; 130 | }; 131 | 132 | const renderView = async (ctx: IContext, viewName: string, initProps?: any, options?: TviewOptions) => { 133 | const html = await renderDom(ctx, viewName, initProps, options); 134 | 135 | ctx.type = 'text/html'; 136 | ctx.body = html; 137 | }; 138 | 139 | export default (): TPlugin => ({ 140 | results: { 141 | async reactView(ctx: IContext, data: IReactViewParms) { 142 | const { 143 | viewName, 144 | initProps = {}, 145 | options = {}, 146 | } = data; 147 | 148 | await renderView(ctx, viewName, initProps, options); 149 | }, 150 | }, 151 | context: { 152 | /** 153 | * @deprecated 请使用ctx.react()函数渲染页面 154 | * @param viewName 155 | * @param initProps 156 | * @param options 157 | */ 158 | async reactView(viewName:string, initProps?:any, options?:TviewOptions) { 159 | await renderView(this, viewName, initProps, options); 160 | }, 161 | async react(viewName:string, initProps?:any, options?:TviewOptions) { 162 | await renderView(this, viewName, initProps, options); 163 | }, 164 | async reactDom(viewName:string, initProps?:any, options?:TviewOptions): Promise { 165 | return await renderDom(this, viewName, initProps, options); 166 | }, 167 | }, 168 | 169 | }); 170 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "noImplicitAny": false, 6 | "sourceMap": false, 7 | "rootDir":"./src", 8 | "outDir":"./lib", 9 | "watch":false, 10 | "declaration": true, 11 | "alwaysStrict": true, 12 | "removeComments": true, 13 | "preserveWatchOutput": true, 14 | "experimentalDecorators": true 15 | }, 16 | "include":[ 17 | "./src/**/*" 18 | ] 19 | } --------------------------------------------------------------------------------