使用指南
安装和开始
运行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-proxy、express-http-proxy等。我们内建了这个服务,是为了让开发人员像原生端开发人员一样,更专注于业务开发,不再为跨域、代理路径、代理服务配置等问题困扰。
如果使用joy
或joy-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提供了以下应用层组件,来提高开发效率。 -->
图中蓝色的箭头表示数据流的方向,红色箭头表示控制流的方向,在内部使用redux来实现整个流程,为了更好的推进工程化以及简化redux的实现,我们抽象了出了Controller和Model两个类,从上图中可以看到,我们的业务都是通过这两个类协同工作实现的,它们只是包含业务方法和生命周期的简单类。
为了更好的理解以下内容,可先阅读以下相关知识:redux, dva concepts
Model
正如我们所知,在任何场合都要求视图和业务分离,Model就是完全负责业务处理的,在@symph/joy里,我们不应该把业务相关的代码放到Model以外的地方,同时Model也是存放业务数据的地方,我们应该保证同一个数据,在应用中只应唯一的存在一处,这样的数据才可控。
Model拥有初始状态initState
和更新状态的方法setState(nextState)
,这和Component的state概念类似,在业务方法(也被叫做effects方法)执行过程中,更新Model中的state
,这里并没有什么魔法和创造新的东西,只是将redux的action
、actionCreator
、reducer
,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类在实例化的时候会添加getState
、setState
,dispatch
等快捷方法。
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
应用,以次来集成到其它的服务器框架中,比如express
、koa
等。
下面例子展示了,如何集成到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
参数。
接下来我们分两步进行导出操作:
- 编译源代码
joy build
- 预渲染需要导出的页面
joy export
添加NPM脚本到package.json
文件中:
{
"scripts": {
"build": "joy build",
"export": "npm run build && joy export"
}
}
现在执行下面一个命令,完成整个导出工作:
npm run export
以上执行完成以后,将得到该应用的静态版本,静态版本需要的所有文件都放置在应用根目录下的out
目录中,只需要将out
目录部署到静态文件服务器就可以了。
你可以定制
out
目录名称,请运行joy export -h
按提示操作。