Code Monkey home page Code Monkey logo

egg-react-ssr's Introduction

简体中文 | English

Egg + React + SSR boilerplate

Build Status Coverage Status download standardjs License License Node

注: 出于功能丰富度以及可维护性考虑,不再建议使用 egg-react-ssr。现推荐使用最新的 ssr 框架能够同时支持在 React, Vue2/3 场景下使用且支持使用 Vite。我们强烈建议你使用新的升级版,无论是渲染机制还是功能丰富度以及可维护性上都有极大提升并经过多个大型线上项目验证。如果你更喜欢开箱即用的体验且希望借助 Serverless 能力一键部署应用或使用 Vue2/Vue3 来做服务端渲染。项目链接 https://github.com/zhangyuang/ssr 官方文档 http://doc.ssr-fc.com/ 我们将会持续更新维护它

最小而美的服务端渲染应用模板,特点

  • 小:实现方式简洁,生产环境构建出来的 bundle 为同等复杂度的 next.js 项目的 0.7 倍,生成文件数量相比于 next.js 减少非常多
  • 全:支持 HMR,支持本地开发以及生产环境 CSR/SSR 两种渲染模式无缝切换,支持定制组件的渲染模式,同时支持 TypeScript 版本
  • 美:基于ReactEggjs框架,拥有强大的插件生态,配置非黑盒,方便加入当前业务的个性化逻辑

正在使用这个项目的公司(应用)名单,按新增时间排序, 如果您正在使用但名单中没有列出来的话请提 issue,欢迎推广分享。

优酷
优酷视频
vmate 积分商城
Vmate短视频
火炽星原CRM
火炽星原CRM
牛牛搭
牛牛搭
cvte
希沃帮助中心
腾讯微卡
腾讯微卡
微脉
微脉
腾讯手游助手
腾讯手游助手
国家现代农业科技创新中心
国家现代农业科技创新中心
国盛证券
国盛证券
极速二维码
极速二维码
100教育
100教育

快速入门

我们使用 create-ssr-app 来快速的创建项目, 支持创建集成了 (js|ts|antd|dva) 等多种功能的应用模版

$ npm init ssr-app my-ssr-project --template=ssr-with-js
$ cd my-ssr-project
$ npm install
$ npm start

注:当 Node.js version >=15 时,应使用 npm init ssr-app my-ssr-project -- --template=ssr-with-js 来传递参数

npm scripts

1)启动服务

启动监听 7001 端口,此端口同时用于服务端渲染以及客户端渲染,通过 query 或者 config 来指定渲染模式

$ npm start # 建议以本方式启动应用,同时启动服务端渲染 + 客户端hydrate

2)只启动服务端渲染,此时仅服务端直出 html,没有与客户端混合的步骤

$ npm run ssr

3)启动客户端静态资源编译

仅限于本地开发使用,启动监听 8000 端口托管前端静态资源,相当于传统的 cra 脚手架开发模式

$ npm run csr

4)配套的脚本

$ npm run prod    # 使用egg-scripts模拟SSR应用生产环境,如无特殊定制要求生产环境可以用该方式启动
$ npm run build   # 打包服务端以及客户端资源文件
$ npm run analyze # 可视化分析客户端打包的资源详情

功能/特性

该模板特色为:写法简单、功能强大、一切都是组件、支持 SSR/CSR 两种渲染模式无缝切换。

更多功能/特性如下:

  • 基于 cra 脚手架开发,由 cra 开发的 React App 可无缝迁移,如果你熟悉 cra 的配置,上手成本几乎为 0
  • 小而美,相比于 beidou,next.js 这样的高度封装方案,我们的实现原理和开发模式一目了然
  • 推荐使用 egg 作为 Node.js 框架但并不强制,事实上你可以发现几乎无需做任何修改即可迁移到 koa,nest.js 等框架
  • 同时支持 SSR 以及 CSR 两种开发模式,本地开发环境以及线上环境皆可无缝切换两种渲染模式
  • 统一前端路由与服务端路由,无需重复编写路由文件配置
  • 支持切换路由时自动获取数据
  • 支持本地开发 HMR
  • 稳定性经过线上大规模应用验证,可提供性能优化方案
  • 支持 tree shaking,优化构建 bundle 大小以及数量
  • 支持 csr/ssr 自定义 layout,无需通过 path 来手动区分
  • 抛弃传统模版引擎,拥抱 React 组件,使用 JSX 来作为模版
  • 配套结合antd的 example 的实现
  • 配套结合react-loadable做路由分割的 example 的实现
  • 配套结合dva做数据管理的 example 的实现
  • 配套结合ssr-with-multipage多页面应用的 example
  • 配套结合Rax版本的实现
  • 配套TypeScript版本的实现

结合 Serverless

我们在 Serverless 场景下的SSR 框架已经正式发布,如果你更喜欢平滑开箱即用体验的更高层次解决方案并且希望能够快速部署。推荐使用该框架进行开发。与本项目不冲突,互相补位

写法

在写法上统一 csr 和 ssr,采用 next 类似的静态的 getInitialProps 作为数据获取方法

function Page(props) {
  return <div> {props.name} </div>
}

Page.getInitialProps = async (ctx) => {
  return Promise.resolve({
    name: 'Egg + React + SSR',
  })
}

