原文发布于咱们前端团队的公众号,建议读者进入公众号看原文,CSDN的排版和配色太难受了,作为一个前端不能忍

 

一、为什么要进行服务端渲染

 

随着前端技术栈和工具链的迭代成熟,前端工程化、模块化的趋势也愈发明显,在这波前端技术浪潮中,涌现了诸如React、Vue、Angular等基于客户端渲染的前端框架,这类框架所构建的单页应用(SPA)具有渲染性能好、可维护性高等优点。但也同时带来了两个缺陷:

       1.屏加载时间过长

       2.不利于SEO

      与传统web项目直接获取服务器端渲染好的HTML不同,单页应用使用JavaScript在客户端生成HTML来呈现内容,用户需要等待JS解析执行完成才能看到页面,这就使得首屏加载时间变长,影响用户体验。此外当搜索引擎爬取网站HTML文件时,单页应用的HTML没有内容,从而影响搜索排名。为了解决这两个缺陷,业界借鉴传统的服务器端直出HTML方案,提出在服务器端执行前端框架(React/Vue/Angular)代码生成HTML,然后将渲染好的HTML返回给客户端,实现CSR前端框架的服务器端渲染。

本文通过一个简单的demo,向读者讲解React服务器端渲染(SSR)的基本原理,在阅读完本文后,读者应该能够掌握:

  1. 服务器端渲染的基本概念和原理

  2. 在SSR项目中渲染组件

  3. 在SSR项目中使用路由

  4. 在SSR项目中使用redux

 

二、在SSR项目中渲染组件

1.使用node进行服务端渲染

我们使用express启动一个Node服务器来进行基本的服务端渲染。首先安装初始化node项目和安装express

npm init

npm install express –save

在根目录中创建文件app.js,监听3000端口的请求,当请求根目录时,返回一些HTML

const express = require('express')
const app = express()
app.get('/', (req,res) => res.send(`
<html><head><title>ssr demo</title></head><body>Hello world</body>
</html>
`))app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))

进入项目根目录,运行node app.js启动项目。

 

鼠标右键查看网页源代码,这就是服务器端直接返回的HTML,我们已经完成了一个基本的服务端渲染。如果我们打开一个react项目并查看网页源代码,会发现代码中并没有页面内容对应的HTML,这是因为react所构建的SPA单页应用是通过在客户端执行JS动态地生成HTML,初始的HTML文件中并没有对应的内容。

 

2.在服务器端编写React代码

我们已经启动了一个Node服务器,下一步我们需要在服务器上编写React代码,我们创建一段这样的React代码并在app.js进行引用

import React from 'react'const Home = () =>{return <div>home</div>}export default Home

然而这段代码并不会运行成功,因为直接在服务器端运行React代码是行不通的,原因有以下几个:

  1. Node不能识别import和export,这二者属于esModule的语法,而Node遵循common.js规范

  2. Node不能识别JSX语法,我们需要使用webpack对项目进行打包转换,使之成为Node能识别的语法

为了使代码能够运行,我们需要安装webpack并进行配置

  1. npm install webpack webpack-cli –save 安装webpack和webpack-cli

  2. 根目录下创建配置文件webpack.server.js并进行相关配置

const path = require('path')    //node的path模块
const nodeExternals = require('webpack-node-externals')module.exports = {target:'node',mode:'development',           //开发模式entry:'./app.js',             //入口output: {                     //打包出口filename:'bundle.js',     //打包后的文件名path:path.resolve(__dirname,'build')    //存放到根目录的build文件夹},externals: [nodeExternals()],  //保持node中require的引用方式module: {rules: [{                  //打包规则test:   /\.js?$/,       //对所有js文件进行打包loader:'babel-loader',  //使用babel-loader进行打包exclude: /node_modules/,//不打包node_modules中的js文件options: {presets: ['react','stage-0',['env', { //loader时额外的打包规则,对react,JSX,ES6进行转换targets: {browsers: ['last 2versions']   //对主流浏览器最近两个版本进行兼容}}]]}}]}
}

3.安装对应的babel

npm install babel-loaderbabel-core –save

npm install babel-preset-react –save

npm install babel-preset-stage-0 –save

npm install babel-preset-env –save

npm install webpack-node-externals –save

 

4.运行webpack --config webpack.server.js

5.启动打包后的文件node  ./build/bundle.js

 

关于webpack的使用,比较陌生的读者可以参考我们公众号这篇:《webpack入门》

https://mp.weixin.qq.com/s/qtw3nKLyo-fSrs8cZ9_zHw

 

3.使用renderToString渲染组件

经过webpack对JSX和ES6进行打包转化后,我们还是无法正确运行我们的代码,以前在客户端渲染DOM时,我们使用下面的代码,但这段代码无法在服务端运行。

import Home from './src/containers/Home'import ReactDom from 'react-dom'ReactDom.render(<Home/>, document.getElementById('root')) //服务端没有DOM

我们需要使用react-dom提供的renderToString方法,将组件渲染成为字符串再插入返回给客户端的HTML中

