@symph/joy使用指南,支持服务端渲染、零配置、MVC、React+Redux的前端框架

使用指南

安装和开始

运行npm init创建一个空工程,并填写项目的基本信息,当然也可以在一个已有的项目中直接安装。

npm install --save @symph/joy react react-dom

@symph/joy 只支持 React 16及以上版本

添加NPM脚本到package.json文件:

{
  "scripts": {
    "dev": "joy",
    "build": "joy build",
    "start": "joy start"
  }
}

创建./src/index.js文件,并插入以下代码:

import React, {Component} from 'react'

export default class Index extends Component{
  render(){
    return <div>Welcome to symphony joy!</div>
  }
}

然后运行npm run dev 命令,在浏览器中输入访问地址http://localhost:3000。如果需要使用其它端口来启动应用 npm run dev -- -p <your port here>

到目前为止,一个简单完整的react app已经创建完成,例子hello-world,到这儿我们拥有了什么功能呢?

  • 一个应用入口(./src/index.js),我们可以在里面完善我们的app内容和添加路由(参考react-router-4的使用方法)
  • 启动了一个开发服务器,可以渲染我们编写的界面了
  • 一个零配置的webpack编译器,监控我们的源码,确保在浏览器和node端正常运行
  • ES6等高级语法支持,不用担心node端不兼容的语法
  • 热加载,如果我们修改了./src/index.js的内容并保存,界面会自动刷新
  • 静态资源服务,在/static/目录下的静态资源,可通过http://localhost:3000/static/访问

样式 CSS

jsx内建样式

和next.js一样,内建了 styled-jsx 模块,支持Component内独立域的CSS样式,不会和组件外同名样式冲突。

export default () =>
  <div>
    Hello world
    <p>scoped!</p>
    <style jsx>{`
      p {
        color: blue;
      }
      div {
        background: red;
      }
      @media (max-width: 600px) {
        div {
          background: blue;
        }
      }
    `}</style>
    <style global jsx>{`
      body {
        background: black;
      }
    `}</style>
  </div>

查看 styled-jsx 文档 ,获取详细信息。

Import CSS / LESS 文件

为了支持导入css和less样式文件,可使用样式插件,具体使用方法请见插件详情页面。

导入图片

@symph/joy-image插件提供了图片导入功能,详细的配置请参见插件主页

  // joy.config.js
const withLess = require('@symph/joy-less')
const withImageLoader = require('@symph/joy-image')

module.exports = {
  serverRender: true,
  plugins: [
    withImageLoader({limit: 8192})
  ]
}
export default () =>
  <img src={require('./image.png')}/>

静态文件

在工程根目录下创建static目录,将需要待访问的文件放入其中,也可以在里面创建子目录管理这些文件,可以通过{assetPrefix}/static/{file}路径访问这些文件。

export default () => <img src="/static/my-image.png" />

自定义 Head

@symph/joy 提供了Head Component来设置html页面的<head>中的内容

import Head from '@symph/joy/head'

export default () =>
  <div>
    <Head>
      <title>My page title</title>
      <meta name="viewport" content="initial-scale=1.0, width=device-width" />
    </Head>
    <p>Hello world!</p>
  </div>

为了避免在head中重复添加多个相同标签,可以给标签添加key属性, 相同的key只会渲染一次。

import Head from '@symph/joy/head'
export default () => (
  <div>
    <Head>
      <title>My page title</title>
      <meta name="viewport" content="initial-scale=1.0, width=device-width" key="viewport" />
    </Head>
    <Head>
      <meta name="viewport" content="initial-scale=1.2, width=device-width" key="viewport" />
    </Head>
    <p>Hello world!</p>
  </div>
)

在上面的例子中,只有第二个<meta name="viewport" />被渲染和添加到页面。

获取数据 fetch

@symph/joy/fetch发送数据请求, 其调用参数和浏览器提供的fetch方法保持一样。

import fetch from '@symph/joy/fetch'

fetch('https://news-at.zhihu.com/api/3/news/hot', {method: 'GET'})
  .then(respone = >{
      // do something...
  });

@symph/joy/fetch 提供简单的跨域解决方案,跨域请求会先转发到node服务端,node服务器作为代理服务器,完成真实的数据请求,并且响应数据回传给浏览器。

TODO 插入流程图

如果想关闭改内建行为,使用jsonp来完成跨域请求,可以在fetch的options参数上设定options.mode='cors'

import fetch from '@symph/joy/fetch'