export default Page

具体说明如下。

  • render 是 React 的视图渲染方法
  • getInitialProps 是获取数据方法,将返回值赋值给组件状态
    • csr 通过高阶组件实现
    • ssr 通过 Node 执行

在运行时,通过config.type来进行区分,是目前最简单的同构渲染方案。当页面初始化加载时,getInitialProps 只会加载在服务端。只有当路由跳转(Link 组件跳转或 API 方法跳转)时,客户端才会执行 getInitialProps。

getInitialProps 入参对象的属性如下:

  • ctx: Node 应用请求的上下文(仅在 SSR 阶段可以获取)
  • Router Props: 包含路由对象属性,包括 pathname 以及 Router params history 等对象,详细信息参考 react-router 文档

一切皆组件

我们的页面基础模版 html,meta 等标签皆使用 JSX 来生成,避免你去使用繁琐的模版引擎语法

const commonNode = (props) =>
  // 为了同时兼容ssr/csr请保留此判断,如果你的layout没有内容请使用 props.children ? { props.children } : ''
  props.children ? (
    <div className="normal">
      <h1 className="title">
        <Link to="/">Egg + React + SSR</Link>
        <div className="author">by ykfe</div>
      </h1>
      {props.children}
    </div>
  ) : (
    ''
  )

const Layout = (props) => {
  if (__isBrowser__) {
    return commonNode(props)
  } else {
    const { serverData } = props.layoutData
    const { injectCss, injectScript } = props.layoutData.app.config
    return (
      <html lang="en">
        <head>
          <meta charSet="utf-8" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1, shrink-to-fit=no"
          />
          <meta name="theme-color" content="#000000" />
          <title>React App</title>
          {injectCss &&
            injectCss.map((item) => (
              <link rel="stylesheet" href={item} key={item} />
            ))}
        </head>
        <body>
          <div id="app">{commonNode(props)}</div>
          {serverData && (
            <script
              dangerouslySetInnerHTML={{
                __html: `window.__USE_SSR__=true; window.__INITIAL_DATA__ =${serialize(
                  serverData
                )}`,
              }}
            />
          )}
          <div
            dangerouslySetInnerHTML={{
              __html: injectScript && injectScript.join(''),
            }}
          />
        </body>
      </html>
    )
  }
}

如何切换渲染模式

在本地开发时,你可以同时启动 ssr/csr 两种渲染模式查看区别,在生产环境时,你可以通过设置 config 中的 type 属性来切换不同的渲染模式或者通过 query 来切换,在流量较大时可以降级为 csr 渲染模式 参考文档如何切换渲染模式

$ open http://localhost:7001/          # 以SSR模式渲染应用
$ open http://localhost:7001/?csr=true # 切换为CSR模式渲染或者通过config.type来设置渲染模式

执行环境

  • 服务器 Node.js >= 7.6, 为了原生的使用 async/await 语法
  • 浏览器版本大于等于 IE9, React 支持到 IE9,但为了更好的在 IE 下使用,你可能需要引入Polyfill

执行流程

运行配置

为了足够灵活使用,这里我们将一些关键项提供可配置的选项,可根据实际需要来配置,如无特殊必要,使用默认配置即可。服务端渲染相关配置信息我们放在 config.ssr.js,在这里我们建议不要将配置放在 egg 的配置文件当中,避免前端 bundle 中包含后端配置文件信息

// config/config.ssr
const resolvePath = (path) => require('path').resolve(process.cwd(), path)

module.exports = {
    type: 'ssr', // 指定运行类型可设置为csr切换为客户端渲染,此时服务端不会做获取数据生成字符串的操作以及不会使用hydrate API
    static: {
        // 设置Node应用的静态资源目录,为了生产环境读取静态资源文件
      prefix: '/',
      dir: resolvePath('dist')
    },
    routes: [
        // 前后端统一使用的路由配置文件,防止重复编写相同的路由
      {
        path: '/', // 请求的path
        exact: true, // 是否精确匹配
        Component: () => (require('@/page/index').default), // 这里使用一个function包裹为了让它延迟require, 否则Node环境无法识别前端组件中用到的import关键字会报错
        controller: 'page', // 需要调用的controller
        handler: 'index' // 需要调用的controller中具体的method
      },
      {
        path: '/news/:id',
        exact: true,
        Component: () => (require('@/page/news').default),
        controller: 'page',
        handler: 'index'
      }
    ],
    injectCss: [
    `/static/css/Page.chunk.css`
  ], // 客户端需要加载的静态样式表
  injectScript: [
    `<script src='/static/js/runtime~Page.js'></script>`,
    `<script src='/static/js/vendor.chunk.js'></script>`,
    `<script src='/static/js/Page.chunk.js'></script>`
  ], // 客户端需要加载的静态资源文件表
  serverJs: resolvePath(`dist/Page.server.js`): string|function, // 打包后的server端的bundle文件路径支持传入CDN地址, 接受直接传入require后的function
  layout: resolvePath(`dist/Layout.server.js`): string|function // 打包后的server端的bundle文件路径支持传入CDN地址, 接受直接传入require后的function
}

开发配置

修改默认 webpack-dev-server的配置

// build/webpack.config.client.js