import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import Home from'./src/containers/Home'const app= express()
const content = renderToString(<Home/>)
app.get('/',(req,res) => res.send(`
<html><head><title>ssr demo</title></head><body>${content}</body>
</html>
`))app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

重新打包并重启服务器,我们就能在页面上看到服务器端渲染的组件

 

4.webpack自动打包和服务端自动重启

      写到这里我们对之前的Node和webpack的启动方式做一个小优化,在这之前,我们每次对项目的改动,都需要重新执行webpack--config webpack.server.js和node ./build/bundle.js来重启项目,现在我们对package.json文件中的script做一些改动,使得服务器能够自动重启和打包

  1. 在webpack --config webpack.server.js后加上—watch就能实现webpack的自动监听打包,当需要被打包的文件发生变化时,webpack就会自动重新打包

  2. 安装nodemon,nodemon是nodemonitor的缩写,nodemon能够帮我们监听文件的变化并自动重启服务器,我们需要运行 npm install nodemon –g安装nodemon,在package.json的script配置项中添加这两句:

 "scripts":{"dev": "nodemon--watch build --exec node \"./build/bundle.js\"","build": "webpack--config webpack.server.js --watch"},

在进行了以上两条配置后,我们开启两个终端,分别运行npm run dev和npm run build就能完成项目的自动打包和服务器重启

    3.安装npm-run-all进一步简化流程:

运行npm install npm-run-all –g安装npm-run-all,并对package.json进行配置

"scripts": {"dev": "npm-run-all--parallel dev:**","dev:start": "nodemon--watch build --exec node \"./build/bundle.js\"","dev:build": "webpack--config webpack.server.js --watch"},

我们在原来的start和build加上dev前缀,表示这是开发环境所使用的命令,在线上环境时我们并不需要执行这两条命令去监听。配置好以后,运行npm run dev,我们就完成了自动打包和服务端启动重启,每次对代码的更改只需要刷新页面就能看到效果,不必像原来那样手动重新打包和重启服务器

 

5.同构的概念

我们在上面的过程中,已经将组件渲染到了页面上,下面我们为组件绑定一个点击事件。

import React from 'react'const Home= () =>{return (<div><div>home</div><button onClick={()=>{alert('click')}}>click</button></div>)
}export default Home

运行代码,刷新页面,我们会发现并没有执行对应的点击事件,这是由于renderToString只渲染了组件的内容,而不会绑定事件,为了能够给页面上的组件绑定事件,我们需要将React代码在服务端执行一遍,在客户端再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为同构。

我们通过<script>标签为页面引入客户端执行的React代码,并通过express的static中间件为js文件配置路由,修改原来的app.js

import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from'react-dom/server'//引入renderToString方法
import Home from './src/containers/Home'const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹const content = renderToString(<Home/>)app.get('/',(req,res)=>res.send(`
<html><head><title>ssr demo</title></head><body>${content}<script src="/index.js"></script></body>
</html>
`))app.listen(3001, () =>console.log('Example app listening on port 3001!'))

然后我们需要编写我们的index.js(客户端的React代码),我们尝试在public文件夹下创建index.js并编写React代码,但这些React代码将无法运行,因为我们同样需要使用webpack对客户端的React进行打包。

 

5.在客户端执行React代码

我们先调整一下目录结构,在src文件夹下新建client文件夹用来存放客户端代码,根目录下新建webpack.client.js作为客户端React代码的webpack配置文件,public文件夹将用来存放webpack打包后的客户端代码;新建server文件夹用来存放服务器端代码,将原来app.js的内容移至server文件夹下的index.js,并修改webpack.server.js的入口。新建containers文件夹存放React代码

下面我们开始编写客户端webpack配置项,在webpack.client.js中编写以下代码:


const path = require('path')                    //node的path模块module.exports = {mode:'development',                         //开发模式entry:'./src/client/index.js',              //入口output: {                                   //打包出口filename:'index.js',                    //打包后的文件名path:path.resolve(__dirname,'public')   //存放到根目录的build文件夹},module: {rules: [{                               //打包规则test:   /\.js?$/,                    //对所有js文件进行打包loader:'babel-loader',               //使用babel-loader进行打包exclude: /node_modules/,             //不打包node_modules中的js文件options: {presets: ['react','stage-0',['env', {     //loader时额外的打包规则,这里对react,JSX进行转换targets: {browsers: ['last 2versions']   //对主流浏览器最近两个版本进行兼容}}]]}}]}
}

同时我们对package.json中的script部分进行修改

 "scripts": {"dev": "npm-run-all--parallel dev:**","dev:start": "nodemon--watch build --exec node \"./build/bundle.js\"","dev:build:server": "webpack--config webpack.server.js --watch","dev:build:client": "webpack--config webpack.client.js --watch"},

重新运行npm run dev,我们就完成了服务端、客户端代码的自动打包,刷新页面,可以看到事件已经成功绑定

这里报了个警告,原因是在React 16中进行服务端渲染时,应该将render()方法替换为hydrate()方法,虽然在React16中仍然能够使用render()渲染HTML,但为了消除错误,最好替换成hydrate()

