在这里主要是向你们介绍下个人开源项目-zz.js
的开发背景、特性、功能、以及如何应用和将来的一些规划。javascript
若是你对next.js
nuxt.js
有了解的话不妨也看看zz.js
。css
Zz
项目地址:github.com/Bigerfe/koa…html
zz
是一个基于 koa2
react16
webpack4
babel7
react-router5
构建而成的 ssr
服务端渲染开发骨架。前端
能够方便的帮咱们快速的构建起一个基础开发服务,帮助咱们迅速进入 react ssr
的开发。java
起初只是想研究下 react ssr
的实现原理,可是学习的过程当中发现网上不少文章介绍的参差不齐,大都是十分简略的实现方式,包括React
官方也缺乏完整的SSR(Server-Side Rendering)文档,只是简单的介绍了一下须要用到的API
,也没法适用于具体项目的开发。node
因此很是想把这块的技术吃透,学习的过程当中同时也萌生了打造一个开源项目的想法,一个是帮助本身提高技术,另外还能够帮助一些小伙伴儿快速的搞定 react srr
基础骨架的搭建,作到开箱即用。react
另一个目的也算是提供一个基于 koa2 的 react ssr
的完整实现,毕竟以前尚未基于koa
的ssr
的实现。webpack
可让对 ssr
服务端渲染感兴趣的同窗方便学习和研究,若是还能一块儿来完善这个项目那就更好了。git
koa2 Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。github
React目前最流行的前端框架之一
ReactDOMServerReact官方提供的服务端渲染有关的库
Webpack 基于 webpack4进行工程化处理
服务器 Node.js >= 10
浏览器版本大于等于IE9, React支持到IE9,但为了更好的在IE下使用,你可能须要引入Polyfill
如下是目前已具有功能点
SSR 模式
仍是 CSR 模式
从这里开始你将了解到怎样让 zz
在本地快速的跑起来,而后进行实际项目开发。
首先你须要安装 node ,而且确保 node 版本是10或以上。mac 下推荐使用 nvm 来管理 node 版本)
$ node -v
10.0x
复制代码
为了方便咱们建立应用和页面,这里提供了一个配套的 zz-cli
脚手架。
先全局安装脚手架。
$ npm i zz-cli -g
复制代码
//初始化项目
$ zzjs -i
$ <Your Project Name>
$ cd <Your Project Name>
$ npm i
$ npm run dev //本地开发的watch 模式
$ open http://<Your local ip>:8808
复制代码
可经过不一样的命令开启不一样的渲染模式。
$ npm run dev //开启本地开发 可修改配置内的属性 isSSR ,支持两种渲染模式
$ npm run dev:csr //开启本地开发 并已 wds 为服务启动 - csr 模式
$ //更多.....
复制代码
├── dist // 生产环境打包后的资源目录
│ ├── static //打包的静态资源文件
│ ├── server //用于同构的运行于 node 端的文件
├── docs // 帮助文档
├── server // 开发时 node 端代码
├── src // 开发时 react 组件相关代码
│ ├── app //应用入口
│ │ ├── layout //layout 组件
│ │ ├── index.js //webpack entry 打包入口
│ │ ├── provider.js //提供数据的基础组件
│ ├── common // 公共资源
│ │ ├── components // 公共组件
│ │ ├── fetch // fetch模块
│ │ ├── module // 公共模块
│ ├── config // 基础配置文件
│ ├── zz-base // zz基础组件
│ ├── pages // 业务页面
│ │ ├── index //默认首页
│ │ │ ├── config 路由配置
│ ├── routes // 路由配置 无需维护
├── test // 单页测试
├── webpack //构建配置
├── app.js //生产环境 app 启动入口 ---> 好比 pm2 start app.js
复制代码
页面入口
关于/src/pages/
下每一个页面的入口的约定,目前只支持一级路由的设置,全部的页面的入口都是 index.js
, zz
内部会自动进行识别。
路由约定
每一个页面的路由配置的方式再也不是集中式配置,而是分治配置,每一个页面对应一个路由配置,请按照下面的格式进行配置
举个栗子
// /src/pages/index 页面目录
// /src/pages/index/config/route.js 路由配置
//路由代码,能够方便的设置路由是否按需加载
import React from 'react';
import BaseBundle from '../../../routes/route-base-bundle';
//import LazyPageCom from '../index'; //静态组件模式
//动态组件配置
const LazyPageCom = (props) => (
<BaseBundle load={() => import(/*webpackChunkName:"chunk-index"*/'../index')}> {(CompIndex) => <CompIndex {...props} />} </BaseBundle> ); //一个页面组件可配置多个路由入口 export default [ { path:'/', component: LazyPageCom, exact:true },{ path: '/index', component: LazyPageCom } ] 复制代码
你只须要修改 webpackChunkName 的名称和 export 导出的数据便可,固然也能够对当前页面配置多个路由.
可经过脚手架快速的建立页面
$ cd <Your Project Name>
$ zzjs -p
$ <Your pageName>
$ open http://<Your local ip>:8808/<Your pageName>
复制代码
为了方便维护和扩展,zz 把路由进行了分治管理,每一个页面的路由都是独立的,只须要单独的配置便可。
请参考路由约定
数据预取的目的是在 node
端渲染组件前提早从接口或者某个数据源获取到数据,也可让某个页面在 CSR
下能够拿到数据,进行组件的 update。
为了方便的实现同构咱们在页面组件内约定了一个数据预取的静态方法 getInitialProps
,当前页面首屏数据都是从这个方法内进行返回。
//基础参数的带入
//opt={query:{},params:{}}
static async getInitialProps(zzOpt){//数据预取
if(__SERVER__){
//若是是服务端渲染的话 能够作的处理
}
//接口 a
const fetch1= fetch.postForm('/fe_api/a', {
data: { a: 4000 }
});
//接口 b
const fecth2= fetch.postForm('/fe_api/b', {
data: { c: 2000 }
});
const resArr =await fetch.multipleFetch(fetch1, fecth2);
//返回数据固定格式 page 表明页面信息,支持 seo 的设置
//fetchData是接口返回的数据
return {
page:{
tdk: {
title: 'ksr 框架',
keyword: 'ssr react',
description: '我是描述'
}
},
fetchData: resArr
}
}
复制代码
在 数据预取同构 已经看到了 getInitialProps
方法返回的数据是一个固定的格式,结果内包含一个 page
字段.
page
字段表示的就是当前页面的 SEO
的信息.
//此处代码已略
return {
page:{
tdk: {
title: 'ksr 框架',
keyword: 'ssr react',
description: '我是描述'
}
},
fetchData: resArr
}
复制代码
一个page 的渲染
页面组件须要继承一个 zz
的基础组件 ZzPageBase
,为咱们封装了一些基础数据获取和存储功能.
须要设置 static contextType = RootContext 为的是让组件能够得到全局的数据.
实现 static async getInitialProps 数据预取方法.
componentDidMount
内是否须要作数据的更新,若是须要更新能够调用getInitialProps方法.
参考完整代码
import React,{useContext} from 'react';
import { Link } from 'react-router-dom';
import RootContext from '../../app/route-context';//自定义 context
import ZzPageBase from '../../zz-base/common/components/zz-page-base';//基础组件 页面组件都须要继承
import fetch from '../../common/fetch';//内置的 fech 模块
export default class Index extends ZzPageBase{
constructor(props,context){
super(props,context);
}
enableSpaDataCache=true;//开启 伪 pwa 数据缓存
//获得 context 对象
static contextType = RootContext;
//基础参数的带入
//opt={query:{},params:{}}
static async getInitialProps(zzOpt){//数据预取
if(__SERVER__){
//若是是服务端渲染的话 能够作的处理
}
const fetch1= fetch.postForm('/fe_api/a', {
data: { a: 4000 }
});
const fecth2= fetch.postForm('/fe_api/b', {
data: { c: 2000 }
});
const resArr =await fetch.multipleFetch(fetch1, fecth2);
//返回数据固定格式 page 表明页面信息,支持 seo 的设置
//fetchData是接口返回的数据
return {
page:{
tdk: {
title: 'ksr 框架',
keyword: 'ssr react',
description: '我是描述'
}
},
fetchData: resArr
}
}
componentDidMount(){
//数据更新 参考
//this.isSSR 标识当前页面是不是 ssr 输出
//this.hasSpaCacheData标识是否有伪 pwa 的缓存数据
if (!this.isSSR && !this.hasSpaCacheData){// 页面若是是客户端的须要从新获取数据
Index.getInitialProps(this.props.zzOpt).then(data=>{
this.setState({
...data
},()=>{
document.title=this.state.page.tdk.title;
});
});
}
}
render(){
const {page,fetchData}=this.state;//得到数据
//参考代码,须要对数据作边界容错处理
return <div className="detailBox"> <div> { page && <div><span>title:{page.tdk.title}</span> <span>ky:{page.tdk.keyword}</span> </div> } </div> { res && res.data.map(item=>{ return <div key={item.id}>{item.keyId}:{item.keyName}---{item.setContent}</div> }) } </div>
}
}
复制代码
在页面组件内设置enableSpaDataCache
值,便可开启这个特性。此特性开启后,可让这个页面的二次访问再也不有数据请求,当前是否须要还要根据本身的实际业务触发。
export default class Index extends zzPageBase{
constructor(props,context){
super(props,context);
}
enableSpaDataCache=true;//开启 伪 pwa 数据缓存
}
复制代码
__SERVER__
常量、表示当前是不是服务端渲染,常常会在组件内被使用
this.isSSR
常量、表示当前页面的渲染是服务端渲染仍是客户端渲染
this.hasSpaCacheData
常量 、 表示当前页面内是否有伪 pwa
的数据
zz
默认有本身的一套配置,好比本地开发端口、静态资源的 cdn 地址等
固然这些配置你也能够进行修改
全局配置文件 /src/config/project-config.js
//fetch 接口 开发环境和生产环境
const DevApiHost ='http://dev.xxx.com'; //开发环境接口地址
const ProductionApiHost ='http://pro.xxx.com';//生产环境接口地址
export default {
//判断是不是开发环境,不然能够理解为生产环境,最好统一使用此方法。保证正确
getIsDev(){
return process.env.NODE_ENV ==='production'
},
openProductionStaticFolder: true,//线上环境是否开启静态目录访问能力
isSSR: true,//是否开启 ssr
nodeServerPort:8808,//服务器和本地 node 服务器启动端口,可自行设置
//业务开发中 fecth api 的地址 ,能够根据环境进行区分
reqApiUrlHost:process.env.IS_DEV?DevApiHost:ProductionApiHost,
devWdsPort:8809,//wds 服务启动的端口,用于开发环境的静态资源的访问和热更新操做
routeIndexFolderName: 'index', //标识业务页面的首页目录名称,路由集中处理后会将此入口排在入口 list 的第一个位置
//TODO:打包到生产环境的时候这个地址会随机的进行分配 可能致使分配不均
staticAssetsCdnHost: [
'//c1.static.aa.com/',
'//c2.static.aa.com/',
'//c3.static.aa.com/'
],
Production_JS_Host:'//c1.static.aa.com',//生产环境 js 资源 host
Production_CSS_Host:'//x2.static.aa.com',//生产环境 css 资源 host
}
复制代码
项目部署就按照常规的方法进行部署 ,使用 pm2
来作进程守护,固然这里只是一个简单的栗子,仅供参考。
执行生产环境构建
$ npm run build
复制代码
使用 pm2 启动 app.js
$ pm2 start app.js -n zz-ssr
复制代码
为了方便有更好的体验,特地准备了一个简单的 DEMO (有点丑, 不要介意^_^ )
redux
这个项目我会持续的进行维护和更新,固然一我的的力量是有限的,但愿更多的人能够一块儿来帮助 zz
成长和完善,欢迎提交 pull request
^_^。
推荐关注个人微信公众号【前端张大胖】,天天推送高质量文章、自学经验和心得,咱们一块儿交流成长。