module.epxorts = {
  devServer: {
    // custom webpack-dev-server config
  }
}

目录结构

目录结构保持了 Egg 的方式,以 app 和 config 目录为主。将前端 React 相关代码放到 web 目录下,webpack 打包相关文件位于 build 目录。整体来看,目录不多,层级不深,属于刚刚好那种。

├── README.md
├── app # egg核心目录
│   ├── controller
│   ├── extend
│   ├── middleware
│   └── router.js # egg路由文件,无特殊需求不需要修改内容
├── app.js # egg 启动入口文件
├── build # webpack配置目录
│   ├── paths.js
│   ├── util.js
│   ├── webpack.config.base.js # 通用的webpack配置
│   ├── webpack.config.client.js # webpack客户端打包配置
│   └── webpack.config.server.js # webpack服务端打包配置
├── config # egg 配置文件目录
│   ├── config.daily.js
│   ├── config.default.js
│   ├── config.ssr.js
│   ├── config.local.js
│   ├── config.prod.js
│   ├── plugin.js
│   └── plugin.local.js
├── dist # build生成静态资源文件目录
│   ├── Page.server.js # 服务端打包后文件(即打包后的serverRender方法)
│   └── static # 前端打包后静态资源目录
└── web # 前端文件目录
    ├── assets
    │   └── common.less
    ├── entry.js # webpack打包入口文件,分环境导出不同配置
    ├── layout
    │   ├── index.js # 页面布局
    │   └── index.less
    └── page
        ├── index
        └── news

Changelog

每一个版本的详细改动请查看 release notes

与其他方案的对比

本地如何调试源码

请查看该wiki

如何向本项目贡献代码

请查看该wiki

Contributors

Thanks goes to these wonderful people (emoji key):


LeonCheung

💻

狼叔

💻

Xu Zhiyong

🐛

Menteceso

📖

jerryYu

💻

dydong

💻

snoy

📖

zhaoxingyue

📖

九牧

🐛

JohannLai

🐛

robert.xu

💻

zhushijie

💻

Cheng Zhongmin

🐛

This project follows the all-contributors specification. Contributions of any kind welcome!

License

MIT

NodeParty 分享

如果你想了解本应用的设计思路,欢迎下载查看本人在 2020.1.11 日在北京 NodeParty 上所做的分享PPT,其中讨论了需要关注的一些问题的设计思路和解决方案的选取

答疑群

虽然我们已经尽力检查了一遍应用,但仍有可能有疏漏的地方,如果你在使用过程中发现任何问题或者建议,欢迎提issue或者PR 欢迎直接扫码加入钉钉群

项目 Star 数增长趋势

Stargazers over time

egg-react-ssr's People

Contributors

allcontributors[bot] avatar anthinkingcoder avatar bs32g1038 avatar dean16930 avatar dependabot[bot] avatar frankfan avatar gogogo1024 avatar i5ting avatar jerryyux avatar johannlai avatar johniexu avatar juzhiyuan avatar kingstone3 avatar lyule avatar macbesu avatar nuintun avatar stone-jin avatar tkgkn avatar xieww avatar zhangyuang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

egg-react-ssr's Issues

[RFC] 新版目录结构规划

目录

$ tree .
.
├── README.md
├── bin
│   └── cli.js
├── config
│   ├── env.js
│   ├── jest
│   │   ├── cssTransform.js
│   │   └── fileTransform.js
│   ├── paths.js
│   ├── util.js
│   ├── webpack.config.base.js
│   ├── webpack.config.client.js
│   └── webpack.config.server.js
├── doc
├── example
│   ├── app
│   │   ├── controller
│   │   │   └── page.js
│   │   └── router.js
│   ├── app.js
│   ├── config
│   │   ├── config.daily.js
│   │   ├── config.default.js
│   │   ├── config.local.js
│   │   ├── config.prod.js
│   │   ├── plugin.js
│   │   └── plugin.local.js
│   ├── dist
│   │   ├── Page.server.js
│   │   └── static
│   │       └── css
│   │           └── Page.css
│   ├── package.json
│   └── web
│       ├── assets
│       │   └── common.less
│       ├── entry.js
│       ├── index.html
│       ├── layout
│       │   ├── index.js
│       │   └── index.less
│       └── page
│           ├── index
│           │   ├── index.js
│           │   └── index.less
│           └── news
│               ├── index.js
│               └── index.less
├── package.json
└── src
    ├── index.js
    └── tpl
        ├── js
        │   ├── entry.js
        │   └── package.json
        └── ts
            ├── entry.js
            └── package.json

21 directories, 37 files

说明

  • bin是脚手架命令
  • example是示例,复用config目录下的webpack配置
  • doc是文档,暂定采用vuepress
  • src是脚手架相关代码,主要是src/tpl下,会提供js和ts二个版本的模板文件

webpack treeskaing 相关bug

image
这几天为了兼容serverless场景对renderToStream方法进行了一些改造,经过测试发现,renderToStream.js期望的被调用环境是在服务端,但是在打包客户端资源时,虽然我们已经启用了tree shaking, 并且打包的结果也并没有renderToStream方法相关代码,但是打包分析的时候以及本地开发模式下,webpack仍然会去分析使用未import的代码,故导致报错