有关hydrate的更多内容,可以看这个讨论:https://www.wengbi.com/thread_50584_1.html

 

6.webpack优化整理

我们在项目中编写了两个webpack配置文件,其实在这两个配置文件当中存在很多共同的部分,我们应该将共同的部分提取出来,减少代码的冗余性。我们安装webpack-merge模块来帮助我们提取公用的webpack配置项。

  1. 新建webpack.base.js文件,将webpack.server.js和webpack.client.js中共同的配置项移到这里并通过module.exports进行导出

module.exports = {module: {rules: [{test:   /\.js?$/,loader:'babel-loader',exclude: /node_modules/,options: {presets: ['react','stage-0',['env', {targets: {browsers: ['last 2versions']}}]]}}]}
}

2.在webpack.server.js和webpack.client.js中通过merge方法将公用配置项和当前配置项进行合并导出。

//webpack.client.js配置
const path = require('path')
const merge = require('webpack-merge')
const config = require('./webpack.base.js')const clientConfig = {mode:'development',entry:'./src/client/index.js',output: {filename:'index.js',path:path.resolve(__dirname,'public')},
}module.exports = merge(config,clientConfig)

 

//webpack.server.js配置
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const merge = require('webpack-merge')
const config = require('./webpack.base.js')const serverConfig = {target:'node',mode:'development',entry:'./app.js',output: {filename:'bundle.js',path:path.resolve(__dirname,'build')},externals: [nodeExternals()],
}module.exports = merge(config,serverConfig)

段落小结:

本小节介绍了如何在服务端进行基础的组件的渲染和事件绑定,通过本小节的讲解,读者应该能够体会到React SSR的基本思路——同构,所谓同构,就是一套React代码在服务器端执行生成HTML,客户端再执行代码接管页面的操作,从而使得页面兼具SSR和CSR的优点。

总结一下服务端渲染组件的步骤:

  1.  建立一个node项目

  2. 编写服务器端的React代码并使用webpack进行打包编译,使用renderToString方法将组件渲染成为HTML。

  3. 编写客户端需要执行的React代码,并使用webpack进行打包编译,通过script标签引入页面,接管页面的操作。

 

三、在SSR项目中使用路由

1.在客户端使用路由

同样的,在使用路由时,我们需要在服务器端和客户端各配置一遍路由,原因会在下文中解释。我们首先进行客户端的路由配置,安装react-router。

npm install react-router-dom —save

 

然后我们在src文件夹下创建Router.js存放路由条目

import React from 'react'                   //引入React以支持JSX
import { Route } from 'react-router-dom'    //引入路由
import Home from './containers/Home'        //引入Home组件export default (<div><Route path="/" exact component={Home}></Route></div>
)

修改client文件夹下的index.js,使用BrowserRouter并引入路由条目

import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from'react-router-dom'
import Router from'../Routers'const App= () => {return (<BrowserRouter>{Router}</BrowserRouter>)
}ReactDom.hydrate(<App/>, document.getElementById('root'))

运行代码,刷新页面,会发现控制台报错:

这是由于我们在Router.js使用路由时,外层需要套一个div,然而服务器端的HTML外层并没有这个div,导致了客户端渲染的页面和服务端渲染的页面内容不同,因而报错,所以我们需要在服务器端再配置一次路由,使得服务器端和客户端渲染的内容一致(当然,如果直接在服务器返回的HTML里加多一个div是可以暂时解决这个报错的,但在服务器端不写路由的话,在接下来的步骤中还会遇到其他错误)

 

2.在服务器端使用路由

修改server文件夹下的index.js,在这里引入服务器端路由。在服务器端我们需要使用StaticRouter来替代BrowserRouter,StaticRouter 是 React-Router 针对服务器端渲染专门提供的一个路由组件,由于StaticRouter不能像BrowserRouter一样感知页面当前页面的url,所以我们需要给StaticRouter传入location={当前页面url},另外使用  StaticRouter时必须传递一个context参数,用于服务端渲染时的参数传递。

import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹app.get('/',(req,res)=>{const content  = renderToString((//在服务端我们需要使用StaticRouter来替代BrowserRouter//传入当前path//context为必填参数,用于服务端渲染参数传递<StaticRouter location={req.path} context={{}}>{Router}</StaticRouter>))res.send(`<html><head><title>ssr demo</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`)
})app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

这时再打开我们的页面,就不会出现错误了。

 

3.通过link实现多页面跳转

我们创建一个Login组件

并在Routers.js中为login组件添加路由

import React from'react'                   //引入React以支持JSX
import { Route } from'react-router-dom'    //引入路由
import Home from'./containers/Home'        //引入Home组件
import Login from'./containers/Login'      //引入Login组件exportdefault (<div><Route path="/" exact component={Home}></Route><Route path="/login" exact component={Login}></Route></div>
)

另外我们需要将src/server/index.js中的路由从匹配‘/’改成‘*’,否则当我们访问http://localhost:3001/login时将由于匹配不到路由而提示404错误。

import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'const app= express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹app.get('*',(req,res)=>{const content  = renderToString((<StaticRouter location={req.path} context={{}}>{Router}</StaticRouter>))res.send(`<html><head><title>ssr demo</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`)
})app.listen(3001, () =>console.log('Exampleapp listening on port 3001!'))

