内容简介:配置完毕之后,接下来就开始开发一个简单的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+前后端同构的尝试就到此完成了!
系列文章:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
D3.js in Action
Elijah Meeks / Manning Publications / 2014-3 / USD 44.99
Table of Contents Part 1: An Introduction to D3 1 An introduction to D3.js 2 Information Visualization Data Flow 3 D ata-Driven Design and Interaction Part 2: The Pillars of Information......一起来看看 《D3.js in Action》 这本书的介绍吧!