目前想的解决方式是只将客户端或者双端能够通用的文件在ykfe-utils中export出来,而renderToStream不export,改为

const renderToStream = require('ykfe-utils/lib/renderToStream')

上面这种方式来引入具体的文件

如果你有更好的解决方式,欢迎评论

feat: 使用jsx 来代替模版引擎

无论是使用模版引擎还是用目前的锚点+replace方式来拼接成完整的html文档,实现起来都很别扭。需要自己来定义如何组装成完成的页面。
如果使用react jsx来做外层模版,则直接renderToStream(<layout><page /></layout>)就可以直接得到完整的页面文档了。而且jsx的写法比起模版引擎的写法无疑要舒服很多。
并且这种做法的好处是可以用使用者自由决定页面应该加入哪些额外的标签,而不用通过约定配置项来决定。之前的做法是通过约定config,再将config的值插入到锚点中,这样的做法不够灵活

目前

  baseHtml.replace('<!-- Start Server Render Head -->', config.head ? config.head.join('') : '')

  const docArr = baseHtml.split('<!-- Start Server Render Document -->')

  const beginDoc = docArr[0].trim().replace('\n', '')
  const beginDocStream = stringToStream(beginDoc.replace('<!-- Start Injecting Style Flows Up and Down -->', `${config.injectCss(chunkName).join('')}`))
  const initialData = !isCsr ? `<script>window.__USE_SSR__=true;window.__USESSR__=true;window.__INITIAL_DATA__ =${serialize(ctx.serverData || {})};</script>` : ''
  const injectScript = config.injectScript ? config.injectScript(chunkName).join('') : config.injectSrcipt(chunkName).join('')

  const endDoc = docArr[1].trim().replace('\n', '')
  const endDocStream = stringToStream(endDoc.replace('<!-- Start InitialData Script  -->', initialData).replace('<!-- Start Client Script -->', injectScript))
  const streamArr = isCsr ? [beginDocStream, endDocStream] : [beginDocStream, stream, endDocStream]
  return multiStream(streamArr)

改造后

if (__isBrowser__) {
    return <div id='app' ><div className='normal'><h1 className='title'><Link to='/'>Egg + React + SSR</Link><div className='author'>by ykfe</div></h1>{props.children}</div></div>
  } else {
    const { injectCss, injectScript, chunkName } = props.layoutData.app.config
    return (
      <html lang='en'>
        <head>
          <meta charSet='utf-8' />
          <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no' />
          <meta name='theme-color' content='#000000' />
          <title>React App</title>
          {
            injectCss && injectCss(chunkName).map(item => <link rel='stylesheet' href={item} />)
          }
        </head>
        <body>
          <div id='app' ><div className='normal'><h1 className='title'><Link to='/'>Egg + React + SSR</Link><div className='author'>by ykfe</div></h1>{props.children}</div></div>
          <div dangerouslySetInnerHTML={{
            __html: injectScript && injectScript(chunkName).join('')
          }} />
        </body>
      </html>
    )
  }

目前唯一的点是,如何使用react jsx组件来作为csr html-webpack-plugin的模版。目前的思路是在本地开发时,使用renderToString api来将组件编译成html字符串并且写入到本地index.html文件,给html-webpack-plugin用

Got a warnning in getInitialProps with a promise.

version

{
  ykfe-util:  "^1.1.0" 
}

I do like this:

Page.getInitialProps = () => {
  console.log('👽  Page.getInitialProps');
  return api.products.list().then(data => ({
    products: data,
  }));
};

api.products.list is like this:
return new Promise((resolve, reject) => {
return fetch(
${APISERVERHOST}/api/${this.name}.json?page=${option.page}&per_page=${option.perPage}
)
.then(response => {
if (response.status >= 400) {
reject('Bad response from server');
}
return resolve(response.json());
})
.then(data => {
return data;
})
.catch(err => {
throw new Error(err);
});
});

and got warnning:

WARNING in ./node_modules/ykfe-utils/es/renderToStream.js 34:25-60
[1] Critical dependency: the request of a dependency is an expression
[1]  @ ./node_modules/ykfe-utils/es/index.js
[1]  @ ./web/entry.js
[1] Child html-webpack-plugin for "index.html":

生产环境ssr与csr两种渲染模式的切换

现状:目前支持本地同时启动两个端口分别是服务端渲染和客户端渲染。但是客户端渲染应用还得手动build bundle扔到cdn发布,无法做到生产环境无缝切换两种渲染方式。当服务器压力过大时可以由服务端渲染模式切换为客户端渲染模式。
目的:希望支持生产环境两种渲染模式的无缝切换

关于[email protected]相关问题

现存问题及优化建议:

  1. js-sass版本在init过程,一直在“下载模版中...”,其实中断构建过程,运行项目是可以的,有待优化;

  2. 为了提高脚手架使用体验,建议在init完成后,新增初始化项目运行命令指南,例如:
    npm install npm run start
    而且注意到js与ts版本开发环境启动命令尚不同,能否统一,或者直接在脚手架初始化时做出命令指南,就不用去阅读package.json中脚本配置。只是建议,考虑下

新增config.ssr.js配置文件