我们可以稍微抽离以上代码中生成HTML的部分,server文件夹下新建utils.js文件,存放生成HTML的代码

import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'export const render = (req) => {const content = renderToString((<StaticRouter location={req.path} context={{}}>{Router}</StaticRouter>));return`<html><head><title>ssr demo</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`
}

原来的server/index.js可以改成以下形式

import express from 'express'
import { render } from './utils'const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹app.get('*',(req,res)=>{res.send(render(req))
})app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))

在进行了以上的步骤后,我们使用Link标签来实现一个导航功能,我们需要创建一个导航栏组件并在home和login中引用这个导航栏组件,对于可以复用的组件,我们在src文件夹下创建component文件夹存放公共组件,并在component下创建header.js作为我们的导航栏组件

import React from 'react'
import { Link } from 'react-router-dom'const Header = () => {return (<div><Link to='/'>Home </Link><Link to='/login'>Login</Link></div>)
}export default Header

然后我们分别在Home组件和Login组件中引用这个导航栏组件,保存代码,刷新页面,现在已经能够在页面上进行路由跳转了。

值得注意的是,只有在第一次进入页面时,浏览器请求了页面文件,之后切换路由的操作都不会重新请求页面,因为这时页面的路由跳转已经是客户端React的路由跳转了。

 

段落小结:

本小节介绍了如何在SSR项目中使用路由,我们需要在服务器端和客户端各配置路由才能正常实现页面跳转,对于配置两次路由的原因,笔者的理解是

1.服务端路由是为了第一次进入页面时能够找到对应的网页文件

2.客户端路由是为了能让React路由接管页面实现无刷新跳转

3.如果服务器端不写路由的话,会导致页面内容不统一而出现报错

此外我们需要注意到,只有在第一次进入页面的时候,浏览器才会使用服务器端路由请求网页文件,当页面渲染后,React的客户端路由将接管页面路由,实现无刷新跳转。

 

四、在SSR项目中使用redux

本小节将讲解如何在SSR项目中使用redux,这是项目中的一个难点,同样的我们需要在客户端和服务器端各执行一次redux的代码,原因会在下文中解释。

1.安装redux以及redux中间件

npm install redux –save

npm install react-redux–save

npm install redux-thunk–save

2.在客户端使用redux

接下来我们进行一系列常规操作,这里不再细讲redux、redux-thunk、react-redux的使用。我们在客户端代码(/client/index.js)里使用redux创建store和reducer,配置中间件thunk,并将store传递给组件。

import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import Routers from '../Routers'
import { createStore,applyMiddleware } from 'react'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'const reducer = (state,action) => {return state
}const store = createStore(reducer,applyMiddleware(thunk))const App = () => {return (<Provider store={store}><BrowserRouter>{Routers}</BrowserRouter></Provider>)
}ReactDom.hydrate(<App/>,document.getElementById('root'))

在子组件(Home)中我们使用react-redux中的connect方法与store进行连接

import React from 'react'
import Header from '../../component/header'
import { connect } from 'react-redux'const Home= () =>{return (<div><Header/><div>{props.name}</div><button onClick={()=>{alert('click')}}>click</button></div>)
}const mapStateToProps = state => ({name:state.name
})export default connect(mapStateToProps,null)(Home)

在写完客户端的redux代码后,我们可以刷新页面看看效果

可以看到页面上会报错,这是由于在访问http://localhost:3001/时,首先会进入server文件夹下的index.js,index.js会去渲染Home组件,当Home组件去调用store里的数据时,由于此时还没有执行客户端的redux代码,导致Home组件找不到store而报错,因此我们需要在服务器端代码(server/until.js)里也创建一次store,并通过react-redux传递给组件

 

3.    在服务器端使用redux

同样的,我们在服务器端的代码中也引入Redux

import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from  '../Routers'
import { createStore,applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'export const render = (req) => {const reducer = (state = { name:'CJW' },action) => {return state}const store= createStore(reducer,applyMiddleware(thunk))const content = renderToString((<Provider store={store}><StaticRouter location={req.path} context={{}}>{Router}</StaticRouter></Provider>));return`<html><head><title>ssr demo</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`
}

然而在服务端这么写store是有坑的,createStore创建的store是单例的store,在服务器端这样的写法将导致所有用户共享一个store,所以我们将创建store这一步封装成一个方法,每次调用都返回一个新的store。此外我们可以将这部分创建store的代码抽离出来,在server和client分别引用,减少代码的冗余。

我们在src目录下创建一个store文件夹,store文件夹下创建index.js存放创建store的代码

import { createStore,applyMiddleware } from'redux'
import thunk from 'redux-thunk'const reducer = (state = { name:'CJW' }, action) => {return state
}const getStore = () => {return createStore(reducer,applyMiddleware(thunk))
}export default getStore

在client/index.js和server/utils.js中都引入getStore方法,删除原来创建store的代码

