{"id":21195632,"url":"https://github.com/qiqiboy/tiger-new","last_synced_at":"2026-01-12T08:35:32.882Z","repository":{"id":37431274,"uuid":"91086492","full_name":"qiqiboy/tiger-new","owner":"qiqiboy","description":"快速生成一个标准前端开发项目的CLI工具，支持SSR同构渲染。","archived":false,"fork":false,"pushed_at":"2024-08-09T06:35:16.000Z","size":1096,"stargazers_count":21,"open_issues_count":4,"forks_count":9,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-30T13:48:40.284Z","etag":null,"topics":["cli","create-react-app","javascript","react","react-cli"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qiqiboy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-05-12T11:52:23.000Z","updated_at":"2025-03-19T11:59:44.000Z","dependencies_parsed_at":"2024-09-17T22:49:50.962Z","dependency_job_id":null,"html_url":"https://github.com/qiqiboy/tiger-new","commit_stats":{"total_commits":459,"total_committers":4,"mean_commits":114.75,"dds":0.1285403050108932,"last_synced_commit":"c10ae9cce034aaa7d2b12b76db6f4cd7bbeafb40"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/qiqiboy/tiger-new","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qiqiboy%2Ftiger-new","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qiqiboy%2Ftiger-new/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qiqiboy%2Ftiger-new/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qiqiboy%2Ftiger-new/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qiqiboy","download_url":"https://codeload.github.com/qiqiboy/tiger-new/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qiqiboy%2Ftiger-new/sbom","scorecard":{"id":753725,"data":{"date":"2025-08-11","repo":{"name":"github.com/qiqiboy/tiger-new","commit":"c10ae9cce034aaa7d2b12b76db6f4cd7bbeafb40"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.6,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-22T20:58:58.378Z","repository_id":37431274,"created_at":"2025-08-22T20:58:58.378Z","updated_at":"2025-08-22T20:58:58.378Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28337599,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T06:09:07.588Z","status":"ssl_error","status_checked_at":"2026-01-12T06:05:18.301Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","create-react-app","javascript","react","react-cli"],"created_at":"2024-11-20T19:29:22.013Z","updated_at":"2026-01-12T08:35:27.871Z","avatar_url":"https://github.com/qiqiboy.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tiger-new\n\n[![npm](https://img.shields.io/npm/v/tiger-new.svg?style=flat)](https://npm.im/tiger-new)\n\n\u003c!-- vim-markdown-toc GFM --\u003e\n\n* [安装](#安装)\n* [使用](#使用)\n    - [创建新项目](#创建新项目)\n    - [升级老项目](#升级老项目)\n    - [功能说明](#功能说明)\n* [更新日志](#更新日志)\n    - [v8.x 新功能](#v8x-新功能)\n    - [v7.x 新功能](#v7x-新功能)\n    - [v6.x 新功能](#v6x-新功能)\n    - [v5.x 新功能](#v5x-新功能)\n    - [v4.x 新功能](#v4x-新功能)\n    - [v3.x 新功能](#v3x-新功能)\n    - [v2.x 新功能](#v2x-新功能)\n* [**SSR**](#ssr)\n    - [开发](#开发)\n    - [发布](#发布)\n    - [路由与异步数据处理](#路由与异步数据处理)\n    - [`withSSR`](#withssr)\n    - [国际化 i18n](#国际化-i18n)\n    - [注意事项](#注意事项)\n\n\u003c!-- vim-markdown-toc --\u003e\n\n快速生成一个标准开发项目的 CLI。(本项目自 facebook 官方出品的 [create-react-app](https://github.com/facebookincubator/create-react-app) 修改而来)\n\n-   CLI-QA 形式初始化配置项目\n-   生成的项目支持 `webpack` + `es6` 开发环境\n-   也支持 jsx 语法，所以也同时可以用来开发 `react` 应用\n-   提供渐进式对`typescript`语法的支持，支持`tsx`开发`react`应用\n-   不仅支持 SPA，也支持**多页面**项目开发\n-   **NEW** 支持 jest 自动化测试\n-   **NEW** 支持[`SSR`](#ssr)(server side render)\n-   多页面应用支持模板分离\n-   打包构建支持抽取打包公共组件、库、样式\n-   支持 `scss`、`less`\n-   支持 `eslint` `tslint` 语法检查\n-   支持 `ternjs` 配置\n\n更多特性及使用细节请安装后创建项目查看\n\n![screenshot](https://cloud.githubusercontent.com/assets/3774036/26042794/b2ee8ce0-396a-11e7-97e1-b52f31309c2c.png)\n\n## 安装\n\n不需要安装，你可以直接使用 **`npx tiger-new`** 快速开始。如果你之前安装过`tiger-new`，请卸载它: **`npm uninstall tiger-new -g`**\n\n## 使用\n\n### 创建新项目\n\n    $ npx tiger-new \u003c项目名|路径\u003e\n\n### 升级老项目\n\n    $ npx tiger-new \u003c项目名|路径\u003e --upgrade\n\n例如：\n\n    $ npx tiger-new my-new-project\n    $ cd my-new-project/\n    $ npm start\n\n### 功能说明\n\n`tiger-new`完全基于`create-react-app`，所以完整支持所有`cra`的所有功能特性。你可以直接参考[`cra官网`](https://create-react-app.dev/)来了解生成项目的基本的使用。\n\n\u003e 注：是指生成的项目的功能特性完全支持`cra`所生成的项目的所有特性。但是`tiger-new`与`cra`的项目创建流程并不一致。\n\n**支持的环境变量**\n\n-   `PORT` 指定本地服务的端口\n-   `HOST` 指定本地服务的 host；请注意，单独设置该变量，将导致本地的`localhost`失效，只能使用指定的`HOST`访问服务\n-   `HTTPS` 配置使用 https；需要有本地的 SSL 证书\n-   `PROXY` 配置本地代理服务器\n-   `DANGEROUSLY_DISABLE_HOST_CHECK` 关闭 host 检测；`DANGEROUSLY_DISABLE_HOST_CHECK=true` 将允许任意的 host 访问\n-   `IGNORE_CSS_ORDER_WARNINGS` 禁止`mini-css-extract-plugin`插件输出`conflicting order`警告信息\n-   `PUBLIC_URL` 类似 webpack 配置中的`config.publicPath`，可以用来控制生成的代码的入口位置\n-   `BASE_NAME` 指定项目的`basename`，例如`BASE_NAME=/account`\n-   `SKIP_CDN` 本次构建不需要上传 CDN，即不使用`package.json`中的`cdn`字段来作为`publicPath`\n-   `BUILD_DIR` 指定项目构建输出目录；不传递该变量情况下，`prodcution`环境输出到`build`目录，`development`环境输出到`buildDev`目录\n-   `SSR` 是否启用`SSR`。默认情况下，当项目存在`SSR`入口文件，将自动启用`SSR`。你可以通过`SSR=false`来禁用这一功能\n-   `RUNTIME` 运行时标记，`web` 或者 `node`\n-   `COMPILE_ON_WARNING` 构建时允许警告\n-   `TSC_COMPILE_ON_ERROR` 开发时允许 ts 编译器错误\n-   `DISABLE_TSC_CHECK` 禁用 typescript 编译检查\n-   `DISABLE_NEW_JSX_TRANSFORM` 不使用 react 新的 JSX transform\n-   `DISABLE_FAST_REFRESH` 不使用 `react-refresh`，对于超大型项目这很有用，因为目前的 `react-refresh` 存在较严重的性能问题\n-   `DISABLE_WEBPACK_CACHE` 不使用 `webpack` 的 `cache` 特性，某些项目可能存在构建时使用 `filesystem` 缓存时产生崩溃\n-   `TIGER_*` 任意的以`TIGER_`开头的变量。**该变量也会传递给 webpack 构建，所以你可以在项目代码中访问该变量：`process.env.TIGER_*`**\n\n以上环境变量，你可以在运行相关命令时指定，也可以通过项目根目录下的`.env` `.env.production` `.env.developement` `.env.local` `.env.production.local` `.env.developement.local` 等文件配置。\n\n但是，请注意，默认以`.local`结尾的环境变量文件，是不包含在项目 git 仓库中的。\n\n**支持的运行命令**\n\n-   `npm start` 启动本地开发服务\n-   `npm run build` 构建生产包(默认输出文件到 build 目录)\n-   `npm run build:dev` 构建测试包(默认输出文件到 buildDev 目录)\n-   `npm test` 运行测试\n-   `npm run serve` 启动本地预览服务器\n-   `npm run i18n-scan` `npm run i18n-read` 读取或者写入 i18n 文件\n-   `npm run count` 查看代码统计\n\n**更多功能请创建项目后查看项目的 README.md 文件**\n\n## 更新日志\n\n### v8.x 新功能\n\n-   支持`typescript@5`\n-   默认模板使用`MUI`组件库\n\n### v7.x 新功能\n\n-   支持`webpack@5`\n-   进一步提高编译性能\n\n### v6.x 新功能\n\n-   支持 [SSR](#ssr) 渲染\n-   同步`CRA 3.x`\n\n### v5.x 新功能\n\n-   集成`jest`测试\n\n### v4.x 新功能\n\n-   可以选择创建普通的开发项目，还是 npm 发布包项目\n\n### v3.x 新功能\n\n-   webpack 升级到 v4\n-   babel 升级到 v7\n-   eslint 升级到 v5\n-   更好的 typescript 支持\n\n### v2.x 新功能\n\n-   持久化缓存的优化\n-   webpack 升级到 2.x\n-   webpack-dev-server 的升级，带来更好的 proxy 支持\n\n## **SSR**\n\n`6.0` 起开始支持`SSR`渲染，感谢 [SSR support #6747](https://github.com/facebook/create-react-app/pull/6747) 这个 PR 带来的灵感。\n\n### 开发\n\n`SSR`是一个可选的功能，并且它与本身的纯静态构建完全兼容，甚至可以共存。要开启项目的`SSR`功能，你只需要添加一个同名入口文件，以`.node.[ext]`作为后缀即可:\n\n```diff\n  ├── app\n  │   ├── components\n  │   ├── hooks\n  │   ├── modules\n  │   ├── types\n  │   ├── utils\n  │   ├── index.tsx\n+ │   ├── index.node.tsx\n  │   ├── about.tsx\n+ │   └── about.node.tsx\n  ├── public\n  │   ├── index.html\n+ │   ├── index.node.html\n  │   └── service-worker.js\n```\n\n\u003e 上述示例以多入口项目做示例，一般来说单入口项目只需要添加一个`index.node.[ext]`即可。对于该示例来说，`/about/*` 的请求都会走`about.node.tsx`，其它请求走默认的`index.node.tsx`。\n\u003e\n\u003e `.node.html`模板是可选的，如果缺省，则以默认的`index.html`作为 SSR 入口模板。\n\n`index.node.[ext]`是一个导出接收 `templateFile` `request` `response` 三个参数的函数，用来做服务端渲染启动。\n\n-   `templateFile` 模板文件路径\n-   `request` `response` 即为 `HTTP request`和`HTTP response` 对象\n\n\u003e 注意：以上几个参数为本地开发环境默认传递的参数，但是服务器部署并一定要求使用 Express 或者必须按照该参数传递。事实上你完全可以自定义你的`index.node.[ext]`入口方法的函数定义，只要做好本地环境和服务器部署环境区分即可。\n\n基本的 `index.node.tsx` 里的内容大致如下：\n\n```typescript\nimport React from 'react';\nimport { renderToString } from 'react-dom/server';\nimport App from './App';\nimport fs from 'fs';\n\nconst renderer = async (templateFile, request, response) =\u003e {\n    // 读取模板文件内容\n    const template = fs.readFileSync(templateFile, 'utf8');\n    // 获取react组件的渲染字符串\n    const body = renderToString(\u003cApp /\u003e);\n    // 替换模板中占位注释字符，完成渲染初始化\n    const html = template.replace('\u003c!-- root --\u003e', body);\n\n    response.send(html);\n};\n\n// 请注意这里需要导出renderer方法\nexport default renderer;\n```\n\n### 发布\n\n要发布测试或者生产环境，依然是运行`npm run build:dev` 或者 `npm run pack`。但是与纯静态项目不一样的是，`SSR`的入口文件会放到 `BUILD_DIR/node` 路径下。\n\n你可以在项目下新建一个`server.js`文件，作为启动入口：\n\n```javascript\n// server.js\nconst path = require('path');\nconst express = require('express');\nconst renderer = require(resolveApp('node/index')).default;\nconst templateFile = resolveApp('node/index.html');\n\nconst app = express();\nconst port = process.env.PORT || 4000; // 默认端口\n\nfunction resolveApp(...dirs) {\n    const buildDir = process.env.NODE_ENV === 'development' ? 'buildDev' : 'build';\n\n    return path.join(process.cwd(), buildDir, ...dirs);\n}\n\n// 将 BUILD_DIR 作为静态资源目录\napp.use(\n    express.static(resolveApp(), {\n        index: false\n    })\n);\n\napp.use(async (req, res, next) =\u003e {\n    try {\n        await renderer(templateFile, req, res);\n    } catch (err) {\n        next(err);\n    }\n});\n\napp.listen(port);\n\nprocess.on('SIGINT', function () {\n    process.exit(0);\n});\n```\n\n再创建`pm2`的配置文件`pm2.config.js`：\n\n```javascript\n// pm2.config.js\nmodule.exports = {\n    // 区分生产和测试环境，分开配置\n    apps: [\n        {\n            name: 'my-ssr-app-prod',\n            script: 'server.js',\n            watch: false,\n            env: {\n                NODE_ENV: 'production',\n                PORT: 4100\n            },\n            instances: -1, // 生产环境使用最大cpu线程数减1，你可以自行修改其他数只或者'max'\n            exec_mode: 'cluster',\n            source_map_support: true,\n            ignore_watch: ['[/\\\\]./', 'node_modules']\n        },\n        {\n            name: 'my-ssr-app-dev',\n            script: 'server.js',\n            watch: false,\n            env: {\n                NODE_ENV: 'development',\n                PORT: 4100\n            },\n            instances: 1, // 测试环境只起一个线程，你也可以自行修改\n            source_map_support: true,\n            ignore_watch: ['[/\\\\]./', 'node_modules']\n        }\n    ]\n};\n```\n\n最终文件结构大概类似：\n\n```diff\n    ├── app\n    ├── build\n    ├── buildDev\n    ├── package.json\n    ├── public\n    ├── scripts\n+   ├── server.js\n+   ├── pm2.config.js\n    └── tsconfig.json\n```\n\n然后你就可以在服务器上通过`pm2`启动、管理你的应用服务：\n\n```bash\n# 一键启动、重载应用，因为我们在一个配置文件里配置了多个app，所以需要通过 `--only` 指定要启动的应用\n\n# development环境\npm2 reload pm2.config.js --only my-ssr-app-dev\n\n# production环境\npm2 reload pm2.config.js --only my-ssr-app-prod\n```\n\n**注意：构建时会同时生成 static 入口和 node 入口，你可以随时根据切换切换到 SSR 或者使用纯静态部署**\n\n### 路由与异步数据处理\n\n`tiger-new`的`SSR`功能仅提供了对相关入口文件的构建编译支持，并不包含更进一步的路由、异步数据处理等逻辑。但是这部分又是实际中比较常见的需求，这里提供一个基于 [`react-router-config`](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config) 和 [`withSSR`](#withssr) 实现的静态路由异步数据与`code splitting`异步组件的实现：\n\n\u003e **withSSR** 是 tiger-new 内置模板里提供的一个高阶组件，它提供了默认的 `withSSR`（给组件扩展 getInitialProps 异步数据处理） 以及 `prefetchRoutesInitialProps`（SSR 端获取匹配路由的异步数据和预加载异步组件）。\n\n**1. 提取路由配置**\n\n我们要将路由配置抽取出来，方便在服务端以及客户端共用。建议将路由配置统一放置到`stores/routes`：\n\n```typescript\n// stores/routes.ts\nimport { RouteItem } from 'utils/withSSR'; // 定义的路由配置项类型\nimport withLoadable from 'utils/withLoadable';\nimport Home from 'modules/Home';\n\n/**\n * 按需加载组件\n *\n * withLoadable是tiger-new内置的异步组件方法，但是这是可选的；\n * 事实上只要异步组件具有一个静态方法 `loadComponent` 即可，这个用于在SSR端预加载组件。\n *\n * 注：'modules/About' 其实是个聚合导出，为了方便统一对这几个组件做按需加载: export { About, AboutUs, AboutCompany }\n */\nconst About = withLoadable(() =\u003e 'modules/About', 'About');\nconst AboutUs = withLoadable(() =\u003e 'modules/About', 'AboutUs');\nconst AboutCompany = withLoadable(() =\u003e 'modules/About', 'AboutCompany');\n\nconst routes: RouteItem[] = [\n    {\n        path: '/',\n        exact: true,\n        component: Home\n    },\n    {\n        path: '/about',\n        component: About,\n        routes: [\n            {\n                path: '/about/us',\n                component: AboutUs\n            },\n            {\n                path: '/about/company',\n                component: AboutCompany\n            }\n        ]\n    }\n];\n\nexport default routes;\n```\n\n**2. CSR 与 SSR 入口处理**\n\nCSR 入口：\n\n```typescript\n// app/index.tsx\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport { BrowserRouter, Route } from 'react-router-dom';\nimport { renderRoutes } from 'react-router-config';\nimport routes from 'stores/routes';\n\n/**\n * CSR端也需要使用 react-router-config 的 renderRoutes 方法渲染路由\n */\nReactDOM[__SSR__ ? 'hydrate' : 'render'](\n    \u003cBrowserRouter\u003e\n        \u003cdiv className=\"app\"\u003e{renderRoutes(routes)}\u003c/div\u003e\n    \u003c/BrowserRouter\u003e,\n    document.getElementById('wrap')\n);\n```\n\nSSR 入口：\n\n```typescript\n// app/index.node.tsx\nimport fs from 'fs';\nimport React from 'react';\nimport { renderToString } from 'react-dom/server';\nimport { StaticRouter, Route, StaticRouterContext } from 'react-router';\nimport { renderRoutes } from 'react-router-config';\nimport type { Request, Response } from 'express'; // 引入ts类型定义，js可以省略\nimport App from 'modules/App';\nimport routes from 'stores/routes';\nimport { prefetchRoutesInitialProps } from 'utils/withSSR';\n\n/**\n * 拦截未捕获的的promise rejection异常，避免报错\n */\nif (!global.__handledRejection__) {\n    global.__handledRejection__ = true;\n\n    process.on('unhandledRejection', () =\u003e {\n        // @TODO 待查明具体的rejection来源，你可以在这里加入对异常的处理逻辑\n    });\n}\n\nconst renderer = async (templateFile: string, request: Request, response: Response) =\u003e {\n    /**\n     * 获取页面初始数据以及预加载异步组件\n     */\n    const initialProps = await prefetchRoutesInitialProps(routes, request.url, request, response, {\n        // 如果有其它数据希望传递给getInitialProps方法，可以在这里指定。没有可以缺省该参数\n    });\n    /**\n     * ctx为一个包含 initialProps 的对象，需要传递给StaticRouter的context属性\n     * 这一点很重要，确保我们的页面组件可以拿到初始化的数据\n     */\n    const ctx: StaticRouterContext = {\n        initialProps\n    };\n\n    let template = fs.readFileSync(templateFile, 'utf8');\n    /**\n     * SSR端需要使用StaticRouter\n     * 并且需要使用 react-router-config 的 renderRoutes 方法渲染路由\n     *\n     * !!!!! 注意StaticRouter的context属性一定不能忘了哦\n     */\n    let body = renderToString(\n        \u003cStaticRouter location={request.url} context={ctx}\u003e\n            \u003cdiv className=\"app\"\u003e{renderRoutes(routes)}\u003c/div\u003e\n        \u003c/StaticRouter\u003e\n    );\n    /**\n     * 将页面的初始数据通过 __DATA__ 渲染到页面上，让 CSR 端的组件读取，以实现同构渲染\n     */\n    let html = template\n        .replace('%ROOT%', body)\n        .replace('%DATA%', `var __DATA__=${initialProps ? JSON.stringify(initialProps) : 'null'}`);\n\n    /**\n     * 处理页面重定向\n     */\n    if (ctx.url) {\n        response.redirect(ctx.url);\n    } else {\n        response.send(html);\n    }\n};\n\nexport default renderer;\n```\n\n**3. 使用 [`withSSR`](#withssr) 高阶组件给路由页面组件绑定数据获取方法**\n\n我们的页面组件应该尽可能依赖于从其`props`中获取相关页面所需数据，减少其内部自身的数据获取逻辑。\n\n```typescript\n// app/modules/Home\nimport React from 'react';\nimport withSSR, { SSRProps } from 'utils/withSSR';\n\nconst Home: React.FC\u003c\n    SSRProps\u003c{\n        homeData: any;\n    }\u003e\n\u003e = props =\u003e {\n    return \u003cdiv className=\"home\"\u003e{props.homeData}\u003c/div\u003e;\n};\n\nexport default withSSR(Home, async () =\u003e {\n    const homeData = await fetch('/api/home');\n\n    return {\n        homeData\n    };\n});\n```\n\n以上三步配置完，即可实现`SSR`与`CSR`的同构渲染。\n\n### `withSSR`\n\n`utils/withSSR` 是 tiger-new 的项目模板中自带的一个用于 `SSR` 数据与路由处理的解决方法。（老项目中如果不存在这个文件，需要自行下载添加：[`utils/withSSR`](https://github.com/qiqiboy/tiger-new/blob/master/template/application/app/utils/withSSR)\n\n它包含一个高阶组件`withSSR`和一个 SSR 端用于预取数据的方法`prefetchRoutesInitialProps`。\n\n**withSSR(WrappedCompoennt, getInitialProps)**\n\n这是一个高阶组件，其 TS 签名如下:\n\n```typescript\ntype SSRProps\u003cMore\u003e = {\n    __error__: Error | undefined;\n    __loading__: boolean;\n    __getData__(extraProps？: {}): Promise\u003cvoid\u003e;\n} \u0026 More;\n\ninterface SSRInitialParams extends Partial\u003cOmit\u003cRouteComponentProps, 'match'\u003e\u003e {\n    match: RouteComponentProps\u003cany\u003e['match'];\n    parentInitialProps: any;\n    request?: Request;\n    response?: Response;\n}\n\nfunction withSSR\u003cSelfProps, More = {}\u003e(\n    WrappedComponent: React.ComponentType\u003cSelfProps \u0026 SSRProps\u003cMore\u003e\u003e,\n    getInitialProps: (props: SSRInitialParams) =\u003e Promise\u003cMore\u003e\n): React.ComponentType\u003cOmit\u003cSelfProps, keyof SSRProps\u003cMore\u003e\u003e\u003e;\n```\n\n`withSSR`会向组件传递`getInitialProps`返回的对象，以及 `__loading__` `__error__` `__getData__` 等三个属性，你可以用这几个属性来处理异步状态。\n\n第二个参数 `getInitialProps` 接受一个对象参数，该方法在 SSR 和 CSR 端都会被调用，所以参数略有不同：\n\n-   node 环境，包含 `request` 和 `response` 对象，不包含 `location` `history`\n-   browser 环境，包含 `location` `history`对象，不包含 `request` 和 `response`\n-   `match` 和 `parentInitialProps` 无论哪个环境都存在\n\n`getInitialProps`应该返回一个包含要传递给组件`object`，它会和组件上层传递的 props 对象合并后传递给当前组件，当前组件就可以通过 props 获取相关数据（注意，如果是从异步调用数据，`getInitialProps` 则需要返回 Promise 对象容器）:\n\n-   如果`getInitialProps`返回空值，例如`null` `undefined`，则表示不在 server 端输出渲染，在页面加载后会在浏览器端重新获取数据\n-   你不需要特别处理`getInitialProps`的异常，如果`getInitialProps`内部调用有异常发生，出错信息会放到 `{ __error__: Error }`传递给组件\n-   同样的组件也会接收`{ __loading__: true }`，如果`getInitialProps`是异步返回数据的话\n-   `getInitialProps` 也会通过 `{ __getData__ }` 传递给组件，方便在组件内部发起重新调用\n\n```typescript\nwithSSR(MyComponent, () =\u003e\n    fetch('/data.json').then(resp =\u003e ({\n        data: resp.toJSON()\n    }))\n);\n\n/**\n * async/await语法，同上\n */\nwithSSR(MyComponent, async () =\u003e {\n    const resp = await fetch('/data.json');\n    return { data: resp.toJSON() };\n});\n\n/**\n * 动态 path 路由，数据需要根据路由参数获取\n */\nwithSSR(UserDetail, async ({ match }) =\u003e {\n    const resp = await fetch(`/api/user/${match.params.userid}`);\n\n    return { userData: resp.toJSON() };\n});\n\n/**\n * 嵌套子路由路由，子路由的数据请求依赖于父级路由的数据\n * 这里假设父级路由获取userList数据，该UserDetail组件获取并渲染userList的第一项对应的数据\n * 通过 parentInitialProps 可以拿到父级路由的初始props对象\n */\nwithSSR(UserDetail, async ({ parentInitialProps }) =\u003e {\n    const resp = await fetch(`/api/user/${parentInitialProps.userList[0].id}`);\n\n    return { userData: resp.toJSON() };\n});\n```\n\n\u003e **TypeScript 注意事项：** 如果使用 ts 开发，请使用 `withSSR` 包装的组件需要通过 `SSRProps\u003c{}\u003e` 来声明组件的 props 类型，这样就可以在组件内部安全的通过 props 访问 `passToComponentPropName` `__error__` `__loading__` `__getData__` 等属性了\n\n```typescript\nconst MyComp: React.FC\u003c\n    SSRProps\u003c{\n        passToComponentPropName: string;\n    }\u003e \u0026\n        RouteComponentProps\n\u003e = props =\u003e {\n    if (props.__loading__) {\n        return 'loading...';\n    }\n\n    if (props.__error__) {\n        return props.__error__.message;\n    }\n\n    return \u003cdiv\u003e{props.passToComponentPropName}\u003c/div\u003e;\n};\n\nexport default withSSR(MyComp, async () =\u003e ({\n    passToComponentPropName: 'I am good!'\n}));\n```\n\n**prefetchRoutesInitialProps**\n\n`prefetchRoutesInitialProps` 用于在 SSR 端预加载通过 `withSSR` 绑定了 `getInitialProps` 方法的组件。它支持嵌套路由。\n\n当匹配到嵌套路由时，它会预先调用父级路由的 `getInitialProps`，然后将结果(`parentInitialProps`)和子路由的匹配信息(`match`等对象)一起传递给子路由的`getInitialProps`。这是一个递归过程，支持多级路由。\n\n```typescript\nfunction prefetchRoutesInitialProps(\n    routes: RouteItem[],\n    url: string,\n    request: any,\n    response: any,\n    extendProps?: object\n): Promise\u003c{}\u003e;\n```\n\n具体使用示例请参考上方 [路由与异步数据处理](#路由与异步数据处理)\n\n### 国际化 i18n\n\n`tiger-new`的模板中带了一个`utils/i18n`模块，用于处理多语言。如果要支持 SSR，需要注意的是，界面语言不可以通过全局的`__()`方法处理了，必须放到组件的生命周期中声明，并且通过`utils/i18n/withI18n`高阶组件传递的`i18n.__()`来处理多语言文案。\n\n**错误示例：**\n\n```typescript\n// About.tsx\nconst title = __('About Us'); // 错误，不可以在组件外直接通过全局 __() 调用语言包\n\nfunction AboutPage() {\n    return \u003cdiv\u003e{title}\u003c/div\u003e;\n}\n```\n\n**正确示例：**\n\n```typescript\n// About.tsx\nimport withI18n, { I18nProps } from 'utils/i18n/withI18n';\n\nfunction AboutPage(props: I18nProps) {\n    const title = props.i18n.__('About Us');\n\n    return \u003cdiv\u003e{title}\u003c/div\u003e;\n}\n\nexport default withI18n(About);\n```\n\n另外服务端入口需要提供下`withI18n`依赖的 i18n 上下文入口：\n\n```typescript\nimport type { Request, Response } from 'express'; // 引入ts类型定义，js可以省略\nimport cookick from 'cookick'; // cookick是utils/i18n模块使用的cookie解析模块\nimport { createI18n, context as i18nContext } from 'utils/i18n';\n\nconst renderer = async (templateFile: string, request: Request, response: Response) =\u003e {\n    // 如果不是html页面请求，忽略；或这里执行其它逻辑处理\n    if (!request.accepts().includes('text/html')) {\n        return response.status(404).end();\n    }\n\n    /**\n     * 设置cookick的解析来源\n     */\n    cookick.updateCookieSource(request.headers.cookie || '');\n\n    /**\n     * 创建一个新的适用于服务端的i18n对象\n     * 注意，这里并没有指定createI18n的第三、四个参数，是因为设置cookie由浏览器端js完成即可\n     */\n    const i18n = createI18n(request.url, request.get('accept-language') || '');\n\n    const initialProps = await prefetchRoutesInitialProps(routes, request.url, request, response, {\n        // 将i18n对象作为getInitialProps的额外参数传递，这样就可以在getIntialProps中也可以访问i18n对象了\n        i18n\n    });\n    const ctx: StaticRouterContext = {\n        initialProps\n    };\n\n    let body = renderToString(\n        \u003cStaticRouter location={request.url} context={ctx}\u003e\n            {/* 这里需要通过i18nContext.Provider将i18n向下传递，供withI18n模块访问 */}\n            \u003ci18nContext.Provider value={i18n}\u003e\n                \u003cApp /\u003e\n            \u003c/i18nContext.Provider\u003e\n        \u003c/StaticRouter\u003e\n    );\n\n    let html = template\n        .replace('%ROOT%', body)\n        .replace('%DATA%', `var __DATA__=${initialProps ? JSON.stringify(initialProps) : 'null'}`);\n\n    if (ctx.url) {\n        response.redirect(ctx.url);\n    } else {\n        response.send(html);\n    }\n};\n```\n\n### 注意事项\n\n-   `SSR`功能并不包含对任何`web` `node`运行时环境的兼容处理，你应当注意自己的代码的环境兼容性\n-   `SSR`功能并不包含任何路由的处理，如果有需要，你需要自行解决（使用 react-router 比较容易解决，参考[路由与异步数据处理](#路由与异步数据处理))\n-   `SSR`功能并不包含任何页面初始化异步数据的处理，如果有需要，你需要自行解决(参考[路由与异步数据处理](#路由与异步数据处理))\n-   `SSR`功能并不包含任何其它对于`SEO`场景的处理，如果有需要，你需要自行解决（建议使用 [`react-helmet`](https://github.com/nfl/react-helmet)，它支持`SSR`）\n-   `mobx-react`在`SSR`场景下会导致内存泄漏，请参考[Server Side Rendering with `useStaticRendering`](https://github.com/mobxjs/mobx-react#server-side-rendering-with-usestaticrendering)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqiqiboy%2Ftiger-new","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqiqiboy%2Ftiger-new","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqiqiboy%2Ftiger-new/lists"}