目前配置项前后端都用到了config.default.js,这就会导致前端代码中会将config.default.js的内容打包进去,有可能会包含一些隐私配置,或者会将后端代码打进去。建议将ssr的相关通用配置统一放到单独的一个配置文件进行维护

ykcli init 报错

node 版本 v10.15.1

(node:92402) UnhandledPromiseRejectionWarning: undefined (node:92402) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1) (node:92402) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

npm run csr 报错

问题

[1] start clientRender
[1]
[1] Options:
[1]   --help         Show help                                             [boolean]
[1]   --version, -v                                       [boolean] [default: false]
[1]
[1] TypeError: Property key of ObjectProperty expected node to be of a type ["Identifier","StringLiteral","NumericLiteral"] but
instead got "BooleanLiteral"
[1]     at validate (C:\admin\github.com.project\pangu.admin\node_modules\_@babel_types@7.5.5@@babel\types\lib\definitions\utils.js:131:13)
[1]     at Object.validate (C:\admin\github.com.project\pangu.admin\node_modules\_@babel_types@7.5.5@@babel\types\lib\definitions\core.js:526:11)
[1]     at validateField (C:\admin\github.com.project\pangu.admin\node_modules\_@babel_types@7.5.5@@babel\types\lib\validators\validate.js:22:9)
[1]     at Object.validate (C:\admin\github.com.project\pangu.admin\node_modules\_@babel_types@7.5.5@@babel\types\lib\validators\validate.js:16:3)
[1]     at NodePath._replaceWith (C:\admin\github.com.project\pangu.admin\node_modules\_@babel_traverse@7.5.5@@babel\traverse\lib\path\replacement.js:194:9)
[1]     at NodePath.replaceWith (C:\admin\github.com.project\pangu.admin\node_modules\_@babel_traverse@7.5.5@@babel\traverse\lib\path\replacement.js:178:8)
[1]     at replaceAndEvaluateNode (C:\admin\github.com.project\pangu.admin\node_modules\_babel-plugin-transform-define@1.3.1@babel-plugin-transform-define\lib\index.js:117:12)
[1]     at processNode (C:\Lizhicheng\github.com.project\pangu.admin\node_modules\_babel-plugin-transform-define@1.3.1@babel-plugin-transform-define\lib\index.js:143:5)
[1]     at PluginPass.Identifier (C:\admin\github.com.project\pangu.admin\node_modules\_babel-plugin-transform-define@1.3.1@babel-plugin-transform-define\lib\index.js:23:9)
[1]     at newFn (C:\admin\github.com.project\pangu.admin\node_modules\_@babel_traverse@7.5.5@@babel\traverse\lib\visitors.js:193:21)

系统环境

node:v10.15.3
win10: v1803

版本依赖

  "dependencies": {
    "egg": "^2.23.0",
    "egg-proxy": "^1.1.0",
    "egg-scripts": "^2.11.0",
    "egg-static": "^2.2.0",
    "koa-router": "^7.4.0",
    "react": "16.9.0",
    "react-dev-utils": "^9.0.3",
    "react-dom": "16.9.0",
    "react-router-dom": "^5.0.1",
    "ykfe-utils": "^2.1.1"
 },
"devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/plugin-transform-runtime": "^7.5.5",
    "@babel/polyfill": "^7.4.4",
    "@babel/preset-env": "^7.5.5",
    "@babel/preset-react": "^7.0.0",
    "@babel/register": "^7.5.5",
    "@babel/runtime": "^7.5.5",
    "babel-loader": "8.0.6",
    "browserslist": "^4.7.0",
    "caniuse-lite": "1.0.30000989",
    "concurrently": "^4.1.2",
    "cross-env": "^5.2.1",
    "css-hot-loader": "^1.4.4",
    "css-loader": "3.2.0",
    "css-modules-require-hook": "^4.2.3",
    "egg-bin": "^4.13.1",
    "file-loader": "4.2.0",
    "less": "^3.10.3",
    "less-loader": "^5.0.0",
    "memory-fs": "^0.4.1",
    "mini-css-extract-plugin": "^0.8.0",
    "nodemon": "^1.19.2",
    "optimize-css-assets-webpack-plugin": "5.0.3",
    "postcss-flexbugs-fixes": "4.1.0",
    "postcss-loader": "3.0.0",
    "postcss-preset-env": "^6.7.0",
    "postcss-safe-parser": "4.0.1",
    "rimraf": "^3.0.0",
    "terser-webpack-plugin": "^2.0.0",
    "url-loader": "2.1.0",
    "webpack": "4.39.3",
    "webpack-bundle-analyzer": "^3.4.1",
    "webpack-cli": "^3.3.8",
    "webpack-dev-server": "3.8.0",
    "webpack-manifest-plugin": "^2.0.4",
    "webpack-merge": "^4.2.2",
    "webpack-node-externals": "^1.7.2",
    "yk-cli": "^2.2.1"
 }

项目来源(官方脚手架)

$ npm install yk-cli -g
$ ykcli init <Your Project Name>
$ cd <Your Project Name>
$ npm i
$ npm start
$ open http://localhost:7001

经过上面步骤尝试 测试 项目,发现命令行报错。后升级依赖包。问题仍然没解决。错误结果一样。

打包结果包含无用依赖

