在這裏主要是向你們介紹下個人開源項目-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
^_^。
推薦關注個人微信公衆號【前端張大胖】,天天推送高質量文章、自學經驗和心得,咱們一塊兒交流成長。