├── .fun └── tmp │ └── deploy │ └── template.yml ├── .funignore ├── .gitignore ├── LICENSE ├── README.md ├── build ├── paths.js ├── util.js ├── webpack.config.base.js ├── webpack.config.client.js └── webpack.config.server.js ├── config └── config.ssr.js ├── createExpressServer.js ├── fc.js ├── package.json ├── template.yml └── web ├── assets └── common.less ├── entry.js ├── layout ├── index.js └── index.less └── page ├── index ├── index.js └── index.less └── news ├── index.js └── index.less /.fun/tmp/deploy/template.yml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | Transform: 'Aliyun::Serverless-2018-04-03' 3 | Resources: 4 | ssr: 5 | Type: 'Aliyun::Serverless::Service' 6 | Properties: 7 | Description: fc ssr demo 8 | InternetAccess: true 9 | page: 10 | Type: 'Aliyun::Serverless::Function' 11 | Properties: 12 | Description: fc ssr demo with nodejs8! 13 | Handler: ./dist/FC.server.handler 14 | Runtime: nodejs8 15 | Timeout: 3 16 | MemorySize: 128 17 | InstanceConcurrency: 1 18 | EnvironmentVariables: 19 | LD_LIBRARY_PATH: >- 20 | /code/.fun/root/usr/lib:/code/.fun/root/usr/lib/x86_64-linux-gnu:/code:/code/lib:/usr/local/lib 21 | PATH: >- 22 | /code/.fun/root/usr/local/bin:/code/.fun/root/usr/local/sbin:/code/.fun/root/usr/bin:/code/.fun/root/usr/sbin:/code/.fun/root/sbin:/code/.fun/root/bin:/code/.fun/python/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/bin 23 | PYTHONUSERBASE: /code/.fun/python 24 | Events: 25 | http-test: 26 | Type: HTTP 27 | Properties: 28 | AuthType: anonymous 29 | Methods: 30 | - GET 31 | -------------------------------------------------------------------------------- /.funignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .DS_Store 64 | 65 | .vscode 66 | 67 | run/ 68 | 69 | dist/ 70 | 71 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ykfe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 声明 2 | 3 | 本项目已不再维护,请使用最新的Serverless场景下的[SSR框架](https://github.com/ykfe/ssr)。 4 | 5 | # SSR 结合 阿里云 Serverless FC(function computed) 示例 6 | 7 | 在本项目中,我们将[egg-react-ssr](https://github.com/ykfe/egg-react-ssr)项目运行在阿里云Serverless函数计算服务[FC](https://help.aliyun.com/document_detail/52895.html?spm=a2c4g.11186623.6.541.45c9368bqWeNxZ)上 8 | 9 | [在线地址](http://ssr-fc.com) 10 | 11 | ## Serverless 12 | 13 | > Serverless 架构是指大量依赖第三方服务(也叫做后端即服务,即“BaaS”)或暂存容器中运行的自定义代码(函数即服务,即“FaaS”)的应用程序,函数是无服务器架构中抽象语言运行时的最小单位。在这种架构中,我们并不看重运行一个函数需要多少 CPU 或 RAM 或任何其他资源,而是更看重运行函数所需的时间,我们也只为这些函数的运行时间付费。 14 | 15 | 即我们只需要关注每一个函数自身的逻辑,而不用操心运行环境等细节,当有事件到来时触发执行,根据执行情况可以卸载。免去管理基础设施、网络资源、实例缩放和系统负载平衡的困扰。您只需要上传您的代码到云服务,它会保障您应用的高可用性和高可拓展性。可以更专注于开发业务逻辑 16 | 17 | ## 开始使用 18 | 19 | 使用之前确保电脑已经安装好[docker](https://github.com/alibaba/funcraft/blob/master/docs/usage/installation-zh.md)并且启动,且全局安装[@alicloud/fun](https://github.com/alibaba/funcraft/blob/master/docs/usage/installation-zh.md), 并且通过`fun config`进行账户的身份信息设置 20 | 21 | ## 配置文件介绍 22 | 23 | ```yaml 24 | ROSTemplateFormatVersion: '2015-09-01' # fc固定格式内容要求 25 | Transform: 'Aliyun::Serverless-2018-04-03' # fc固定格式内容要求 26 | Resources: 27 | ssr-fc.com: # 自定义线上域名 28 | Type: 'Aliyun::Serverless::CustomDomain' 29 | Properties: 30 | Protocol: HTTP 31 | RouteConfig: 32 | routes: # 应用路由 33 | '/*': 34 | ServiceName: ssr 35 | FunctionName: page 36 | ssr: # server name 服务名 37 | Type: 'Aliyun::Serverless::Service' 38 | Properties: 39 | Description: 'fc ssr demo' 40 | page: # function name 方法名 41 | Type: 'Aliyun::Serverless::Function' # 字段类型 42 | Properties: 43 | Handler: index.handler 44 | CodeUri: '.' # 工作目录 45 | Description: 'fc ssr demo with nodejs8!' 46 | Runtime: nodejs8 # 运行环境 47 | Events: 48 | http-test: 49 | Type: HTTP 50 | Properties: 51 | AuthType: ANONYMOUS 52 | Methods: ['GET'] # 支持的方法 53 | ``` 54 | 55 | ## 本地运行 56 | 57 | 本地开发使用以下命令启动服务,支持HMR 58 | 59 | ``` 60 | $ npm i 61 | $ npm start 62 | $ open http://localhost:8000/2016-08-15/proxy/ssr/page/ 63 | ``` 64 | 65 | 注:FC启动的应用链接规范为 `http://localhost:8000/2016-08-15/proxy/${service name}/${function name}/` 66 | 67 | ## 执行流程 68 | 69 | 这里简单介绍一下FC-SSR应用的执行流程 70 | 71 | ![](https://img.alicdn.com/tfs/TB1V_57iAT2gK0jSZFkXXcIQFXa-1948-702.jpg) 72 | 73 | ### 使用express代替原生的http trigger提供的对象 74 | 75 | 首先, 我们用了express作为我们的Node框架,之所以不用FC原生的http trigger(触发器)提供的对象,是因为我们生产环境需要用到静态资源托管这个功能,且FC原生的请求上下文对象是http模块提供的(req, res)经过处理后的对象,在属性上有一些缺失。 76 | 77 | 关于FC如何接入express请查看该[教程](https://github.com/muxiangqiu/fc-express-nodejs8) 78 | 79 | ### 本地开发代理前端静态资源支持HMR 80 | 81 | 如果你对[egg-react-ssr](https://github.com/ykfe/egg-react-ssr)这个项目熟悉的话,在本地开发我们用ykcli去编译我们的前端静态资源文件放在内存中,并且通过webpack-dev-server启动一个服务托管它们。在我们本地开发时我们需要使用这些内存中的文件而不是本地硬盘中的文件。 82 | 83 | #### 设置publicPath 84 | 85 | 我们需要将`webpack.config.client.js`的publicPath设置为`/2016-08-15/proxy/ssr/page/`, 为什么要这么设置在`hot-update.json`中会讲到 86 | 你会发现我们的前端静态资源文件实际上是被编译到 `http://localhost:8888/2016-08-15/proxy/ssr/page/static/css/Page.chunk.css` 这样的路径下面(与FC应用路径唯一的区别只是端口不一致) 87 | 这里我们使用`express-http-proxy`这个库来转发我们的请求 88 | 89 | #### 代理hot-update.json文件 90 | 91 | 这里有一个需要注意的地方就是,HMR功能需要去加载一个 `hot-update.json` 这样的文件来实现,但是webpack会用当前页面的端口以及publicPath作为路径去下载该文件。如果我们还是和之前一样设置`publicPath='/'`的话,那么实际请求的路径是`localhost:8000/hot-update.json` 这样的路径并没有被我们的FC应用的http触发器所监听到,所以不会触发Proxy,必须得是`localhost:8000/2016-08-15/proxy/ssr/page/xxx`这样的路径才会触发,所以我们需要设置 `publicPath='/2016-08-15/proxy/ssr/page/'` 92 | 93 | ### docker 应用访问宿主机服务 94 | 95 | 由于我们的前端静态资源是在宿主机而不是在docker中启动的,所以我们没办法在FC应用中通过localhost来访问宿主机资源,这里有两种方式 96 | 97 | - 通过ifconfig查看本机ip,替换localhost 98 | - 使用host.docker.internal来访问宿主机服务 99 | 100 | ## 两种线上发布方式 101 | 102 | 由于FC有应用发布大小的限制,压缩后代码小于50M, 解压后小于200M。所以与egg-react-ssr项目我们的发布方式不同,在生产环境我们修改了`webpack.config.server.js`去除了`externanls`选项来将一些第三方库与我们的bundle.server.js打在了一起,经过测试在生产环境压缩后的bundle.server.js在200kb左右时性能无明显影响,但当第三方库过多时如果超过几MB时对ttfb性能有影响。优点是我们不需要将node_modules上传到云端了发布速度更快, 需要发布的包更小。 103 | `注:由于需要使用webpack去处理服务端Node相关代码,由于webpack require expression的特性不支持动态故请不要随意修改本应用目录结构,否则可能无法运行` 104 | 发布前确保已经通过fun config进行个人帐户的设置,且需要绑定[自定义域名](https://help.aliyun.com/document_detail/90722.html?spm=a2c4g.11174283.6.682.1a245212Zcy5ax)解析到FC的触发器,否则无法访问 105 | 106 | 那么让我们来对比一下两种发布方式的优缺点 107 | 108 | ### 打包开启externals选项 109 | 110 | 开启此选项的目的是将第三方模块依赖外置,此时需要保证我们的运行环境存在node_modules, 为了尽量控制包大小,我们只安装生产环境所需依赖 111 | 112 | - 优点: 启动速度更快,服务端bundle更小 113 | - 缺点: 需要上传的代码量大,上传速度慢 114 | 115 | ``` 116 | $ npm run build 117 | $ rm -rf node_modules && npm i --production 118 | $ fun deploy 119 | ``` 120 | 121 | bundle.server.js 大小 开发环境在 260kB 左右, 生产环境在 6KB 左右 122 | 123 | 具体发布信息 124 | 125 | ![](https://img.alicdn.com/tfs/TB1cxh4iLb2gK0jSZK9XXaEgFXa-1188-790.jpg) 126 | 127 | 可以看到我们上传了压缩后在20MB大小的代码,上传时间大概在20S左右 128 | 129 | ![](https://img.alicdn.com/tfs/TB15YR6iNn1gK0jSZKPXXXvUXXa-2878-1222.jpg) 130 | 131 | TTFB 时间在 50ms 左右 白屏时间在 168ms 左右 132 | 133 | ![](https://img.alicdn.com/tfs/TB1yOt4iKT2gK0jSZFvXXXnFXXa-2492-990.jpg) 134 | 135 | 函数执行时间 8ms, 运行内存在25MB 136 | 137 | ### 不开启externals选项 138 | 139 | 通过这种方式我们将node_modules中的依赖与我们的业务代码打包在了一起 140 | 141 | - 优点,上传速度快,需要上传的总代码量变小 142 | - 缺点,服务端bundle会增大导致运行内存增加,特别是本地开发的时候明显增大, webpack打包构建速度变慢 143 | 144 | `为了更明显的比较两种方式,我们引入多个第三方库测试性能` 145 | 146 | ``` 147 | $ npm i antd antd-mobile axios 148 | $ npm run build && fun deploy 149 | ``` 150 | 151 | bundle.server.js 大小 开发环境在 35MB 左右, 生产环境在 4.4MB 左右 152 | 153 | 具体发布信息 154 | 155 | ![](https://img.alicdn.com/tfs/TB1vH1viRr0gK0jSZFnXXbRRXXa-1082-802.jpg) 156 | 157 | 可以看到我们上传了压缩后在4.4MB大小的代码,上传时间大概在2S左右 158 | 159 | ![](https://gw.alicdn.com/tfs/TB1zi1uiQT2gK0jSZPcXXcKkpXa-2878-1170.jpg) 160 | 161 | TTFB 时间在 50ms 左右 白屏时间在 240ms 左右, 无明显变化 162 | 163 | ![](https://gw.alicdn.com/tfs/TB1JWSsiNn1gK0jSZKPXXXvUXXa-2462-878.jpg) 164 | 165 | 函数执行时间 6ms,首次运行内存129MB, 后续运行内存在52MB,运行内存显著增大, 因为我们一次性加载了所有的服务端bundle会包含一些暂时用不到的模块 166 | 167 | ## 总结 168 | 169 | 区分与传统的Node.js应用,在serverless场景下,我们一般不上传node_modules来将整个服务端文件打包成一个bundle后发布,这种方式存在以下优缺点 170 | 171 | ### 优势 172 | 173 | 发布快 174 | 175 | ### 劣势 176 | 177 | 将所有服务端代码打包到一个bundle存在以下问题 178 | - 编译后代码无法debug是黑盒 179 | - webpack打包时会静态分析代码,导致实际执行时不会运行报错的代码在编译阶段报错 180 | - webpack打包treeshaking基于es6 modules,如果第三方模块没有导出es6 modules格式的模块或者哪块不小心使用了commonjs会导致最后的代码bundle非常大需要手动将执行时不需要的依赖externals(排除)掉 181 | 182 | 在Serverless场景下我们对第三方模块的编写规范和引入规范要求更加的严格,同时需要尽量控制用到的第三方模块数量,因为一不小心就会造成很多无用的依赖被打包进来造成服务端bundle巨大执行内存显著增加。例如antd这种库,一旦tree shaking不成功,就会造成代码体积十分庞大的后果。 183 | 184 | ## Serverless与传统方案对比 185 | 186 | 相较于我们传统的应用发布方案,Serverless存在以下诸多优势 187 | 188 | - 降低启动成本 189 | - 实现快速上线 190 | - 系统安全性更高 191 | - 自动扩缩容能力 192 | 193 | ### 降低启动成本 194 | 195 | 当我们作为一家公司开发一个 Web 应用时,在开发的时候,我们需要版本管理服务器、持续集成服务器、测试服务器、应用版本管理仓库等作为基础的服务。线上运行的时候,为了应对大量的请求,我们需要一个好的数据库服务器。当我们的应用面向了普通的用户时,我们需要: 196 | 邮件服务,用于发送提醒、注册等服务 197 | 短信服务(依国家实名规定),用于注册、登录等用户授权操作 198 | 对于大公司而言,这些都是现成的基础设施。可对于新创企业来说,这都是一些启动成本。 199 | 200 | ### 快速上线 201 | 202 | 对于一个 Web 项目来说,启动一个项目需要一系列的 hello, world。当我们在本地搭建环境的时候,是一个 hello, world,当我们将程序部署到开发环境时,也是一个部署相关的 hello, world。虽然看上去有些不同,但是总的来说,都是 it works!。 203 | Serverless 在部署上的优势,使得你可以轻松地实现上线。 204 | 205 | ### 系统安全性更高 206 | 207 | 在 Serverless 架构下每个函数都是独立虚拟机级别环境运行互不干扰,并且我们不需要操心服务器被攻击的事情了,这些问题云服务商都会帮我们解决 208 | 209 | ### 自动扩缩容 210 | 211 | Serverless第二个常被提及的特点是自动扩缩容。前面说了函数即应用,一个函数只做一件事,可以独立的进行扩缩容,而不用担心影响其他函数,并且由于粒度更小,扩缩容速度也更快。而对于单体应用和微服务,借助于各种容器编排技术,虽然也能实现自动扩缩容,但由于粒度关系,相比函数,始终会存在一定的资源浪费。比如一个微服务提供两个API,其中一个API需要进行扩容,而另一个并不需要,那么这时候扩容,对于不需要的API就是一种浪费。 212 | 213 | ## 未来可优化空间 214 | 215 | 目前看起来本项目所包含的文件还是略有多余。在之后我们将会将egg, koa, express 等框架内置进 faasruntime,启动的时候指定框架类型即可。且相关的webpack配置之后将会以脚手架的形式进行封装而不暴露在应用内部 216 | 217 | ## 答疑群 218 | 219 | 虽然我们已经尽力检查了一遍应用,但仍有可能有疏漏的地方,如果你在使用过程中发现任何问题或者建议,欢迎提[issue](https://github.com/ykfe/ssr-with-fc/issues)或者[PR](https://github.com/ykfe/ssr-with-fc/pulls) 220 | 欢迎直接扫码加入钉钉群 221 | -------------------------------------------------------------------------------- /build/paths.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | const path = require('path') 5 | const fs = require('fs') 6 | const url = require('url') 7 | 8 | // Make sure any symlinks in the project folder are resolved: 9 | // https://github.com/facebook/create-react-app/issues/637 10 | const appDirectory = fs.realpathSync(process.cwd()) 11 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath) 12 | 13 | const envPublicUrl = process.env.PUBLIC_URL 14 | 15 | function ensureSlash (inputPath, needsSlash) { 16 | const hasSlash = inputPath.endsWith('/') 17 | if (hasSlash && !needsSlash) { 18 | return inputPath.substr(0, inputPath.length - 1) 19 | } else if (!hasSlash && needsSlash) { 20 | return `${inputPath}/` 21 | } else { 22 | return inputPath 23 | } 24 | } 25 | 26 | const getPublicUrl = appPackageJson => 27 | envPublicUrl || require(appPackageJson).homepage 28 | 29 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 30 | // "public path" at which the app is served. 31 | // Webpack needs to know it to put the right `, 29 | ``, 30 | `` 31 | ], // 客户端需要加载的静态资源文件表 32 | serverJs: 'entry' // web/entry 33 | } 34 | -------------------------------------------------------------------------------- /createExpressServer.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import renderToStreamForFaas from 'ykfe-utils/es/renderToStreamForFass' 3 | 4 | const ssrConfig = require('./config/config.ssr') 5 | const isDev = process.env.local 6 | 7 | const createServer = () => { 8 | const server = express() 9 | if (isDev) { 10 | const proxy = require('express-http-proxy') 11 | // 为了docker可以使用宿主机的服务,这里需要使用本机IP地址 12 | server.use('*', proxy(`http://host.docker.internal:8888`, { 13 | filter: function (req, res) { 14 | return /(\/static)|(\/sockjs-node)|hot-update/.test(req.baseUrl) 15 | }, 16 | proxyReqPathResolver: function (req) { 17 | return '/2016-08-15/proxy/ssr/page' + req.baseUrl 18 | } 19 | })) 20 | } 21 | ssrConfig.routes.map(item => { 22 | server.get(item.path, async (req, res) => { 23 | const ctx = { 24 | req, 25 | res, 26 | path: req.path, 27 | app: { 28 | config: ssrConfig 29 | } 30 | } 31 | try { 32 | const stream = await renderToStreamForFaas(ctx, ssrConfig) 33 | res.status(200) 34 | .set('Content-Type', 'text/html') 35 | res.write('') 36 | stream.pipe(res, { end: 'false' }) 37 | stream.on('end', () => { 38 | res.end() 39 | }) 40 | } catch (error) { 41 | console.log(`renderStream Error ${error}`) 42 | } 43 | }) 44 | }) 45 | server.use(express.static('dist')) 46 | 47 | return server 48 | } 49 | 50 | export const server = createServer() 51 | -------------------------------------------------------------------------------- /fc.js: -------------------------------------------------------------------------------- 1 | import { Server } from '@webserverless/fc-express' 2 | import getRawBody from 'raw-body' 3 | import { server } from './createExpressServer' 4 | const proxyServer = new Server(server) 5 | 6 | // http trigger 7 | export const handler = async (req, res, context) => { 8 | req.body = await getRawBody(req) 9 | proxyServer.httpProxy(req, res, context) 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "2.0.0", 4 | "dependencies": { 5 | "@webserverless/fc-express": "0.1.1", 6 | "express": "4.17.1", 7 | "react": "16.8.3", 8 | "react-dom": "16.8.3", 9 | "react-router-dom": "^5.0.0", 10 | "stream-to-string": "^1.2.0", 11 | "ykfe-utils": "^2.0.0" 12 | }, 13 | "scripts": { 14 | "start": "concurrently \"npm run ssr\" \"npm run csr\"", 15 | "ssr": "concurrently \"fun local start\" \"cross-env NODE_ENV=development webpack --watch --config ./build/webpack.config.server.js\"", 16 | "csr": "cross-env NODE_ENV=development RUNTIME=serverless ykcli dev --PORT 8888", 17 | "debug": "concurrently \"fun local start -d 3000 --config vscode\" \"cross-env NODE_ENV=development webpack --watch --config ./build/webpack.config.server.js\"", 18 | "deploy": "npm run build && fun deploy", 19 | "build:server": "cross-env NODE_ENV=production webpack --display-used-exports --optimize-minimize --config build/webpack.config.server.js", 20 | "build:client": "cross-env NODE_ENV=production RUNTIME=serverless ykcli build", 21 | "build": "rimraf dist && npm run build:server && npm run build:client", 22 | "analyze": "cross-env NODE_ENV=production npm_config_report=true ykcli build" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 9", 28 | "not op_mini all" 29 | ], 30 | "devDependencies": { 31 | "@babel/core": "^7.4.4", 32 | "@babel/polyfill": "^7.4.4", 33 | "@babel/preset-env": "^7.5.5", 34 | "@babel/preset-react": "^7.0.0", 35 | "babel-loader": "^8.0.4", 36 | "browserslist": "^4.6.3", 37 | "concurrently": "^4.1.0", 38 | "cross-env": "^5.2.0", 39 | "css-hot-loader": "^1.4.3", 40 | "css-loader": "1.0.0", 41 | "css-modules-require-hook": "^4.2.3", 42 | "express-http-proxy": "^1.6.0", 43 | "file-loader": "2.0.0", 44 | "ignore-loader": "^0.1.2", 45 | "less": "^3.9.0", 46 | "less-loader": "^4.1.0", 47 | "mini-css-extract-plugin": "^0.5.0", 48 | "optimize-css-assets-webpack-plugin": "5.0.1", 49 | "postcss-flexbugs-fixes": "4.1.0", 50 | "postcss-loader": "3.0.0", 51 | "postcss-preset-env": "^6.0.5", 52 | "postcss-safe-parser": "4.0.1", 53 | "react-dev-utils": "^8.0.0", 54 | "rimraf": "^2.6.3", 55 | "terser-webpack-plugin": "^1.2.0", 56 | "url-loader": "1.1.1", 57 | "webpack": "^4.19.1", 58 | "webpack-bundle-analyzer": "^3.0.3", 59 | "webpack-cli": "^3.3.3", 60 | "webpack-dev-server": "3.3.1", 61 | "webpack-manifest-plugin": "^2.0.4", 62 | "webpack-merge": "^4.1.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | Transform: 'Aliyun::Serverless-2018-04-03' 3 | Resources: 4 | ssr-fc.com: # domain name 5 | Type: 'Aliyun::Serverless::CustomDomain' 6 | Properties: 7 | Protocol: HTTP 8 | RouteConfig: 9 | routes: 10 | '/*': 11 | ServiceName: ssr 12 | FunctionName: page 13 | ssr: 14 | Type: 'Aliyun::Serverless::Service' 15 | Properties: 16 | Description: 'fc ssr demo' 17 | page: 18 | Type: 'Aliyun::Serverless::Function' 19 | Properties: 20 | Handler: ./dist/FC.server.handler 21 | CodeUri: '.' 22 | Description: 'fc ssr demo with nodejs8!' 23 | Runtime: nodejs8 24 | Events: 25 | http-test: 26 | Type: HTTP 27 | Properties: 28 | AuthType: ANONYMOUS 29 | Methods: ['GET'] -------------------------------------------------------------------------------- /web/assets/common.less: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | a { 7 | color: #fff; 8 | text-decoration: none; 9 | } 10 | 11 | ul { 12 | list-style: none; 13 | } -------------------------------------------------------------------------------- /web/entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { BrowserRouter, StaticRouter, Route } from 'react-router-dom' 4 | import defaultLayout from '@/layout' 5 | import { getWrappedComponent, getComponent } from 'ykfe-utils' 6 | import config from '../config/config.ssr' 7 | const Routes = config.routes 8 | 9 | const clientRender = async () => { 10 | // 客户端渲染||hydrate 11 | ReactDOM[window.__USE_SSR__ ? 'hydrate' : 'render']( 12 | 13 | { 14 | // 使用高阶组件getWrappedComponent使得csr首次进入页面以及csr/ssr切换路由时调用getInitialProps 15 | Routes.map(({ path, exact, Component }) => { 16 | const ActiveComponent = Component() 17 | const Layout = ActiveComponent.Layout || defaultLayout 18 | return { 19 | const WrappedComponent = getWrappedComponent(ActiveComponent) 20 | return 21 | }} /> 22 | }) 23 | } 24 | 25 | , document.getElementById('app')) 26 | 27 | if (process.env.NODE_ENV === 'development' && module.hot) { 28 | module.hot.accept() 29 | } 30 | } 31 | 32 | const serverRender = async (ctx) => { 33 | // 服务端渲染 根据ctx.path获取请求的具体组件,调用getInitialProps并渲染 34 | const ActiveComponent = getComponent(Routes, ctx.path)() 35 | const Layout = ActiveComponent.Layout || defaultLayout 36 | const serverData = ActiveComponent.getInitialProps ? await ActiveComponent.getInitialProps(ctx) : {} 37 | ctx.serverData = serverData 38 | return 39 | 40 | 41 | 42 | 43 | } 44 | 45 | export default __isBrowser__ ? clientRender() : serverRender 46 | -------------------------------------------------------------------------------- /web/layout/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import serialize from 'serialize-javascript' 4 | import { Link } from 'react-router-dom' 5 | import config from '../../config/config.ssr' 6 | import '@/assets/common.less' 7 | import './index.less' 8 | 9 | const commonNode = props => ( 10 | // 为了同时兼容ssr/csr请保留此判断,如果你的layout没有内容请使用 props.children ?
{ props.children }
: '' 11 | props.children 12 | ?

Egg + React + SSR
by ykfe

{props.children}
13 | : '' 14 | ) 15 | 16 | const Layout = (props) => { 17 | if (__isBrowser__) { 18 | return commonNode(props) 19 | } else { 20 | const { serverData } = props.layoutData 21 | const { injectCss, injectScript } = props.layoutData.app.config 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Serverless 服务端渲染 31 | { 32 | injectCss && injectCss.map(item => ) 33 | } 34 | 35 | 36 |
{ commonNode(props) }
37 | { 38 | serverData &&