通过npm run analyze的分析我们可以看到,vendor.chunk.js中打包进了multistream等stream相关的库的依赖。这些库在client端是没有用到的,很明显我们使用import {} from 'ykfe-utils'时,没有做tree shaking的操作。以及发现react react-dom react-router等依赖出现了多次,说明没有做重复依赖的去重操作。

image

egg-react-ssr/example/ssr-with-antd/例子中build:client和build:server写入css文件造成覆盖

现象:
egg-react-ssr/example/ssr-with-antd/项目中,

build:client
popo_2019-08-28  20-50-56
build:server
f765543acd434c81a35b33fb1416b0ec_09b651fea17fe6a03afad39a41422bfa

3.chunk.css被覆盖了

初步分析:

// package.json
{
    "build": "rimraf dist && ykcli build && npm run build:server",
}

同时调用了webpack.config.base.js先后写入css文件,造成覆盖

//  build/webpack.config.base.js    
new MiniCssExtractPlugin({
      filename: 'static/css/[name].css',
      chunkFilename: 'static/css/[name].chunk.css'
})

支持嵌套路由吗?

想在 index page 里面嵌套子的 react router,发现不工作,代码如下:

     <input />
     <Link to="/home">home</Link>
      <Link to="/about">about</Link>
      <Route path="/home">
          <HomeTab />
        </Route>
        <Route path="/about">
          <AboutTab />
        </Route>
    </div>

然后尝试修改了 config.ssr.js,让多个路径匹配到相同组件,如下:

    {
      path: '/',
      exact: true,
      Component: () => (require('@/page/index').default), // 这里使用一个function包裹为了让它延迟require
      controller: 'page',
      handler: 'index'
    },
    {
      path: '/home',
      exact: true,
      Component: () => (require('@/page/index').default), // 这里使用一个function包裹为了让它延迟require
      controller: 'page',
      handler: 'index'
    },
    {
      path: '/about',
      exact: true,
      Component: () => (require('@/page/index').default), // 这里使用一个function包裹为了让它延迟require
      controller: 'page',
      handler: 'index'
    }

这样路由就能工作了,不过这样,他们共有的父节点,在每次切换路由时会更新,比如在上面的 input 组件里输入内容,再切换子路由,input 的输入会被清空

是否支持远程加载静态资源文件

目前示例里的前端页面代码都是放在web/page目录下的。通过config.ssr.js中的这块代码
Component: () => (require('@/page/index').default)去加载。
但是现在有一种场景就是,这些前端代码都是单独部署的独立应用,是否可以提供一种机制去加载这些独立应用编译打包后的js,css文件 @zhangyuang

add koa/express support

  • add packages/middleware,package name:ssr-middleware
  • add koa/koa middlwares

example

const conf = require('./config/config.ssr')
const ssr = require('egg-react-ssr').koa(conf);

const Koa = require('koa');
const app = new Koa();

//  mount routes from config
app.use(ssr)

// ctx.ssrRender()
app.use(async ctx => {
  ctx.ssrRender(...);
});

app.listen(3000);

injectScript支持类似webpack的[contenthash]

现象:
目前injectScript引入script时未带url查询参数或更新文件名,造成发布后http强缓存。
<script src='/static/js/Page.chunk.js'></script>
建议:
支持类似webpack的[contenthash]
<script src='/static/js/Page.chunk.[contenthash].js'></script>

PR is Welcome

List

目前存在以下功能需要完善,欢迎PR

  • 修复 ssr-with-loadable 进入详情页面刷新的warning // fix by this commit
  • 修复 ssr-with-mobx 修改store无法热更新的bug(暂时搁置,解法使用hooks与getInitialProps冲突)
  • yk-cli新增可以创建多个不同类型的example的功能
  • 补充yk-cli 单元测试
  • 补充ykfe-utils 单元测试

ssr中使用hooks报错

image
在ssr应用中使用react hooks会报上图错误。
查了一下原因是因为出现了多个实例的react-dom。发现将renderToStream方法移动到项目中就没事了。
google了一下类似的问题挺多的。#14898 #13972

访问 8000 端口 csr 模式,改动 web/layout 文件夹名称会报错,找不到 /node_modules/yk-cli/dist/Layout.server.js

报错如下:

(node:24001) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open '/data/workSpace/wecard-homepage-ssr/node_modules/yk-cli/dist/Layout.server.js'
[1]     at Object.openSync (fs.js:443:3)
[1]     at Object.readFileSync (fs.js:343:35)
[1]     at Object.Module._extensions..js (internal/modules/cjs/loader.js:788:20)
[1]     at Module.load (internal/modules/cjs/loader.js:653:32)
[1]     at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
[1]     at Function.Module._load (internal/modules/cjs/loader.js:585:3)
[1]     at Module.require (internal/modules/cjs/loader.js:692:17)
[1]     at require (internal/modules/cjs/helpers.js:25:18)
[1]     at renderLayout (/data/workSpace/wecard-homepage-ssr/node_modules/yk-cli/lib/renderLayout.js:49:18)
[1] (node:24001) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 6)

Windows下运行异常问题

经测试,项目在非Windows环境(Mac、Ubuntu)运行正常。但在windows环境下,由于Windows command无法正常设置NODE_ENV,会阻塞退出,导致webpack无法正常生成dist目录文件,因此无法正常运行项目。如下图:

