介绍下我的开源项目Zz.js(轻量易用的 React SSR 开发骨架)

1,628 阅读4分钟

写在前面

在这里主要是向大家介绍下我的开源项目-zz.js的开发背景、特性、功能、以及如何应用和未来的一些规划。

如果你对next.js nuxt.js有了解的话不妨也看看zz.js

Zz项目地址:github.com/Bigerfe/koa…

Zz介绍

zz 是一个基于 koa2 react16 webpack4 babel7 react-router5 构建而成的 ssr 服务端渲染开发骨架。

可以方便的帮我们快速的构建起一个基础开发服务,帮助我们迅速进入 react ssr的开发。

初衷

起初只是想研究下 react ssr的实现原理,但是学习的过程中发现网上很多文章介绍的良莠不齐,大都是十分简略的实现方式,包括React官方也缺少完整的SSR(Server-Side Rendering)文档,只是简单的介绍了一下需要用到的API,也无法适用于具体项目的开发。

所以非常想把这块的技术吃透,学习的过程中同时也萌生了打造一个开源项目的想法,一个是帮助自己提升技术,另外还可以帮助一些小伙伴儿快速的搞定 react srr 基础骨架的搭建,做到开箱即用。

另外一个目的也算是提供一个基于 koa2 的 react ssr 的完整实现,毕竟之前还没有基于koassr的实现。

可以让对 ssr 服务端渲染感兴趣的同学方便学习和研究,如果还能一起来完善这个项目那就更好了。

技术栈

koa2 Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。

React目前最流行的前端框架之一

React Router5

ReactDOMServerReact官方提供的服务端渲染有关的库

Webpack 基于 webpack4进行工程化处理

Babel7

运行环境

  • 服务器 Node.js >= 10

  • 浏览器版本大于等于IE9, React支持到IE9,但为了更好的在IE下使用,你可能需要引入Polyfill

功能特性

以下是目前已具备功能点

  • 支持本地开发HMR
  • 支持tree shaking以及打包去重依赖
  • 支持csr/ssr自定义layout
  • 同时支持SSR以及CSR两种开发模式,本地开发环境以及线上环境皆可无缝切换两种渲染模式
  • 路由分治管理,不再需要维护单独的路由表,省去维护的烦恼
  • 支持某个具体的页面是 SSR 模式还是 CSR 模式
  • 伪 PWA 支持,开启此特性后页面的二次访问不再请求接口,同时解决页面回退后原页面定位不准的问题
  • 路由双模式支持,可方便的配置当前路由是否按需加载
  • Webpack生产环境构建,也可以修改现有配置

快速上手

从这里开始你将了解到怎样让 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
        } 
    }

页面 SEO

在 数据预取同构 已经看到了 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>
    }
}

伪 PWA 支持

在页面组件内设置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

为了方便有更好的体验,特意准备了一个简单的 DEMO (有点丑, 不要介意^_^ )

demo.zz.bigerfe.com

未来的规划

  • 状态管理不足,如果太复杂的项目可能利于后期维护,所以后面会支持redux
  • 加如单元测试
  • 还没想好....

最后

这个项目我会持续的进行维护和更新,当然一个人的力量是有限的,希望更多的人可以一起来帮助 zz 成长和完善,欢迎提交 pull request ^_^。

到这里请停一下

  • 我正在打造一个纯技术交流群,以学习、交流、思考、提升能力为目标,因为一个人学不如大家一起学,有了更多的交流才会进步的更快。
  • 有想入群的加我微信 223344386 回复加群即可

希望本文可以给你带了一些帮助,文中如有错误,欢迎在评论区指。 如果这篇文章帮助到了你,欢迎点赞和关注。

另外推荐关注我的微信公众号【前端技术江湖】,

系统学习

建议从我的小册入手

React SSR 服务端渲染原理解析与实践