fetch('https://news-at.zhihu.com/api/3/news/hot', {method: 'GET', mode:'cors})
  .then(respone = >{
      // do something...
  });

也可以使用其它的类似解决方案,例如:node-http-proxyexpress-http-proxy等。我们内建了这个服务,是为了让开发人员像原生端开发人员一样,更专注于业务开发,不再为跨域、代理路径、代理服务配置等问题困扰。

如果使用joyjoy-start来启动应用,不需要任何配置,即可使用跨域服务。如果项目采用了自定义Server,需要开发者将@symph/joy/proxy-api-middleware代理服务注册到自定义的Server中。

const express = require('express')
const symph = require('@symph/joy')
const {createProxyApiMiddleware} = require('@symph/joy/proxy-api-middleware')

const app = symph({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
  const server = express()
  server.use(createProxyApiMiddleware())  //register proxy, 
  server.get('*', (req, res) => {
    return handle(req, res)
  })
})

createProxyApiMiddleware(options)fetch(url, options)方法的options参数对象中可以使用proxyPrefix参数,用于设置proxy的url路径前缀,当应用不是部署在url根路径下时,这非常有用。

应用组件

<!-- 由于javascript语言的开放性,在实际的开发工作中,不同的团队和开发人员,所编写的应用在结构和代码风格上往往存在较大的差异,这给迭代维护和多人协同开发带来了麻烦, 同时为了让开发人员更专注于业务开发,@symph/joy提供了以下应用层组件,来提高开发效率。 -->

app work flow

图中蓝色的箭头表示数据流的方向,红色箭头表示控制流的方向,在内部使用redux来实现整个流程,为了更好的推进工程化以及简化redux的实现,我们抽象了出了Controller和Model两个类,从上图中可以看到,我们的业务都是通过这两个类协同工作实现的,它们只是包含业务方法和生命周期的简单类。

为了更好的理解以下内容,可先阅读以下相关知识:reduxdva concepts

Model

正如我们所知,在任何场合都要求视图和业务分离,Model就是完全负责业务处理的,在@symph/joy里,我们不应该把业务相关的代码放到Model以外的地方,同时Model也是存放业务数据的地方,我们应该保证同一个数据,在应用中只应唯一的存在一处,这样的数据才可控。

Model拥有初始状态initState和更新状态的方法setState(nextState),这和Component的state概念类似,在业务方法(也被叫做effects方法)执行过程中,更新Model中的state,这里并没有什么魔法和创造新的东西,只是将redux的actionactionCreatorreducer,thunk等复杂概率抽象成业务状态和流程,从而方便代码管理,开发时也更专注于业务.

下面是一个简单的model示例:

import model from '@symph/joy/model'

@model()
export default class ProductsModel {

  // the mount point of store state tree, must unique in the app.
  namespace = 'products';

  // model has own state, this is the initial state
  initState = {
    pageIndex: null,
    pageSize: 5,
    products: [],
  };

  async getProducts({pageIndex = 1, pageSize}) {
    // fetch data
    let data = await new Promise((resolve, reject) => {
      setTimeout(() => {
        let resultData = [];
        for (let i = (pageIndex - 1) * pageSize; i < pageIndex * pageSize; i++) {
          resultData.push({
            id: i,
            name: 'iphone 7',
            price: 4999,
          })
        }
        resolve(resultData)
      }, 200);
    });

    let {products} = this.getState();
    if (pageIndex === 1) {
      products = data;
    } else {
      products = [...products, ...data];
    }

    this.setState({
      products,
      pageIndex,
      pageSize
    });
  }

};

我们使用@model()将一个类声明为Model类,Model类在实例化的时候会添加getStatesetStatedispatch等快捷方法。

Model API
namespace

model将会被注册到store中,由store统一管理model的状态,使用store.getState()[namespace]来访问对应model的state, store中不能存在两个相同的namespace的model。

initState

设置model的初始化状态,由于model.state可能会被多个async业务方法同时操作,所以为了保证state的有效性,请在需要使用state时使用setState(nextState)来获取当前state的最新值,并使用getState()方法更新当前的state。

setState(nextState)

setState(nextState)更新model的状态,nextState是当前state的一个子集,系统将使用浅拷贝的方式合并当前的状态,并更新store的state。

getState()

getState()获取当前model的状态。

getStoreState()

getStoreState()获取当前整个store的状。

dispatch(action)

返回值:Promise,目标业务方法的返回值。

和redux的store.dispatch(action)的使用一样,由系统分发action到指定的model业务方法中, action.type的格式为modelNamespace/customServiceMethod

为了方便调用model中的其它业务方法,可直接使用await this.effect(action)的方式调用。

async effects(action)

我们将实现具体业务功能的函数,称之为effect方法。在controller或者其他model中通过dispatch(action)方法调用这类方法,effect方法是async函数,在里面可以使用await来编排业务逻辑,并返回Promise对象作为dispatch的返回值,所以可以在业务调用方,通过检测Promise来获得effect执行的结果。

Dva Model

我们同时兼容dva风格的model对象,使用方法和上面一样,model对象的定义请参考 Dva Concepts ;

Controller

Controller的作用是连接View和Model组件,并新增了async componentPrepare()生命周期方法,该方法是一个异步函数,在服务端渲染时,会等待该方法执行完成后,才会渲染出界面,浏览器会直接使用在服务端获取到的数据来渲染界面,不再重复执行componentPrepare方法。如果没有启动服务端渲染,或者是在浏览器上动态加载该组件时,该方法将在客户端上自动运行。

import React, {Component} from 'react';
import ProductsModel from '../models/ProductsModel'
import controller, {requireModel} from '@symph/joy/controller'


@requireModel(ProductsModel)          // register model
@controller((state) => {              // state is store's state
  return {
    products: state.products.products // bind model's state to props
  }
})
export default class IndexController extends Component {

  async componentPrepare() {
    let {dispatch} = this.props;
    // call model's effect method
    await dispatch({
      type: 'products/getProducts', 
      pageIndex: 1,
      pageSize: 5,
    });
  }

  render() {
    let {products = []} = this.props;
    return (
      <div >
        <div>Product List</div>
        <div>
          {products.map((product, i) => {
            return <div key={product.id} onClick={this.onClickProduct.bind(product)}>{product.id}:{product.name}</div>
          })}
        </div>
      </div>
    );
  }
}

创建和使用Controller的步骤:

  • 使用@controller(mapStateToProps)装饰器将一个普通的Component声明为一个Controller,mapStateToProps参数实现model状态和组件props属性绑定,当model的state发生改变时,同时会触发组件props的改变并重新渲染界面。

  • 使用@requireModel(ModelClass)注册Controller需要依赖的Model,在浏览器端页面可能是按需加载的,所以通常只需要第一个使用到Model的Controller上注册一次就可以了,重复注册无效,但也会出任何问题。

  • 每个controller的props都会被注入一个redux的dispatch方法,dispatch方法是controller调用model的唯一途径,该方法的返回值是业务方法的返回值,一个promise对象。

Router

使用方法请参考:react-router-4

我们并未对react-router-4做任何的修改,仅仅只是封装了一个外壳,方便统一调用。

导入路径

import {Switch, Route} from '@symph/joy/router'

代码启动 Server

通常我们使用joy start来启动应用,但是我们依然可以使用纯代码来启动@symph/joy应用,以次来集成到其它的服务器框架中,比如expresskoa等。

下面例子展示了,如何集成到express中,并且修改路由\a\b.

// server.js
const express = require('express')
const joy = require('@symph/joy')

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = joy({ dev, dir: '.' })
const handle = app.getRequestHandler()

const server = express()
const preapredApp = app.prepare()

server.get('/a', (req, res) => {
  preapredApp.then(() => {
    return app.render(req, res, '/b', req.query)
  })
})

server.get('*', (req, res) => {
  preapredApp.then(() => {
    return handle(req, res);
  })
})

server.listen(port, (err) => {
  if (err) throw err
  console.log(`> Ready on http://localhost:${port}`)
})

通过集成到已有的express服务器中时,我们的应用是挂载到url的某个子路径上的,此时请参考assetPrefix的配置说明。

joy(options: object) API 提供以下参数:

  • dev: bool: false 是否以开发模式启动应用
  • dir: string: '.' 应用放置的路径,相对于server.js文件
  • quiet: bool: false 是否隐藏服务器错误信息
  • conf: object: {} 和joy.config.js相同的配置对象,如果设置了该值,则忽略joy.config.js文件。

最后修改NPM start脚本:

{
  "scripts": {
    "dev": "joy",
    "build": "joy build",
    "start": "NODE_ENV=production node server.js"
  }
}

从上面例子中可以看到,可以使用函数调用的方式来渲染界面,所以我们将@symph/joy作为express、koa等服务端框架的View模块来使用。

动态导入 import

@symph/joy支持JavaScript的TC39 dynamic import提议,意味着你可以将代码分割为多个代码块,在浏览器上加载时,按需import需要的模块。同时这并不影响服务端渲染,这是因为@symph/joy/dynamic在服务端渲染时,依然使用同步的方式加载import的模块。

下面展示了@symph/joy/dynamic的2种用法:

基础用法和配置:

  • ssr: bool: true, 设置是否开启服务端渲染
  • loading: Component: <p>loading...</p> 加载过程中,展示的组件
import dynamic from '@symph/joy/dynamic'

const DynamicComponent = dynamic(import('../components/hello'), {
   ssr: true,
   loading:<div>...</div>
})

export default () =>
  <div>
    <Header />
    <DynamicComponent />
    <p>HOME PAGE is here!</p>
  </div>

一次加载多个模块

import dynamic from '@symph/joy/dynamic'

const HelloBundle = dynamic({
  modules: props => {
    const components = {
      Hello1: import('../components/hello1'),
      Hello2: import('../components/hello2')
    }
    // Add remove components based on props
    return components
  },
  render: (props, { Hello1, Hello2 }) =>
    <div>
      <h1>
        {props.title}
      </h1>
      <Hello1 />
      <Hello2 />
    </div>
})

export default () => <HelloBundle title="Dynamic Bundle" />

自定义 <Document>

  • 服务端渲染时,使用该组件生成静态的html文档

  • 如果需要在后html文件引入额外的<script><lint>标签,需要自定义<Document>,例如在使用@symph/joy-css插件时,需要引入/_symphony/static/style.css样式文件。

@symph/joy中,<Main>组件(默认存放路径:src/index.js)中只需包含功能代码,不能包含document标签中的<head><body>部分,这样设计目的是让开发从一开始就专注于业务。<Main>以外的部分,并不会在浏览器端初始化,所以不能在这里放置任何的业务代码,如果希望在整个应用里共享一部分功能,请将它们放到<Main>中。

import Document, { Head, Main, JoyScript } from '@symph/joy/document'

export default class MyDocument extends Document {
  render () {
    return (
      <html>
        <Head>
          {/* add custom style file */}
          <link rel='stylesheet' href='/_symphony/static/style.css' />
        </Head>
        <body>
          <Main />
          <JoyScript />
        </body>
      </html>
    )
  }
}

打包部署

部署的时候,我们先使用joy build命令来预编译源代码,生成.joy目标目录(或者使用distDir设置自定义的目录名称),然后将项目上传到生产机器上,在生产机器上执行joy start命令,直接启动应用。我们可以在package.json中添加以下内容:

{
  "name": "my-app",
  "dependencies": {
    "@symph/joy": "latest"
  },
  "scripts": {
    "dev": "joy",
    "build": "joy build",
    "start": "joy start"
  }
}

@symph/joy 可以部署到不同的域名或路径上,可参考assetPrefix的设置说明。

在运行joy build的时候,NODE_ENV被默认设置为production, 使用joy启动开发环境的时候,设置为development。如果你是在自定义的Server内启动了应用,需要你自己设置NODE_ENV=production

静态HTML输出

joy export用于将@symph/joy app输出为静态html资源,可在浏览器上直接访问,而不需要Node.js服务器。导出后的静态版本,仍然支持@symph/joy的绝大部分特性,比如:动态路由、按需加载等。

joy export的原理是将请求可渲染的部分,预先渲染为HTML,这和当用户request到达Node.js服务器上时,实时渲染的工作流程一样。默认只渲染出index.html文件,浏览器加载该文件后,客户端Router再根据当前url,加载相应的页面。这要求我们在业务服务器上,例如JAVA的Spring MVC中,使用@RequestMapping(path="/**", method=RequestMethod.GET)正则路由来匹配应用内部的所有路径,并都返回index.js这个文件。

@Controller
@RequestMapping("/**")
public class ViewController {

    @RequestMapping(path = "/**", method = RequestMethod.GET)
    public Map<String, Appointment> pages() {
       return "forward:/static/index.html";
    }

}

导出步骤

在没有任何的配置情况下,joy export提供默认的配置exportPathMap进行导出,如果你需要添加其它导出页面,请先在joy.config.js中设置exportPathMap参数。

接下来我们分两步进行导出操作:

  1. 编译源代码 joy build
  2. 预渲染需要导出的页面 joy export

添加NPM脚本到package.json文件中:

{
  "scripts": {
    "build": "joy build",
    "export": "npm run build && joy export"
  }
}

现在执行下面一个命令,完成整个导出工作:

npm run export

以上执行完成以后,将得到该应用的静态版本,静态版本需要的所有文件都放置在应用根目录下的out目录中,只需要将out目录部署到静态文件服务器就可以了。

你可以定制out目录名称,请运行joy export -h按提示操作。

转载于:https://my.oschina.net/u/3896268/blog/1858930

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值