image

解决方法:package.json脚本新增cross-env 抹平运行环境兼容问题

接下来的规划

现状

目前本应用能够通过脚手架新建比较稳定的运行,目前该配置可以解决大部分项目的基本需求,但为了应付灵活多变的需求,我们还需要不断的完善本应用所支持的场景,目前初步列出以下待办事项

数据流

提供结合多种流行的数据管理方式的example

  • 结合dva的example
  • 结合mobx的example

组件到达时间统计

服务端渲染应用的首屏时间是一个很难通过浏览器提供的API来直观看出的数据,我们必须在应用中手动触发时间统计埋点,如何让开发者做到无感知的给应用组件中添加时间统计的埋点也是一项有价值的任务,以上一个页面的unload时间为基准,即performance.timing.navigationStart的时间,目前只支持页面级别的组件信息统计

  • 当前应用在服务端的一些性能监控数据
  • 显示该组件在服务端被渲染完毕的时间
  • 显示该组件在浏览器中出现的时间

结合loadable

将bundle能够根据路由来分割能够进一步的加快首屏domContentLoaded的时间,不过对首屏时间的影响不大,但loadable结合SSR应用定义的getInitialProps方法来作为数据获取的方法,并没有那么简单,需要修改react-loadable源码

  • 结合loadable进行路由分割(example已发布,但仍有完善空间

更细粒度的组件获取

我们目前的getInitalProps方法只支持在路由级别的"大"组件中调用生效,如果能够支持在每一个更小粒度的组件也是通过该方法去获取数据,那我们就可以更精细的控制一个组件的渲染逻辑,究竟要放在服务端还是客户端。将首屏性能做到极致

  • getInitalProps方法支持更细粒度的组件

多页面应用

目前给的example是单页面应用,如果要做多页面应用实质上是多个entry分别打包为单独的bundle,但如何做到配置更加灵活,更加轻便是个不小的难题,以及需要讨论既然ssr已经解决了seo的问题,就解决了多页面应用的一个优势。ssr + 路由分割理论上已经足够好用了,而多页面应用的缺点却很多,所以得考虑当前应用究竟有没有必要做成多页面应用的必要。

  • 讨论是否需要支持多页面应用

添加测试

  • 单元测试
  • ci
  • e2e测试

ts化

  • ykfe-utils 使用ts重写

支持单个小组件的getInitialProps方法的调用

目前getInitialProps只支持路由级别的大组件才会被调用,next.js也是这样。
如果可以支持定义每一个小组件也使用getInitialProps来定义获取数据的逻辑,我们就可以精细的控制一个组件的获取数据以及渲染的过程,到底要在服务端做还是在客户端做。可以根据实际需求,来让需要优先展示的模块放在服务端渲染,次级模块在客户端渲染。将性能做到极致

请教一下,ssr-with-ts 在 config.ssr.js 引入一个配置文件后,csr 打开空白,ssr没问题。

可能是我的打开方式不对,复现 demo 如下,麻烦帮忙看看~ 感谢🙏

https://github.com/JohannLai/ssr-with-ts/commit/00f32189dc3e4978cbb1950d0cf450606bda9825

还增加了这个 文件https://github.com/JohannLai/ssr-with-ts/blob/master/config/config.default.js

原意是想在 page 底下写路由文件,然后引用整理放到 config.ssr.js 中,使到页面和路由不分开

使用mobx的demo时出现些问题

当我修改mobx的数据时,页面会报错,Uncaught Error: MobX Provider: The set of provided stores has changed,需要手动刷新页面

请问原来是 class 写的页面怎么做迁移?

例如这样:

import React, { Component } from "react";
import { Link } from "react-router-dom";
import { Carousel } from 'antd';

export default class extends Component {
    state = {};

    render() {
        return (
            <section>
              test 
            </section>
        );
    }
}

还是说必须要使用 function 的写法?

error

Page Controller renderToStream Error TypeError: config.injectScript is not a function

新版yk-cli讨论方案

通过压缩包的方式

// 发布之前将example打成一个压缩包发布到yk-cli的npm包中
prePublish: {
tar ../example/ssr-with-js    ssr-with-js.tgz
}

init: {
// 在每次用脚手架新建项目时,解压tgz包为example
tar   ssr-with-js.tgz   ssr-with-js
}

缺陷:不能保证example为远程仓库最新的代码,一旦需要用到最新的,需要强制更新cli

将example单独仓库维护

脚手架下载代码后,缓存到本地,在每次init之前,比对远程仓库版本与本地目录版本号,如果不一致重新下载
优势:能够保证用到最新的代码
缺陷:需要写获取远程版本号以及比对版本号的逻辑,且example单独分仓库,整体性有所欠缺

利用git config + mv命令

初始化.git文件,并且通过git config,使得只clone example文件夹,再将ssr-with-js文件夹利用mv命令提取到当前执行cli命令的目录。至于.git文件夹是否删除皆可
优势:无需将example分仓库维护,实现简单
缺陷:需要考虑用户设备无git命令以及mv命令的情况
业界类似方案:create-react-app

关于 webpack output 配置的疑问

你好,看文档的时候有一个疑惑,为什么output的 filenamechunkFilename 只使用了 [name] , 而没有用 [contenthash] / [chunkhash] 等。

打包出来的名称没有变化,会不会导致因为缓存而让用户无法得到最新的文件?如果是使用协商缓存,那好像强缓存会更好一点,hash值变了就重新获取,协商还要来回通信

所以这部分的考量是什么呢?

yk-cli v1.0.15重构说明

with this commit feat: 重构yk-cli

工具

这里我们使用yargs来作为脚手架的底层服务

代码风格

我们的应用的代码风格无论是js或是ts皆采用standardjs规范

跨平台

为了使我们的脚手架能够在windows平台和*nix平台上都能够稳定使用,这里我们使用了shelljs,该脚本使用原生的fs模块来实现一些跨平台命令例如cp mv rm等命令

callback promise化

为了防止代码中出现大量的if else 以及callback语句造成可读性低下,在这里我们将callback都用promise包一层。这里可以使用Node原生的util.promisify方法或者自己手动封装

去除css预处理器选项

这里我们默认只提供一种css预处理器使用,因为如果要提供多种预处理器会涉及到比较大量复杂的文件处理操作,一个是容易影响稳定性,另一个是容易导致执行效率不高且代码可读性差

todoList

  • ts版本的example,ts版本还在完善中,等成熟后我们会在脚手架中加入ts的选项
  • 状态管理,我们将会提供使用状态管理方案例如dva的example

serverRender 使用 mobx-react 出现 hook 错误

hi,你好,可能是我使用不当出现以下问题,麻烦指导一下,感谢~
操作系统:centos7
使用语言: typescript

package.json

  "dependencies": {
    "egg-scripts": "^2.10.0",
    "midway": "^1.0.0",
    "mobx": "^5.13.0",
    "mobx-react": "^6.1.4",
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "react-router-dom": "^5.1.2",
    "yk-cli": "^2.6.0",
    "ykfe-utils": "^2.1.2"
  },

复现步骤:

  1. 新建一个 typescript 的 ssr 项目
  2. 加入以下内容:
  3. npm start
import { Provider, useStaticRendering } from 'mobx-react';


const serverRender = async (ctx: Context): Promise<JSX.Element> => {
  useStaticRendering(true);
  // 服务端渲染 根据ctx.path获取请求的具体组件,调用getInitialProps并渲染
  const ActiveComponent = getComponent(routes, ctx.path)()
  const Layout = ActiveComponent.Layout || defaultLayout
  const serverData = ActiveComponent.getInitialProps ? await ActiveComponent.getInitialProps(ctx) : {}
  ctx.serverData = serverData
  return  <Provider Index={{test: 111}} >
            <StaticRouter location={ctx.req.url} context={serverData}>
                <Layout layoutData={ctx}>
                    <ActiveComponent {...serverData} />
                </Layout>
            </StaticRouter>
        </Provider>
}

错误日志:

019-11-22 10:39:02,015 ERROR 22235 [-/10.13.82.114/-/130ms GET /] nodejs.Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
[0] [0] 1. You might have mismatching versions of React and the renderer (such as React DOM)
[0] [0] 2. You might be breaking the Rules of Hooks
[0] [0] 3. You might have more than one copy of React in the same app
[0] [0] See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
[0] [0] 1. You might have mismatching versions of React and the renderer (such as React DOM)
[0] [0] 2. You might be breaking the Rules of Hooks
[0] [0] 3. You might have more than one copy of React in the same app
[0] [0] See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
[0] [0]     at resolveDispatcher (webpack-internal:///./node_modules/react/cjs/react.development.js:1590:13)
[0] [0]     at Object.useContext (webpack-internal:///./node_modules/react/cjs/react.development.js:1598:20)
[0] [0]     at X (webpack-internal:///./node_modules/mobx-react/dist/mobx-react.module.js:26:5804)
[0] [0]     at processChild (/data/workSpace/test-mobx-ssr/node_modules/react-dom/cjs/react-dom-server.node.development.js:3204:14)
[0] [0]     at resolve (/data/workSpace/test-mobx-ssr/node_modules/react-dom/cjs/react-dom-server.node.development.js:3124:5)
[0] [0]     at ReactDOMServerRenderer.render (/data/workSpace/test-mobx-ssr/node_modules/react-dom/cjs/react-dom-server.node.development.js:3598:22)
[0] [0]     at ReactDOMServerRenderer.read (/data/workSpace/test-mobx-ssr/node_modules/react-dom/cjs/react-dom-server.node.development.js:3536:29)
[0] [0]     at ReactMarkupReadableStream._read (/data/workSpace/test-mobx-ssr/node_modules/react-dom/cjs/react-dom-server.node.development.js:4298:38)
[0] [0]     at ReactMarkupReadableStream.Readable.read (_stream_readable.js:470:10)
[0] [0]     at resume_ (_stream_readable.js:949:12)
[0] [0] headerSent: true
[0] [0] pid: 22235
[0] [0] hostname: VM_9_43_centos
[0] [0] 

CSR的一些疑问

我本地yarn csr 开启客户端渲染
查看界面元素发现这些资源文件:
20190925095846
但是看代码逻辑是不包含这些资源文件的啊:
20190925095707
不知道是不是我哪里没理解清楚? @zhangyuang

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.