一开始,我想学习服务器端渲染,首先想到的是next.js,一个成熟的方案。看了一两天,很有意思,很优雅,但是打包后原理不太清楚,感觉不能灵活嵌入老项目。所以看着各种数据,我想梳理同构的线索,一步步实现我的同构模板。相关代码见我的GitHub。感谢阅读!
待办事项列表
数据:如何保持前后应用状态一致。路由:服务器与客户端匹配方案代码:同构,哪里可以共享,哪里需要差异化的静态资源:如何将css/图片等ssr直出资源引入服务器:渲染路由页面时如何匹配css/chunks资源。打包方案:如何在服务器和浏览器上编写自己的webpack配置文件SEO:头文件头处理方案。
要运行一个普通的网页,需要生成dom。加载dom树后,js绑定相关的dom事件来监控页面的交互。服务器没有dom执行环境,所以所有的服务器渲染实际上都返回一个静态文本,其中填充了初始数据。在react中,除了用于生成dom的常用渲染方法之外,还提供了renderToString和renderToStaticMarkup方法来生成字符串。由于VitualDOM的存在,这些方法可以像前面的字符串模板一样组合生成普通的字符串,返回给客户端接管,然后绑定相关的事件。最新的React v16使用水合物和ssr,这使得客户端能够渲染服务器的Vitualdom并重用它。加载js后,客户端不会再刷一面,减少了开销,避免了浏览器再次刷DOM时的闪屏体验。react的组件像往常一样写成spa,前端和后端是共享的。唯一的区别是门户的呈现方法已经更改了名称,客户端将挂载dom。
//clinet . jsreachdom . hydrate(app/,document . getelementbyid(' app '))//server . jsconst html=reactidom . rendertostring(app/)同构后的网站操作流程图
从阿里前端偷了一张照片。乍一看,ssr和csr的区别在于2 3 4 5。spa模式简单粗暴地返回一个空白的html页面,然后在11年内加载数据来填充页面。在此之前,页面都是空白的。Ssr会根据路由信息提前获取路由页面的初始数据,返回页面时已经有了初步的内容,不空白,搜索引擎容易记录。
路线匹配
浏览器端的路由匹配应根据spa进行。跳过了.
服务器端路由需要注意,一个是后端服务路由(如koa-router)的匹配问题,另一个是react-router路由表匹配后对react应用的匹配问题。
服务器端路由可以通过/react前缀与其他api接口区分开来。这种路由匹配方式甚至可以让服务器端渲染支持ejs等老项目的模板渲染方式,可以在系统升级中实现逐步升级。
//app.js文件(后端门户)从“”导入react controller。/controllers/react-controller//API routing app . use(API controller . routes())//Ejs page routing app . use(Ejs controller . routes())//react page routing app . use(reactioncontroller . routes())//文件react-controller.js从' KOA-router ' const router=new router({ prefix : '/react ' })router . all('/'),async (CTX,Next)={ const html=wait render(CTX)CTX导入路由器。body=html})导出默认路由器react-专门为ssr设计的StaticRouter接口称为静态路由。诚然,与客户端不同,服务器对应于一个网络请求,而路由是当前请求的url,这是唯一且不可更改的。返回ssr直出页面后,页面交互会导致地址栏的改变。只要使用了react-router提供的方法,无论是hash还是history,都属于浏览器端react-router的工作,完美继承了spa的优点。只有当您在输入字段中点击输入时,才会启动新一轮的后台请求。
从“react-router-DOM”const app=()={ return(provider store={ store }静态路由器位置={ CTX . URL } context={ context } layout//静态路由器/provider)}导入{static router}以应用状态数据管理
在过去的服务器端渲染中,客户端网页下载后需要立即看到的数据被放在服务器上并提前准备,会延迟显示。ajax请求的数据的交互逻辑放在页面加载的js文件中。
换个反应,其实套路是一样的。但不同的是:
传统的字符串模板、组件模板是相互分离的,数据可以单独引入,然后组装成一个html。在react ssr中,页面只能通过defaultValue和defaultProps呈现一次,不能重新呈现。
不能把defaultValude写死,只能用道具的数据方案。在执行renderToString之前,提前准备好整个应用程序状态的所有数据。全局数据管理方案可以考虑redux和mobx。
需要准备初始渲染数据,因此需要准确获取哪些组件将在当前地址进行渲染。react-router-config和react-router是同源的,是支持静态路由表配置的工具,提供了matchRoutes方法来获取匹配的路由数组。
从“react-router-config”导入{ matchRoutes }可从“@ loaded/component”加载的导入=const Root=loaded((props)=import('。/pages/Root ')常量索引=可加载(()=导入('。/pages/Index ')const Home=可加载(()=import('。/pages/Home ')const routes=[{ path : '/',component: Root,routes: [ { path: '/index ',component: Index,},{ path: '/home ',component: Home,Syncdata()={ } routes :[]}]}]router . all('/',async (URL,next)={ const branch=match routes(routes,URL)}) component,最美的方式是许多使用@loadable/component(一种代码拆分方案)库的开发人员反复提到这个问题,作者也明确表示不支持。绝对不可能不支持懒加载。因此,我错误地编写了代码,并在所需的路由对象中定义了一个asyncData方法。
计算机网络服务器
//routes.js{ path: '/home ',component: Home,asyncData (store,query) { const city=(query || ' ')。split('=')[1]让promise=store . dispatch(fetchcity list and temperature(city | | undefined))让promise 2=store . dispatch(setRefetchFlag(false))返回Promise.all([promise,promise2])返回promise } }//render . jsimport { match routes } from ' react-router-config ' import create store from './store/redux/Index ' const store=createstore()const branch=match routes(routes,URL)const promises=branch . map({ route })={//遍历所有匹配的路由并预加载数据返回路由. asyncData?route.asyncData(存储,查询): promise。resolve(null)})//完成存储预加载数据的初始化。等待承诺。所有(承诺)//获取最新的商店常量预加载状态=商店。getState()const App=(props)={ return(Provider store={ store } static router位置={ CTX . URL } context={ context } Layout//static router/Provider)}//数据准备就绪后,render应用const html=render tostring(App/)//将预加载的数据挂载到window '下并返回。客户端拾取返回html head/head body div id=' app ' $ { html }/div脚本窗口。_ _ preloaded _ state _ _=$ {JSON。stringfy(预加载状态)};/脚本/正文/html客户端
为了保证两端应用数据的一致性,客户端应该用相同的数据初始化redux的存储一次,然后生成应用。如果两者的dom/数据不一致,导致浏览器接管时dom重新生成一次,控制台会在开发模式下输出错误消息,开发体验完美。随后的ajax数据在componentDidMount和events中执行,并自然地从服务器的逻辑中剥离。
//获取初始化数据常量预加载状态=窗口。_ _ preloaded _ state _ _ | |未定义的删除窗口。_ _ preloaded _ state _ _//客户端存储初始化const store=create store(preloaded state)const app=()={ return(提供程序存储={store}浏览器路由器布局//浏览器路由器/The Provider )}//loadableReady由@ load Abel/组件提供,接口客户端由loadableReady()调用。然后(()={ react Tom .水合物(app/,document . getelementbyid(' app '))}代码拆分模式下的服务器也必须有。这就提出了如何避免重复请求的问题。我们知道componentDidMount方法只执行一次。如果服务器请求的数据有标识符,我们可以根据标识符决定是否在客户端发起新的请求。应该注意的是,在判断完成后,标识符被重置。
从“react-redux”@ connect导入{ connect }(state=({ refetchFlag : state . weather . refetchFlag,quality : state . weather . Quality })、dispatch=({ fetchcity listand Quality :()=dispatch(setRefetchFlag(true)))))导出默认类Quality扩展了Component { componentdimount(){ const { location : { search },FetchCityList和Quality(City | | Undefined): SetRefetchFlag()} }打包方案
客户端打包
我想说的是“一切照旧”。因为浏览器还在运行spa。有关入门级的详细信息,请参见github。至于如何配置得赏心悦目,用起来得心应手,就按照项目要求展现你的神奇吧。
服务器打包
客户端和客户端之间的异同:
与以下相同:
Bable需要兼容不同版本的js语法
Webpack v4 /babel v7.闻起来很香
.留空
不同:
不同的导入文件和导出文件
这里可以把整个服务器入口app.js作为打包入口,也可以把react route的起点文件作为打包入口,配置输出是umd模块,然后app.js需要。以后者为例(好处是在项目升级时尽可能减少对原系统的影响,也方便排查问题和调试断点):
//webpack . server . jsconst webpackConfig={ entry : { server : }。/src/server/index.js' },output : { path : path . resolve(_ dirname,Build '),filename :' [name]。js ',library target : ' UMD ' } }//app . jsconst reatkoarouter=require('。/build/server’)。defaultapp . use(reatkoaurter . routes())CSS,图像资源
懒,还没开始研究,占个坑
当它是节点附带的模块时,要求避免被webpack打包
Const服务器配置={ 0.target:' node'}需要第三方模块时如何避免被打包
Const服务器配置={ 0.externals :[require(' web pack-node-externals ')()]生产环境代码不需要混淆和压缩
.留空
服务器直接输出时的资源集合
当服务器输出html时,需要定义css资源和js资源,让客户端接管并下载使用。如果没有追击,可以直接添加客户端的所有输出文件,暴力、安全、简单。但是上面提到的@可加载/组件库,在实现了路由组件的惰性加载/代码拆分功能后,还提供了全套服务,配合webpack工具和ssr工具,帮助我们收集资源。
//webpack。基地。jsconst WebPACKconfig={ plugins :[.new LoadablePlugin()]}//render。jsimport {组块提取器}从"@可加载/服务器" const App=()={ return(提供程序存储={store}静态路由器位置={ CTX。URL }上下文={ context } Layout//静态路由器/提供商)} const webStats=路径。resolve(_ dirname ,)./public/loaded-stats。JSON ',//该文件由工具插件自动生成)const webExtractor=新块提取器({ entrypoints 3360[' client '],//为入口文件名stats文件: webStats })const jsx=web extractor。collectchunks(App/)const html=renderToString(jsx)const script tags=web extractor。getscript tags()const LinkTags=web extractor。getlink tags()const style tags=web extractor。getstyle tags()const preloaded state=store。getstate()常量头盔=助手。renderstatic()返回` html head $ { helper。标题。tostring()} $ { helper。梅塔。tostring()} $ { helper。梅塔。tostring()}STORE=' love '窗户_ _ PRELOADED _ STATE _ _=$ { JSON。stringify(PRELOADED STATE)};/script $ { script tags }/body/html ` SEO信息
上面已经透露了。使用了一个反应头盔库。具体用法可查看官方仓库,信息可直接写在组件上,最后根据优先级提升到头头部。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。