内容简介:配置完毕之后,接下来就开始开发一个简单的Demo页面吧~ 首先要定义好Demo的model模型:将定义好的然后编写container组件即可:
配置完毕之后,接下来就开始开发一个简单的Demo页面吧~ 首先要定义好Demo的model模型:
models/demo.ts
import { demoModalState } from "typings";
import { createModel } from "@rematch/core";
export const demo = createModel({
state: ({
outstr: "Hello World",
count: 10
} as any) as demoModalState,
reducers: {
"@init": (state: demoModalState, init: demoModalState) => {
state = init;
return state;
},
add(state: demoModalState, num) {
state.count = state.count + (num || 1);
return state;
},
reverse(state: demoModalState) {
state.outstr = state.outstr
.split("")
.reverse()
.join("");
return state;
}
}
});
复制代码
将定义好的 interface 统一放到 typings 目录下面。
typings/state/demo.d.ts
export interface demoModalState {
count?: number;
outstr?: string;
}
复制代码
然后编写container组件即可:
containers/demo/index.tsx
import React, { Component } from "react";
import { connect } from "react-redux";
import { Button } from "antd";
import { DemoProps } from "typings";
import utils from "lib/utils";
import "./demo.scss";
class Demo extends Component<DemoProps> {
static defaultProps: DemoProps = {
count: 0,
outstr: "Hello World",
Add: () => void {},
Reverse: () => void {}
};
constructor(props) {
super(props);
}
render() {
const { Add, Reverse, count, outstr } = this.props;
return (
<div>
<Button type="primary" onClick={Reverse}>
click me to Reverse words
</Button>
<span className="output">{outstr}</span>
<Button onClick={() => Add(1)}>click me to add number</Button> now
number is : {count}
</div>
);
}
}
const mapStateToProps = (store: any) => ({
...store.demo,
url: store.common.url
});
const mapDispatchToProps = (dispatch: any) => ({
Add: dispatch.demo.add,
Reverse: dispatch.demo.reverse
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Demo);
复制代码
最后将组件注册进路由中就大功告成了:
entry/home/routes.tsx
import Loadable from "react-loadable";
import * as Path from "constants/path";
import Loading from "components/loading";
export default [
{
name: "demo",
path: Path.Demo,
component: Loadable({
loader: () => import("containers/demo"),
loading: Loading
}),
exact: true
}
];
复制代码
Path.Demo 是定义的常量,值为 /demo 。
前端组件写完了之后,别忘了对应的node中的路由和ssr的代码。
/src/routes/index.ts
import Router from "koa-router";
import homeController from "controllers/homeController";
const router = Router();
router.get("/demo", homeController.demo);
export default router;
复制代码
接下来就是业务处理的 homeController 文件了:
src/controllers/homeController.tsx
import getPage from "../utils/getPage";
import { Entry, configureStore } from "../public/buildServer/home";
interface homeState {
demo: (ctx: any) => {};
}
const home: homeState = {
async demo(ctx) {
const store = configureStore({
demo: {
count: 10,
outstr: "Hello World!"
}
});
const page = await getPage({
store,
url: ctx.url,
Component: Entry,
page: "home",
model: "demo"
});
ctx.render(page);
}
};
export default home;
复制代码
好!第一个SSR页面大功告成!
接下来启动打包之后访问页面即可
$ npm run startfe $ npm run start 复制代码
注意,node中的ssr代码需要使用前端打包的产物,因此在 startfe 没有结束之前运行 start 会报错的!
最后访问 localhost:7999/demo 页面就可以查看效果了。
todolist页面
第一个页面构建完毕之后,我们可以在写一个复杂一点的todolist页面来检查一下 react-router 的spa效果,以及完善后续的首屏数据加载的问题。
依然是先定义model:
models/todolist.ts
import { createModel } from "@rematch/core";
import { todoListModal } from "typings";
export const todolist = createModel({
state: ({
list: []
} as any) as todoListModal,
reducers: {
"@init": (state: todoListModal, init: todoListModal) => {
state = init;
return state;
},
deleteItem: (state: todoListModal, id: string) => {
state.list = state.list.filter(item => item.id !== id);
return state;
},
addItem: (state: todoListModal, text: string) => {
const id = Math.random()
.toString(16)
.slice(2);
state.list.push({
id,
text
});
return state;
}
},
effects: dispatch => ({
async asyncDelete(id: string) {
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
dispatch.todolist.deleteItem(id);
return Promise.resolve();
}
})
});
复制代码
只需要这些代码就可以完成一个以前十分复杂的react-redux版的todolist,是不是感觉@rematch非常友好!
接下来写一个简单的todolist页面:
containers/todolist/index.tsx
import React, { Component } from "react";
import { connect } from "react-redux";
import { todolistProps, todolistState } from "typings";
import utils from "lib/utils";
import "./todolist.scss";
class Todolist extends Component<todolistProps, todolistState> {
constructor(props) {
super(props);
this.state = {
text: ""
};
utils.bindMethods(
["addItem", "changeInput", "deleteItem", "asyncDelete"],
this
);
}
addItem() {
const { text } = this.state;
this.props.addItem(text);
this.setState({
text: ""
});
}
deleteItem(id: string) {
this.props.deleteItem(id);
}
asyncDelete(id: string) {
this.props.asyncDelete(id);
}
changeInput(e) {
this.setState({
text: e.target.value
});
}
render() {
const { list = [] } = this.props;
const { text } = this.state;
return (
<>
<input className="input" value={text} onChange={this.changeInput} />
<button onClick={this.addItem}>Add</button>
<ol className="todo-list">
{list.map(item => {
return (
<li className="todo-item" key={item.id}>
<span>{item.text}</span>
<button onClick={() => this.deleteItem(item.id)}>delete</button>
<button onClick={() => this.asyncDelete(item.id)}>
async delete
</button>
</li>
);
})}
</ol>
</>
);
}
}
const mapStateToProps = store => {
return {
...store.todolist
};
};
const mapDispatchToProps = dispatch => {
return {
...dispatch.todolist
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Todolist);
复制代码
然后别忘了给前端和后端路由注册组件:
js/entry/home/routes.tsx
import Loadable from "react-loadable";
import * as Path from "constants/path";
import Loading from "components/loading";
export default [
{
name: "demo",
path: Path.Demo,
component: Loadable({
loader: () => import("containers/demo"),
loading: Loading
}),
exact: true
},
{
name: "todolist",
path: Path.Todolist,
component: Loadable({
loader: () => import("containers/todolist"),
loading: Loading
}),
exact: true
}
];
复制代码
Path.Todolist 是定义的常量,值为 / 。
src/routes/index.ts
import Router from "koa-router";
import homeController from "controllers/homeController";
const router = Router();
router.get("/", homeController.index);
router.get("/demo", homeController.demo);
export default router;
复制代码
最后完善一下全局的 Layout 组件,加上两个公共路由即可:
js/components/layout/index.tsx
import React, { Component } from "react";
import { Link } from "react-router-dom";
import * as Path from "constants/path";
export default class Layout extends Component {
render() {
return (
<>
<h4>
<Link to={Path.Todolist}>Todo List</Link>
</h4>
<h4>
<Link to={Path.Demo}>demo</Link>
</h4>
<div>{this.props.children}</div>
</>
);
}
}
复制代码
然后再访问我们的页面,就可以看到顶部有两个常驻的路由供我们切换了
至此spa+ssr的构建就完成了!
首屏数据加载
首屏数据即在node中提前加载访问的第一个页面的数据,其他页面没有数据的预加载。
得意于 @rematch/dispatch 的便利性,我们可以给每个 model 都定义一套公共的用于拉取首屏数据的函数 prefetchData()
因此我们给两个 model 都改造一下L
models/todolist.ts
import { createModel } from "@rematch/core";
import { todoListModal } from "typings";
export const todolist = createModel({
state: ({
list: []
} as any) as todoListModal,
reducers: {
"@init": (state: todoListModal, init: todoListModal) => {
state = init;
return state;
},
deleteItem: (state: todoListModal, id: string) => {
state.list = state.list.filter(item => item.id !== id);
return state;
},
addItem: (state: todoListModal, text: string) => {
const id = Math.random()
.toString(16)
.slice(2);
state.list.push({
id,
text
});
return state;
}
},
effects: dispatch => ({
async asyncDelete(id: string) {
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
dispatch.todolist.deleteItem(id);
return Promise.resolve();
},
async prefetchData(init) {
dispatch.todolist["@init"](init);
return Promise.resolve();
}
})
});
复制代码
models/demo.ts
import { demoModalState } from "typings";
import { createModel } from "@rematch/core";
export const demo = createModel({
state: ({
outstr: "Hello World",
count: 10
} as any) as demoModalState,
reducers: {
"@init": (state: demoModalState, init: demoModalState) => {
state = init;
return state;
},
add(state: demoModalState, num) {
state.count = state.count + (num || 1);
return state;
},
reverse(state: demoModalState) {
state.outstr = state.outstr
.split("")
.reverse()
.join("");
return state;
}
},
effects: dispatch => ({
async prefetchData() {
const number = await new Promise(resolve => {
setTimeout(() => {
console.log("prefetch first screen data!");
resolve(13);
}, 1000);
});
dispatch.demo.add(number);
return Promise.resolve();
}
})
});
复制代码
有了 prefetchData 函数之后,我们就可以在node做ssr的时候直接调用这个函数即可完成首屏数据的加载。
src/utils/getPage.tsx
import { getBundles } from "react-loadable/webpack";
import React from "react";
import { getScript, getStyle } from "./bundle";
import { renderToString } from "react-dom/server";
import Loadable from "react-loadable";
export default async function getPage({
store,
url,
Component,
page,
model,
params = {}
}) {
const manifest = require("../public/buildPublic/manifest.json");
const mainjs = getScript(manifest[`${page}.js`]);
const maincss = getStyle(manifest[`${page}.css`]);
if (!Component && !store) {
return {
html: "",
scripts: mainjs,
styles: maincss,
__INIT_STATES__: "{}"
};
}
let modules: string[] = [];
const dom = (
<Loadable.Capture
report={moduleName => {
modules.push(moduleName);
}}
>
<Component url={url} store={store} />
</Loadable.Capture>
);
// prefetch first screen data
if (store.dispatch[model] && store.dispatch[model].prefetchData) {
await store.dispatch[model].prefetchData(params);
}
const html = renderToString(dom);
const stats = require("../public/buildPublic/react-loadable.json");
let bundles: any[] = getBundles(stats, modules);
const _styles = bundles
.filter(bundle => bundle && bundle.file.endsWith(".css"))
.map(bundle => getStyle(bundle.publicPath))
.concat(maincss);
const styles = [...new Set(_styles)].join("\n");
const _scripts = bundles
.filter(bundle => bundle && bundle.file.endsWith(".js"))
.map(bundle => getScript(bundle.publicPath))
.concat(mainjs);
const scripts = [...new Set(_scripts)].join("\n");
return {
html,
__INIT_STATES__: JSON.stringify(store.getState()),
scripts,
styles
};
}
复制代码
这里我们多了两个参数—— model 和 params ,分别表示当前的 model 以及要传入 prefetchData 函数的参数。
然后我们在处理一下 homeController 中调用 getPage 的地方就完成了:
src/controllers/homeController.tsx
import getPage from "../utils/getPage";
import { Entry, configureStore } from "../public/buildServer/home";
interface homeState {
index: (ctx: any) => {};
demo: (ctx: any) => {};
}
const home: homeState = {
async index(ctx) {
const store = configureStore({
todolist: {
list: []
}
});
const page = await getPage({
store,
url: ctx.url,
Component: Entry,
page: "home",
model: "todolist",
params: {
list: [
{
id: "hello",
text: "node prefetch data"
}
]
}
});
ctx.render(page);
},
async demo(ctx) {
const store = configureStore({
demo: {
count: 10,
outstr: "Hello World!"
}
});
const page = await getPage({
store,
url: ctx.url,
Component: Entry,
page: "home",
model: "demo"
});
ctx.render(page);
}
};
export default home;
复制代码
所有工作准备就绪之后,再次打开我们的网站,访问 localhost:7999 ,发现已经可以顺利的加载首屏数据了。
首屏数据加载优化
我们并不想只有经过node访问的页面才会拉取数据,经过前端路由切换的页面也要加载首屏数据,只不过是在 componentDidMount 之后再加载而已,因此我们需要改造一下 demo 组件:
containers/demo.tsx
// ...
componentDidMount() {
this.props.prefetchData();
}
// ...
复制代码
改造完之后,我们发现当首屏加载的是 /todolist 页面的时候,前端切换到 /demo 页面,过一会会成功触发 prefetchData() 函数, count 变成了23。
但是当我们直接访问 /demo 页面的时候,却发现经过的node的首屏数据加载之后, count 的初始值就是23,然后过了一会 prefetchData() 执行完之后 count 变成了36,这不符合我们的预期,因此首屏数据加载这里还需要优化。
我们需要判断哪个页面进行了首屏数据加载,当该页面已经进行了首屏数据加载之后, didmount 时便不再加载数据。
因此这里我想了几种办法之后,最后选择了记录url的方式。
增加一个公共的model: common
models/common.ts
import { CommonModelState } from "typings";
import { createModel } from "@rematch/core";
export const common = createModel({
state: ({} as any) as CommonModelState,
reducers: {
"@init": (state: CommonModelState, init: CommonModelState) => {
state = init;
return state;
}
}
});
复制代码
然后在 homeController 中初始化store的时候将url注入到 common 这个model里面:
homeController.ts
const store = configureStore({
common: {
url: ctx.url
},
// ...
});
复制代码
这样我们就可以通过common这个model中的url参数获知到已经经过首屏数据加载的页面了,然后对 container 的 connect 部分改造一下,将 url 参数注入到 props 中:
containers/demo/index.tsx
const mapStateToProps = (store: any) => ({
...store.demo,
url: store.common.url
});
复制代码
接下来在 utils 中写一个拉取数据的函数,根据当前 location 和 props.url 来判断是否需要拉取数据。
js/lib/utils.ts
const utils = {
// ...
fetchData(props, fn) {
const { location, url } = props;
if (!location || !url) {
fn();
return;
}
if (location.pathname !== url) {
fn();
}
}
};
export default utils;
复制代码
最后给每一个 container 加上 fetchData 函数即可:
componentDidMount() {
utils.fetchData(this.props, this.props.prefetchData);
}
复制代码
至此,首次进行SPA+SSR+前后端同构的尝试就到此完成了!
系列文章:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Domain-Driven Design
Eric Evans / Addison-Wesley Professional / 2003-8-30 / USD 74.99
"Eric Evans has written a fantastic book on how you can make the design of your software match your mental model of the problem domain you are addressing. "His book is very compatible with XP. It is n......一起来看看 《Domain-Driven Design》 这本书的介绍吧!