nodejs抓取动态网页(基础概念SSR:即服务端渲染(ServerSide)(图))
优采云 发布时间: 2022-03-17 06:02nodejs抓取动态网页(基础概念SSR:即服务端渲染(ServerSide)(图))
基本概念
SSR:服务器端渲染(Server Side Render) 传统的服务器端渲染可以使用Java、php等开发语言来实现。随着Node.js及相关前端技术的不断进步,前端同学也可以使用这个完整的独立服务端渲染。
流程:浏览器发送请求->服务器运行react代码生成页面->服务器返回页面->浏览器下载HTML文档->页面就绪,即:当前页面由服务器生成并发送给浏览器。
对应CSR:客户端渲染过程:浏览器发送请求->服务器返回空白HTML(HTML收录根节点和js文件)->浏览器下载js文件->浏览器运行React代码->页面就绪:当前页面内容由js渲染
如何区分页面是否为服务端渲染:右键->显示网页源代码。如果页面内容在 HTML 文档中,则为服务器端渲染,否则为客户端渲染。
比较
为什么要使用服务器端渲染
首屏加载时间优化,因为SSR直接返回生成内容的HTML,而普通CSR先返回空白HTML,然后浏览器动态加载JavaScript脚本并在页面有内容之前渲染;所以SSR首屏加载更快,减少白屏时间,用户体验更好。
SEO(Search Engine Optimization),搜索关键词时的排名,对于大多数搜索引擎来说,不识别JavaScript内容,只识别HTML内容。 (注:原则上最好不要使用服务端渲染,所以如果只有SEO需求,可以使用预渲染等技术代替)
构建服务器渲染项目
(1)使用Node.js作为服务端和客户端的中间层,承担代理代理,处理cookies等操作。
(2) hydra的使用:在服务端渲染的情况下,使用hydra代替render。它的作用是把相关的事件注入到HTML页面中(即:让React组件的数据跟随HTML 文档一起传递给浏览器页面),可以保持服务器端数据与浏览器端一致,避免闪屏,让首次加载体验更加高效流畅。
ReactDom.hydrate(, document.getElementById('root'));
(3)服务端代码webpack编译:一般会构建一个webpack.server.js文件。除了常规的参数配置外,target参数需要设置为'node'。
const serverConfig = {
target: 'node',
entry: './src/server/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '../dist')
},
externals: [nodeExternals()],
module: {
rules: [{
test: /\.js?$/,
loader: 'babel-loader',
exclude: [
path.join(__dirname, './node_modules')
]
}
...
]
}
(此处省略样式打包,代码压缩,运行坏境配置等等...)
...
};
(4) 使用 react-dom/server 下的 renderToString 方法,将各种复杂的组件和代码在服务器上转换成 HTML 字符串返回给浏览器,并在初始请求上发送标签以加快页面加载速度并允许搜索引擎抓取页面以进行 SEO。
const render = (store, routes, req, context) => {
const content = renderToString((
{renderRoutes(routes)}
));
return `
ssr
${content}
`;
}
app.get('*', function (req, res) {
...
const html = render(store, routes, req, context);
res.send(html);
});
renderToString 的类似函数: i. renderToStaticMarkup:不同的是,renderToStaticMarkup 渲染的是纯 HTML,没有 data-reactid。 JavaScript 加载完成后,由于无法识别之前服务器渲染的内容,重新渲染(可能页面会闪退)。
二。 renderToNodeStream:将 React 元素渲染为其初始 HTML,返回一个可读的输出 HTML 字符串流。
三。 renderToStaticNodeStream:类似于renderToNodeStream,除了这不会创建React内部使用的额外DOM属性,例如data-reactroot。
(5) 使用redux来承担数据准备和状态维护的职责,通常用react-redux、redux-thunk(中间件:发送异步请求到action)。(这个猿目前用的比较多的是Redux和 Mobx,这里以 Redux 为例)。 A. 创建一个 store(服务端需要为每个请求创建一次,客户端只创建一次):
const reducer = combineReducers({
home: homeReducer,
page1: page1Reducer,
page2: page2Reducer
});
export const getStore = (req) => {
return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
}
export const getClientStore = () => {
return createStore(reducer, window.STATE_FROM_SERVER, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}
B. action:负责将数据从应用程序传输到商店,是商店数据的唯一来源
export const getData = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('interfaceUrl/xxx')
.then((res) => {
dispatch({
type: 'HOME_LIST',
list: res.list
})
});
}
}
C. reducer:接收旧的状态和动作,返回新的状态,响应动作并发送给store。
export default (state = { list: [] }, action) => {
switch(action.type) {
case 'HOME_LIST':
return {
...state,
list: action.list
}
default:
return state;
}
}
export default (state = { list: [] }, action) => {
switch(action.type) {
case 'HOME_LIST':
return {
...state,
list: action.list
}
default:
return state;
}
}
D. Provider 使用 react-redux 的 connect 将组件连接到 store
Provider 将之前创建的 store 作为 prop 传递给 Provider
const content = renderToString((
{renderRoutes(routes)}
));
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) 接收四个参数。前两个属性是常用的。 mapStateToProps 函数允许我们将 store 中的数据作为 props 绑定到组件。 mapDispatchToProps 将 Actions 作为 props 绑定到组件
connect(mapStateToProps(),mapDispatchToProps())(MyComponent)
(6) 使用react-router承担路由职责。服务端路由和客户端路由不同。它是无状态的。React提供了一个无状态组件StaticRouter,它将当前URL传递给StaticRouter和调用 ReactDOMServer.renderToString() 来匹配路由视图。
服务器
import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'
{renderRoutes(routes)}
浏览器端
import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'
{renderRoutes(routes)}
当浏览器的地址栏发生变化时,前端会匹配路由视图,而由于req.path发生变化,服务器会匹配路由视图,保持了前后端路由的一致性意见。页面刷新后,当前视图仍能正常显示。如果只有浏览器端路由,使用BrowserRouter,当页面地址改变后刷新页面时,会因为没有对应的html而找不到页面。返回一个完整的html给客户端,页面依旧正常显示。推荐使用react-router-config插件,然后在StaticRouter和BrowserRouter标签的子元素中添加renderRoutes(routes)如上:创建router.js文件
const routes = [{ component: Root,
routes: [
{ path: '/',
exact: true,
component: Home,
loadData: Home.loadData
},
{ path: '/child/:id',
component: Child,
loadData: Child.loadData
routes: [
path: '/child/:id/grand-child',
component: GrandChild,
loadData: GrandChild.loadData
]
}
]
}];
当浏览器请求一个地址时,server.js可以在实际渲染之前通过matchRouters判断要渲染的内容,调用loaderData函数进行action dispatch,返回promise->promiseAll->renderToString,最后生成HTML文档返回。
import { matchRoutes } from 'react-router-config'
const loadBranchData = (location) => {
const branch = matchRoutes(routes, location.pathname)
const promises = branch.map(({ route, match }) => {
return route.loadData
? route.loadData(match)
: Promise.resolve(null)
})
return Promise.all(promises)
}
(7)写组件的时候注意代码同构(即:一组React代码在服务端执行一次,在客户端执行一次)由于服务端绑定事件无效,服务端只返回Page样式(&灌水数据),同时返回JavaScript文件,该事件只有在浏览器下载执行JavaScript时才能绑定,我们希望这个过程只需要写代码一次,此时会用到同构和服务,客户端渲染样式,客户端执行时绑定事件。
优点:共享前端代码,节省开发时间缺点:由于服务器端和浏览器环境的不同,会出现一些问题,比如找不到document等对象,DOM计算错误,不一致前端渲染和服务端渲染之间的内容等;前端可以做非常复杂的请求合并和延迟处理,但是对于同构来说,所有这些请求只有在提前得到结果时才会渲染。
以上就是本文的全部内容。希望对大家的学习有所帮助,也希望大家多多支持Script Home。