Open ascoders opened 8 years ago
Mark! 满满干货。
赞,前排留坐....
感觉如果不想调用两次RenderToString
,可以参考react-redux-universal-hot-example,里面使用到了redux-async-connect来触发所有需要的网络请求。
当然,局限性是有的。因为这个做法是依据match
返回的components
上面的reduxAsyncConnect
来找到需要执行的异步请求,故而无需调用RenderToString
来触发网络请求,却增加了以下局限性:
asyncConnect
部分,位于component的decorator);<Route />
定义的component才可以定义初始网络请求。不过,我觉得这两个限制都是有意义的,而且对大部分情况都适用。唯一不好的地方是若有历史代码,须根据第一点修改。但使用在新项目中就很爽了。
当然,React貌似也有计划增加异步渲染的逻辑,那以后做起来就更方便了。
ps: 社区也有很多异步的renderToString的实现,例如react-async,不足是并非官方提供,并且需要稍微改变定义component的方式(一般采用decorator的方式)
感谢 @jetzhliu 提供 React 未来会增加异步渲染的重要计划。
另外 reduxAsyncConnect 的思路与将请求放置到 static 变量中无异,本人觉得有第三种局限性:无法获取组件实例,这直接导致了后端请求的参数可能无法与前端代码无缝对接。
比如组件某个实例的 state
就无法被访问到。而渲染两次最大好处在于后端请求部分不需要拆出来,发送请求的参数能取自每个组件实例内部的变量。
再次期待 React 异步渲染的官方支持,非常赞!
有简单但完整demo没?
@ouvens 待我整理一下,这周末前会发出来
能否发下前端目录结构并说明下各目录的代码规范?
期待 demo
最好按照 state 树结构拆分文件夹,每个文件夹下对应
action.tsx
与store.tsx
这里的 store.tsx 是干什么用的?
@ystarlongzi 笔误,是 reducer.tsx
,已修正。
@codering 今天把 demo 整理出来后,再写一篇目录规范描述
@ouvens @ystarlongzi demo传送门: https://github.com/ascoders/isomorphic-react-redux-app
@codering 下面是项目启动介绍,目录结构在项目里,感谢支持。
In node-server
directory, run the following commands:
cnpm install
npm start
In my-app
directory, run the following commands:
cnpm install
npm start
This command is executed with Webpack, and you can create other app's folder such like second-app
sell-system
...
Then visit http://localhost:8080/my-app
Warning: You might see the TS error, we're trying to fix it, it does not prevent development
In my-app
directory, run the following commands:
npm run preview
This command is executed with Fis3, use production setting
npm run remote
In my-app
directory, change host
in fis-conf.js
, your code will be pushed to remote machine
In my-app
directory, run the following commands:
npm run production
Then you can find static-my-app.tar
my-app.tar
in output dictionary
@ascoders
辛苦啦!
@ascoders
对了,文件后缀名 .tsx 有什么寓意嘛?
对了,文件后缀名 .tsx 有什么寓意嘛?
tsx = typescript + jsx,在大型项目中不能保证大家技术栈是统一的,通过标准的文件名可以做到区分,同时明确的后缀名可以让编译工具更明确,IDE识别得更智能!
牛逼~点赞,构建、编译脚本啥时候开源呐
感谢标叔捧场了,献上构建脚本传送门:https://github.com/fit-component/isomorphic-build
mark
要 mark 的同学,直接点右边的 subscribe
还是很期待的
前端是个比较苦逼的工种,面临着一年一变的开发框架,一季一变的脚手架,一月一变工具库,这几年现已经发展到整个开发生态圈一年一变。
然而对于新技术的追求是一定要有的,毕竟唯一不变的东西就是变化,在互联网行业跟不上变化就等于淘汰。对于比较有开发经验的前端同学们来说,学习一项新的框架是非常轻松的,积极订阅技术周刊、看文档、逛github都可以使你迅速跟上前端变化的节奏。
回到现实,在大公司的大业务线,比如我所负责的百度贴吧,情况没有那么乐观。一个十多年的业务线所积累的业务代码是每一个个体无法想象,也无法掌控的,贴吧的前端代码几乎反应了整个前端历史的发展轨迹:在体系复杂的基础项目、林林种种的创新项目、变化多样的运营项目中,几乎所有博文中介绍过的优雅,神奇,黑科技的方法毫无例外都被使用过,框架集中在了jquery生态,是jquery时代混合php编程的经典范例。然而随着前端的发展,产品迭代的加速,旧的前端开发架构已经越来越无力。
在前后端分离开发方式早就被实践的今天,想在贴吧做一点点改变也会受到编译脚本、模块耦合,php全环境问题的困扰,任何小小的优化都会牵一发而动全身,于是我们开始了漫长的改造,从制作新的编译脚本,使用新开发流程,对fis通用化定制,以及后端UI层改为nodejs全方位辅助前端模块化开发,框架选用了React。
写到这里,应该总结一些为什么要使用React理由,毕竟前端变化那么快,为什么这么看好React呢?React不仅仅有非常优秀的模块化机制,普通的业务模块也能拆出来拥抱npm,更重要的是推出了虚拟dom思想,提高dom渲染效率,使得跨平台开发成为可能。也许在未来web app会替代native app(假设),可是虚拟dom更使后端渲染成为了可能,web app也需要借助虚拟dom的优势优化首屏用户体验。
Fis3 vs Webpack
fis3是完整的前端构建工具,webpack是前端打包工具,现在fis3也拥有了webpack对npm生态打包的能力,详情参考这篇文章:如何用 fis3 来开发 React?。
让 fis3 拥有 webpack 的打包能力,只需要
fis-conf.js
添加如下配置:假设我们将项目分为
client
与server
,可以按需加载前端用到的文件:再将前端文件使用 typescript 编译:
如果上线后需要将文件发布到 cdn 域名下,可以动态替换,同时开启压缩等操作:
生产环境需要压缩前端文件:
这样就将所有
js
依赖文件都打包到/client/pkg/bundle.js
,css
文件都打包到/client/pkg/bundle.css
,同时fis3会自动替换html中的引用。Yog2 vs express
yog2是基于express封装的nodejs UI层解决方案,文档地址。主要特点使用了app拆分,使得协同开发变得方便。业务项目与node根项目分离,本地开发时,使用fis3的
http-push
能力提交到node根项目子项目文件夹中,保证不同业务项目的分离。先安装yog2:
运行:
让项目上传到 yog2 根项目中,需要修改
fis-confg.js
:支持 bigpipe、quickling,以及后端渲染,默认支持mvc模式,自动路由:
/server/api/user.ts
的default export
默认监听/[project-name]/api/user
这个url。开发中支持热更新,只要添加
--watch
参数,无需重启 node 就可以更新代码逻辑:Fit vs Antd
Fit和Antd类似,是一款基于commonjs规范的React组件库,同时提供了对公司内部业务线的定制组件,不同的是,Fit组件源码使用typescript编写,使得可维护性较强,由FEX团队负责维护(现在还未对外开放)。
除了提供通用的业务组件以外,还提供了同构插件 fit-isomorphic-redux-tools,这个组件提供了基于redux的同构渲染方法支持。
React 后端渲染企业级实践
先从业务角度解析一遍后端渲染准备工作,之后再解析内部原理。
后端模板的准备工作
对纯前端页面来说,后端模板只需要提供基础模板,以及各种
api
接口。为了实现后端渲染,需要根据当前(html5)路由动态添加内容放到模版中去,因此fit-isomorphic-redux-tools
提供了封装好的serverRender
函数:server/action/index.ts
server/router.ts
从
server/router.ts
说起,引入了 service(下一节介绍),对非/api
开头的 url 路径返回server/action/index.ts
文件中的内容。server/action/index.ts
这个文件引用了三个client
目录下文件,分别是routes
路由定义、basename
此模块的命名空间、rootReducer
redux 聚合后的 reducer。读取了client/index.html
中内容,最后将参数全部传入serverRender
函数中,通过enableServerRender
设置是否开启后端渲染。如果开启了后端渲染,访问页面时,会根据当前路由渲染出对应的 html 片段插入到模板文件中返回给客户端。在后端抽象出统一的 service 接口
server/service/index.ts
fit-isomorphic-redux-tools
还提供了两个工具initService
export 出去供 router 绑定路由,routerDecorator
是个装饰器,第一个参数设置 url 地址,第二个参数设置 httpMethod。定义一个 Service 类,每一个成员函数都是对应的后端 api 函数,支持同步和异步方法。最后创建一个 Service 的实例。当通过 http 请求访问时,同步和异步方法是没有任何区别的,当请求从后端执行时,不会发起新的 http 请求 ,而是直接访问到这个函数,对异步函数进行异步处理,使得与同步函数效果统一。
自此后端模块介绍完毕了,可以对 service 进行自由拆分,例如分成多个文件继承等等。
前端模板文件处理
client/index.html
引入
mod.js
是为了支持 fis 的模块化寻找(webpack将类似逻辑预制到打包文件中,所以不需要手动引用),index.tsx
是入口文件,需要通过fis-conf.js
设置其为非模块化(仅入口非模块化),之后都是模块化引用:window.__INITIAL_STATE__ = __serverData('__INITIAL_STATE__');
这段代码存在的意义是,后端渲染开启时,会替换__serverData('__INITIAL_STATE__')
为后端渲染后的内容,在 redux 初始化时传入window.__INITIAL_STATE__
参数,让前端继承了后端渲染后的 store 状态,之后页面完全交给前端接手。前端入口文件处理
client/index.tsx
fit-isomorphic-redux-tools
提供了方法routerFactory
返回最终渲染到页面上的 React 组件,第一个参数是路由设置,第二个参数是项目命名空间(字符串,作为路由的第一层路径,区分子项目),第三个参数是 redux 的聚合 reducer。routes 是非常单一的 react-router 路由定义文件:
client/routes.tsx
reducer也是基本的 redux 使用方法:
client/reducer.tsx
config 文件是定义文件,将静态定义内容存放于此:
client/config.tsx
action、reducer
存放在 stores 文件夹下. actions 可共用,但对于复杂项目,最好按照 state 树结构拆分文件夹,每个文件夹下对应
action.tsx
与reducer.tsx
。将 Redux 数据流与组件完全解耦。特别对于可能在后端发送的请求,可以使用
fit-isormophic-redux-tools
提供的fetch
方法:client/stores/user/action.tsx
然后在前端任何地方执行,它都只是一个普通的请求,如果这个 action 在后端被触发(比如被放置在 componentWillMount生命周期中),还记得 service 中这段代码吗?
会直接调用此方法。第一个参数是 params(get) 与 data(post) 数据的 merge,第二个参数是 req,如果在后端执行此方法,则这个 req 是获取页面模板时的。
组件
使用
connect
将 redux 的 state 注入到组件的 props,还不熟悉的同学可以搜一搜 react-redux 教程。组件的介绍为什么这么简单?因为有了
fit-isormophic-redux-tools
插件的帮助,组件中抹平了同构请求的差异。再次强调一遍,在任何地方调用 action ,如果这段逻辑在后端被触发,它会自动向 service 取数据。fit-isomorphic-redux-tools 剖析
核心函数
serverRender
代码片段renderFullPage
方法,返回页面模板,可接收参数将后端渲染的内容填入其中,如果不开启后端渲染,无参调用此方法即可。为了抹平前端请求在后端处理的差异,需要触发两次
renderToString
方法,上述代码是第一次。因为fetch
方法在前后端都会调用,我们将serverRequestHelper.Request
传入其中,当 action 在后端执行时,不会返回数据,而是将此 action 存放在Map
对象中,渲染完毕后再将 action 提取出来单独执行:因为 react 渲染是同步的(vue2.0 对此做了改进,可谓抓住了 react 的痛点),对异步操作无法处理,因此需要多渲染一次。这时,redux 的 store 中已经有了动态请求所需的数据,我们只需要再次渲染,就可以获取所有完整数据了:
核心函数
promise-moddleware
代码片段篇幅原因,默认大家了解 redux 中间件的工作原理。这里有个约定,action 所有异步请求都放在 promise 字段上,dispatch 分为三个状态 (_REQUEST,_SUCCESS,_FAILURE)。前端请求都是异步的,因此使用
promise.then
统一处理,后端请求因为直接访问 model ,异步时,与前端同样处理,同步时,直接调用 promise 函数获取结果。还记得server/service/index.ts
文件中为何能支持普通方法,与async
方法吗?因为这里分开处理了。核心函数
service
代码片段这里有两个函数,将 service 层抽象出来。
routerDecorator
装饰器用于定义函数的路由信息,initService
将 service 信息初始化到路由中,如果是GET
请求,将 query 参数注入到 service 中,其它请求会对 query 与 body 参数做 merge 后再传给 service。总结
React 组件生态降低了团队维护成本,提高开发效率,同时督促我们开发时模块解耦,配合 redux 将数据层与模版层分离,拓展了仅支持 view 层的 React。后端渲染大大提高了首屏效率,大家可以自己规划后端渲染架构,也可以直接使用
fit-isormophic-redux-tools
。目前来看,React 后端渲染的短板在于
RenderToString
是同步的,必须依赖两次渲染才能方便获取异步数据(也可以放在静态变量中实现一次渲染),对于两层以上的异步依赖关系处理起来更加复杂,这需要 React 自身后续继续优化。当然,任何技术都是为了满足项目需求为前提,简单的异步数据获取已经可以满足大部分业务需求。webpack只是个打包工具,我们不要过分放大它的优势,一个成熟的业务线需要 gulp 或者 fis3 这种重量级构建工具完成一系列的流程,如今 fis3 已经支持 npm 生态,正在不断改造与进步。对 express 熟悉的同学,转到企业开发时不妨考虑一下 yog2,提供了一套完整的企业开发流程。
如有遗误,感谢指正。
线上项目
http://tieba.baidu.com/n/tbhighh5/album/456900832689737361 ,开启后端渲染 http://tieba.baidu.com/n/tbhighh5/album/456900832689737361?nsr=1 ,关闭后端渲染