import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import Routers from '../Routers'
import getStore from '../store'const App = () => {return (<Provider store={getStore()}><BrowserRouter>{Routers}</BrowserRouter></Provider>)
}ReactDom.hydrate(<App/>,document.getElementById('root'))

这里只是展示了一个简单的store创建,在实际使用中,我们需要创建一个规范的store,实现reducer、store和action的分离,但这里作为一个简单的demo就不进行这些操作了。

 

4.   异步请求数据

我们安装axios来方便我们的异步请求

npm install axios --save

由于已经安装了thunk,因此我们可以在action中发送异步请求,这一块也是thunk的基础内容,不做过多的讲解。修改Home文件夹下的index.js,代码如下(我这个axios请求的接口会返回一个列表,读者可以请求自己项目中的接口或请求各种公开的api)

(在这粘贴了半天都是灰色的。。。CSDN这个编辑器咋回事)

import React from 'react'
import Header from '../../component/header'
import { connect } from 'react-redux'
import axios from 'axios'class Home extends React.Component {//在componentDidMount中发送异步请求componentDidMount(){this.props.getList()}render(){console.log(this.props.list)return (<div><Header/>{  this.props.list?<div>{this.props.list.map(item=>(<div>{item.title}</div>))}</div>:''}<button onClick={()=>{alert('click')}}>click</button></div>)}
}//使用redux-thunk,在action中写axios并dispatch
const getData = () => {return (dispatch) => {//接收来自mapDispatchToProps的dispatch方法axios.get('http://异步请求的接口).then((res)=>{const list = res.data.datadispatch({type:'CHANGE_LIST',list:list})})}
}const mapStateToProps = state => ({name:state.name,list:state.list
})const mapDispatchToProps = dispatch => ({getList(){//调用dispatch时会自动执行getData里return的方法dispatch(getData())}
})export default connect(mapStateToProps , mapDispatchToProps)(Home)

折腾了半天,我们保存代码,刷新页面,可以看到页面上成功显示了异步请求的内容,然而我们右键检查网页源代码,却发现并没有对应内容的HTML,这是由于服务器端执行React代码时,并不会触发componentDidMount(),因此服务器端的store始终是空的

7.使用loadData填充数据

为了使得返回客户端的HTML包含异步请求的数据,实际上我们需要根据不同的页面,给当前的store填充数据,为了实现这个目的,我们需要满足以下两个条件:

1.服务器端代码在进入某个页面时能匹配到对应组件里的axios请求

2.被匹配的组件能将axios请求得到数据传递给服务器端的store

对于这个问题,React-Router已经为SSR提供了一些方法(参考官方文档:https://reacttraining.com/react-router/web/guides/server-rendering),我们需要进行以下几个步骤:

  1. 修改路由条目(Router.js)从导出组件改为导出数组

import Home from './containers/Home'        //引入Home组件
import Login from './containers/Login'      //引入Login组件export default [{path:'/',component:Home,         //渲染Home组件exact:true,             //严格匹配loadData:Home.loadData, //传入loadData方法key:'Home'              //用于后续循坏时提供key},{path:'/login',component:Login,        exact:true,            key:'login' }
]

2.  分别修改server和client文件夹下的util.js和index.js

import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter,Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'export const render = (req) => {const store= getStore()const content = renderToString((<Provider store={store}><StaticRouter location={req.path} context={{}}><div>{routers.map(router=> (<Route{...router}/>))}</div></StaticRouter></Provider>));return`<html><head><title>ssr demo</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`
}
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter,Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'const App = () => {return (<Provider store={getStore()}><BrowserRouter><div>{routers.map(router=> (<Route{...router}/>))}</div></BrowserRouter></Provider>)
}ReactDom.hydrate(<App/>,document.getElementById('root'))

3.在server/index.js引入matchPath方法匹配当前页面路由,并执行对应的loadData方法

import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter,Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'export const render = (req) => {const matchRoutes = []routers.some(route=> {matchPath(req.path, route) ? matchRoutes.push(route) : ''})console.log(matchRoutes)const store = getStore()const content = renderToString((<Provider store={store}><StaticRouter location={req.path} context={{}}><div>{routers.map(router=> (<Route{...router}/>))}</div></StaticRouter></Provider>));return`<html><head><title>ssr demo</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`
}

这三步需要结合着看,在这一步骤,我们的目的是进入某个组件时,执行组件的loadData方法(方法将在下文实现),loadData方法将获取axios请求得到的数据,并将数据传递给服务器端的store。

我们可以打印一下匹配到的路由项,路由项的内容就是我们在Router.js中配置的路由条目。

下面我们来进行loadData方法的实现

import React from 'react'
import Header from '../../component/header'
import { connect } from 'react-redux'
import axios from 'axios'class Home extends React.Component {//在componentDidMount中发送异步请求componentDidMount(){this.props.getList()}render(){console.log(this.props.list)return (<div><Header/>{  this.props.list?<div>{this.props.list.map(item=>(<div>{item.title}</div>))}</div>:''}<button onClick={()=>{alert('click')}}>click</button></div>)}
}Home.loadData = (store) => {store.dispatch(getData())
} //使用redux-thunk,在action中写axios并dispatch
const getData = () => {return (dispatch) => {//接收来自mapDispatchToProps的dispatch方法axios.get('接口地址').then((res)=>{const list = res.data.datadispatch({type:'CHANGE_LIST',list:list})})}
} const mapStateToProps = state => ({name:state.name,list:state.list
}) const mapDispatchToProps = dispatch => ({getList(){//调用dispatch时会自动执行getData里return的方法dispatch(getData())}
})export default connect(mapStateToProps , mapDispatchToProps)(Home)

 

在loadData方法中,我们直接接收服务器端的store,调用store的dispatch方法来更新store的数据,但是光是这么写,我们还是会得到空的内容。这是因为axios请求是异步操作,服务器端渲染已经先于请求执行了,这里我们借助promise来修正我们的执行顺序,由于axios本身是一个promise对象,我们可以return axios对象,在loadData方法中,将dispatch也return出去,这样我们就能在server/util.js中得到这个promise,并调用Promise.all方法,让所有异步操作执行完后再渲染HTML,修改server/util.js,完成我们的最后一步

 

import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter,Route,matchPath } from 'react-router-dom'
import { Provider } from 'react-redux'
import routers from '../Routers'
import getStore from '../store'export const render = (req,res) => {//将res传入以使用res.send()方法const store = getStore()const matchRoutes = []const promises = []routers.some(route=> {matchPath(req.path, route) ? matchRoutes.push(route) : ''})matchRoutes.forEach( item=> {promises.push(item.loadData(store))})Promise.all(promises).then(()=>{//可以console一下看到当前的store已经有数据console.log(store.getState())const content = renderToString((<Provider store={store}><StaticRouter location={req.path} context={{}}><div>{routers.map(router=> (<Route{...router}/>))}</div></StaticRouter></Provider>));res.send(`<html><head><title>ssrdemo</title></head><body><div id="root">${content}</div><script src="/index.js"></script></body></html>`)})
}

代码写到这,我们总算能够在服务器端正确地使用redux,在踩了无数坑之后,我们终于能够搭建一个基本的SSR项目了。有关服务器端渲染的讲解,到这里也就告一段落了

 

段落小结:

本小节介绍了如何在SSR项目中使用Redux并使用axios异步获取数据,和组件渲染、路由使用一样,我们需要在服务器端和客户端各使用一次Redux,其原因在于服务器端渲染只会走一遍生命周期,并且在第一次render后便会停止。异步数据请求放在componentDidMount函数中不会被触发,即使我们提前发起数据请求,由于是异步返回,得到的数据也无法再次触发render,所以返回给客户端的HTML依然没有异步请求的数据。对于这种情况,我们通过改造路由,使得每次进入对应页面时能够匹配对应的loadData方法,为服务器端的store注入数据,并将生成HTML的步骤放在异步请求完成之后。

 

五、结尾语

   通过本文的介绍,读者应该对React服务端渲染有了初步的认识,能够在服务器端进行基本的组件渲染、路由跳转和使用redux,对webpack和Node在React项目中所起的作用也有了更进一步的了解。对于SSR的取舍,业界存在很多讨论,虽然SSR能够解决传统SPA项目首屏加载时间过长和不利于SEO的缺陷,但SSR同样带来了服务器负担重,实施难度大等诸多问题。如果对首屏加载时间和SEO没有极致的追求,我们可以选择更轻便的方案,如利用webpack和react-router分割代码,减少首屏加载时间;利用prerender进行预渲染,优化项目搜索引擎排名,不一定需要对整个项目采用同构的方式进行服务器端渲染。也许目前为止同构SSR并不是一种完美的方案,但在广大前端技术人员对性能和体验永不止步的追求中,有关SSR的讨论和迭代还将长久地持续下去。

 

参考文章:

《前后端渲染和同构渲染》:https://blog.csdn.net/qizhiqq/article/details/70904799

《Vue-SSR指南》:https://ssr.vuejs.org/zh/#

《next.js 的服务端渲染机制》:https://www.jianshu.com/p/a3bce57e7349

以上几篇文章对SSR的原理和优缺点进行了深入地剖析,帮助读者认识SSR的应用情景和使用局限。具有很高的参考价值

 

 

文章前后写了近3个月,希望能给大家带来帮助呀,本文总结自Dell Lee老师的React服务端渲染课程,Dell老师真的讲的很好,深入浅出,各个细节也有顾及。笔者在课程的基础上进行概括总结,并对课程中一些模糊的细节进行考据和思考,文章经过了公司前端团队评审,如果文章能够对大家有所启发,那真是太好了,希望大家多多支持Dell Lee老师的课程,毕竟光读一篇文章并不能完整掌握SSR。也希望读者能给咱公司前端团队的公众号点个关注,会定时推送技术分析噢

 

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 服务端渲染基础(25)

    因大拉钩教育大前端一期学员已发布相同内容&#xff0c;所以这里只能为转载&#xff01; 这里也提一句&#xff1a;sb CSDN的原创审核机制&#xff01;&#xff01;&#xff01; 服务端渲染基础 概述 随着前端技术栈和工具链的迭代成熟&#xff0c;前端工程化、模块化也已成为…...

    2024/5/7 3:56:32
  2. 服务端渲染看这一篇就够了(Next.js)

    背景&#xff1a; 以前&#xff1a;没有服务端渲染的概念&#xff0c;大规模应用之前&#xff0c;用户请求网页&#xff0c;都是后端先调用数据库&#xff0c;获得数据之后&#xff0c;将数据和页面元素进行拼装&#xff0c;组合成完整的 html 页面&#xff0c;再直接返回给浏…...

    2024/4/21 4:38:14
  3. Vue 服务端渲染(SSR)

    一、什么是服务端渲染(SSR) Server Side Render简称SSR&#xff0c;服务端渲染。在ajax兴起之前&#xff0c;特别是在SPA&#xff08;单页面应用&#xff08;Single-Page Application&#xff09;&#xff09;技术流行之前&#xff0c;大部分的web应用都采用的是服务端渲染。即…...

    2024/4/28 1:34:27
  4. React 服务端渲染最佳解决方案

    最近在开发一个服务端渲染工具&#xff0c;通过一篇小文大致介绍下服务端渲染&#xff0c;和服务端渲染的方式方法。在此文后面有两中服务端渲染方式的构思&#xff0c;根据你对服务端渲染的利弊权衡&#xff0c;你会选择哪一种服务端渲染方式呢&#xff1f; 什么是服务器端渲染…...

    2024/5/7 20:38:52
  5. Angular 6 服务端渲染之 udao 终章

    先介绍下小朋友 udao&#xff0c;首先是一个开源项目&#xff0c;代码足够简单&#xff0c;其次是跟随 Angular 大小版本一起成长的项目&#xff0c;会定期更新所有依赖包以及兼容最新版本的写法 Github 地址也贴出来好多次了&#xff1a;github.com/OrangeXC/ud… 本来行文目的…...

    2024/4/21 4:38:12
  6. angular---路由传参数

    ...

    2024/4/21 4:38:10
  7. 关于路由传参与接收参数

    路由 跳转传参的方式有很多&#xff0c;下面我总结一下常用的传参方式及其相对应的接收参数方式。 第一&#xff0c;参数是以;分隔 http://localhost:8092/account/manage/issue;id161123434754052096 路由跳转方式&#xff1a; 1&#xff0c;routerLink的方式 <a [r…...

    2024/4/20 19:45:53
  8. Angular路由在标签里传参

    &#xff08;情景&#xff1a;点击news.component.html里的路由标签跳转到详情页(news-detail.component.html)&#xff0c;并把参数传过去&#xff09;1.在app-routing.module.ts里配置路由: ............此处省略 const routes: Routes [// 刚进来为空的话就跳转到home路由…...

    2024/4/20 12:37:55
  9. angularjs的路由传参数方式

    第一种: 参数会以?的形式出现在访问地址中?:userId会自动变为?userId0010001 $stateProvider.state(page.home, { url:"/home?:userId", templateUrl: "/html/homeView", controller: "homeController", title:"首页&quo…...

    2024/4/20 19:45:51
  10. Meteor 中文教程

    Meteor 简介 Meteor是跨时代的全栈Web框架&#xff0c;Github stars数已超过Rails&#xff0c;它能迅速的开发实时&#xff08;Real-Time&#xff09;和响应式&#xff08;Reactive&#xff09;的应用&#xff0c;并且可以在一套代码中支持web&#xff0c;iOS&#xff0c;Andro…...

    2024/4/20 19:45:49
  11. 为页面嵌入PDF文件—零基础自学网页制作

    在页面中嵌入PDF文件 今天我们来嵌入一个pdf文件。 其实使用<iframe>标签也可以导入pdf标签&#xff0c;就是把pdf文件路径赋予src属性就可以了。 示例代码如下&#xff1a; <body><iframe width"100%" height"900px" src"pdf/人工…...

    2024/4/21 4:38:10
  12. AngularJs 基础教程 —— 与php服务器

    本文为 H5EDU机构官方 HTML5培训教程&#xff0c;主要介绍&#xff1a; AngularJs 基础教程—— PHP 简介PHP 是服务器端脚本语言。 您应当具备的基础知识 在继续学习之前&#xff0c;您需要对以下知识有基本的了解&#xff1a; HTML CSS 如果您希望首先学习这些项目&#xf…...

    2024/4/25 2:35:58
  13. 记Angular与Django REST框架的一次合作(2):前端组件化——Angular

    注&#xff1a;这是这个系列的第二部分&#xff0c;主要集中在Angular的使用方面。之前使用过AngularJS&#xff08;Angular 1.x&#xff09;&#xff0c;混在Django的模板中使用&#xff0c;这些页面一般完全是结果展示页。在有Django表单输入的页面中&#xff0c;就很难将两者…...

    2024/4/27 8:45:28
  14. 入门ROS教程与视频汇总(kinetic)

    参考网址&#xff1a; 1 MooreRobots 2 Richard Wang 3 Shawn Chen 部分视频网址&#xff1a; http://v.youku.com/v_show/id_XMjUxMTc5MzE5Mg http://i.youku.com/i/UMTg1NDE4MDM2/videos 注意下面是机器翻译结果~MooreRobots博客关于博客文章【概述】获得通过模拟在ROS…...

    2024/5/8 18:35:54
  15. 一篇全面的zepplin教程

    https://www.jianshu.com/p/090f02005e07 1.Zeppelin是什么? Apache Zeppelin是一款基于Web的Notebook(类似于jupyter notebook)&#xff0c;支持交互式地数据分析。 Zeppelin可实现你所需要的&#xff1a; 数据采集数据发现数据分析数据可视化和协作 支持多种语言&#xff…...

    2024/4/21 4:38:05
  16. 【资料】228页的 《重学TS》PDF 终于来了,一份值得一读的 TS 学习资料

    「《重学TS v1.0》」 PDF 是今年阿宝哥发布的第三本电子书&#xff0c;前两本分别是《前端进阶篇》&#xff08;下载量 9500&#xff09; 和《了不起的 TS 和 Deno》&#xff08;下载量近 2060&#xff09;&#xff0c;这里衷心感谢大家对阿宝哥的认可与支持。在学习 TS 的过程…...

    2024/4/21 4:38:05
  17. html注释标签,完整PDF

    前言 JavaScript是面向 Web 的编程语言&#xff0c;获得了所有网页浏览器的支持&#xff0c;是目前使用最广泛的脚本编程语言之一&#xff0c;也是网页设计和 Web 应用必须掌握的基本工具。 JavaScript主要用途 嵌入动态文本与HTML页面对浏览器时间做出相应读写HTML元素在数…...

    2024/4/21 4:38:03
  18. webgoat全关教程手册

    Webgoat&Webwolf owaspbwa里面的两个服务 搭建 先要安装jdk、Webgoat和Webwolf Webgoat和Webwolf jdk1.8不支持了&#xff0c;需要安装jdk11 去git上下载Webgoat和Webwolf https://github.com/WebGoat/WebGoat/releases/tag/v8.0.0.M26 去oracle官网下载JDK https:/…...

    2024/5/8 15:08:42
  19. web前端入门到实战:HTML 转 PDF 图文报表实践

    导出 PDF 图文报表实践 方法一&#xff1a; jsPDF 使用 jsPDF 时&#xff0c;需要注意的是其默认单位为 mm&#xff0c;需要在 new jsPDF() 时传入配置 const doc new jsPDF({unit: px,format: a4 })这个方法废了。这个鬼东西多行文本和多个图片&#xff0c;简直要人命&…...

    2024/4/21 4:38:01
  20. JavaTutorialNetwork 中文系列教程 · 翻译完成

    原文&#xff1a;JavaTutorialNetwork 协议&#xff1a;CC BY-NC-SA 4.0 欢迎任何人参与和完善&#xff1a;一个人可以走的很快&#xff0c;但是一群人却可以走的更远。 在线阅读ApacheCN 学习资源 目录 JavaTutorialNetwork 中文系列教程Java 基础 Java 概述在 Ubuntu 上安装…...

    2024/4/21 4:38:01

最新文章

  1. 【北京迅为】《iTOP-3588开发板快速烧写手册》-第11章 救砖方法

    RK3588是一款低功耗、高性能的处理器&#xff0c;适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用&#xff0c;RK3588支持8K视频编解码&#xff0c;内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP&…...

    2024/5/8 23:06:19
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/5/7 10:36:02
  3. 前端 js 经典:字符编码详解

    前言&#xff1a;计算机只能识别二进制&#xff0c;开发语言中数据类型还有数字&#xff0c;字母&#xff0c;中文&#xff0c;特殊符号等&#xff0c;都需要转化成二进制编码才能让技术机识别。 一. 编码方式 ACSLL、Unicode、utf-8、URL 编码、base64 等。 1. ACSLL 对英语…...

    2024/5/4 10:30:35
  4. xv6项目开源—05

    xv6项目开源—05.md 理论&#xff1a; 1、设备驱动程序在两种环境中执行代码&#xff1a;上半部分在进程的内核线程中运行&#xff0c;下半部分在中断时执行。上半部分通过系统调用进行调用&#xff0c;如希望设备执行I/O操作的read和write。这段代码可能会要求硬件执行操作&…...

    2024/5/8 1:57:10
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/8 6:01:22
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/7 9:45:25
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/5/4 23:54:56
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/7 14:25:14
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/5/4 23:54:56
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/5/4 23:55:05
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/5/4 23:54:56
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/5/7 11:36:39
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/5/4 23:54:56
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/6 1:40:42
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/5/4 23:54:56
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/5/8 20:48:49
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/5/7 9:26:26
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/5/4 23:54:56
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/8 19:33:07
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/5 8:13:33
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/5/8 20:38:49
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/5/4 23:54:58
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/6 21:42:42
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/5/4 23:54:56
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57