您好,登錄后才能下訂單哦!
使用React怎么對(duì)服務(wù)端進(jìn)行渲染?很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來(lái)學(xué)習(xí)下,希望你能有所收獲。
如果是客戶端渲染,瀏覽器首先會(huì)向?yàn)g覽器發(fā)送請(qǐng)求,服務(wù)器返回頁(yè)面的html文件,然后html中再向服務(wù)器發(fā)送請(qǐng)求,服務(wù)器返回js文件,js文件在瀏覽器中執(zhí)行繪制出頁(yè)面結(jié)構(gòu)渲染到瀏覽器完成頁(yè)面渲染。
如果是服務(wù)器端渲染這個(gè)流程就不同了,瀏覽器發(fā)送請(qǐng)求,服務(wù)器端運(yùn)行React代碼生成頁(yè)面,然后服務(wù)器將生成好的頁(yè)面返回給瀏覽器,瀏覽器進(jìn)行渲染。這種情況下React代碼就是服務(wù)器的一部分而不是前端部分了。
這里我們進(jìn)行代碼的演示,首選需要npm init初始化項(xiàng)目,然后安裝react,express,webpack,webpack-cli,webpack-node-externals。
我們首先編寫(xiě)一個(gè)React的組件。 .src/components/Home/index.js, 因?yàn)槲覀冞@個(gè)js是在node環(huán)境執(zhí)行的所以我們要遵循CommonJS規(guī)范,使用require和module.exports進(jìn)行導(dǎo)入導(dǎo)出。
const React = require('react'); const Home = () => { return <div>home</div> } module.exports = { default: Home };
我們這里開(kāi)發(fā)的Home組件是不能直接在node中運(yùn)行的,需要借助webpack工具將jsx語(yǔ)法打包編譯成js語(yǔ)法,讓nodejs可以爭(zhēng)取的識(shí)別,我們需要?jiǎng)?chuàng)建一個(gè)webpack.server.js文件。
在服務(wù)器端使用webpack需要添加一個(gè)target為node的鍵值對(duì)。我們知道在服務(wù)器端如果使用path路徑是不需要打包到j(luò)s中的,如果在瀏覽器端使用了path是需要打包到j(luò)s中的,所以在服務(wù)器端和在瀏覽器端需要編譯出來(lái)的js是完全不同的。所以我們?cè)诖虬臅r(shí)候要告訴webpack打包的是服務(wù)器端的代碼還是瀏覽器端的代碼。
entry入口文件就是我們node的啟動(dòng)文件,這里我們寫(xiě)成./src/index.js,輸出的output文件名稱為bundle,目錄在跟目錄的build文件夾中。
const Path = require('path'); const NodeExternals = require('webpack-node-externals'); // 服務(wù)端運(yùn)行webpack需要運(yùn)行NodeExternals, 他的作用是將express這類node模塊不被打包到j(luò)s里。 module.exports = { target: 'node', mode: 'development', entry: './src/server/index.js', output: { filename: 'bundle.js', path: Path.resolve(__dirname, 'build') }, externals: [NodeExternals()], module: { rules: [ { test: /.js?$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['react', 'stage-0', ['env', { targets: { browsers: ['last 2 versions'] } }]] } } ] } }
npm install babel-loader babel-core babel-preset-react babel-preset-stage-0 babel-preset-env --save
接著我們這里基于express模塊來(lái)編寫(xiě)一個(gè)簡(jiǎn)單的服務(wù)。./src/server/index.js
var express = require('express'); var app = express(); const Home = require('../Components/Home'); app.get('*', function(req, res) { res.send(`<h2>hello</h2>`); }) var server = app.listen(3000);
運(yùn)行webpack使用webpack.server.js配置文件來(lái)執(zhí)行。
webpack --config webpack.server.js
打包之后在我們的目錄下會(huì)出現(xiàn)一個(gè)bundle.js,這個(gè)js就是我們打包生成的最終可以運(yùn)行的代碼。我們可以使用node運(yùn)行這個(gè)文件, 就啟動(dòng)了一個(gè)3000端口的服務(wù)器。我們?cè)L問(wèn)127.0.0.1:3000可以訪問(wèn)這個(gè)服務(wù),看到瀏覽器輸出Hello。
node ./build/bundile.js
上面的代碼我們運(yùn)行前會(huì)使用webpack進(jìn)行編譯,所以也就支持了ES Modules規(guī)范,不再?gòu)?qiáng)制使用CommonJS了。
src/components/Home/index.js
import React from 'react'; const Home = () => { return <div>home</div> } export default Home;
/src/server/index.js中我們可以使用Home組件,這里我們首先需要安裝react-dom,借助renderToString將Home組件轉(zhuǎn)換為標(biāo)簽字符串,當(dāng)然這里需要依賴React所以我們需要引入React。
import express from 'express'; import Home from '../Components/Home'; import React from 'react'; import { renderToString } from 'react-dom/server'; const app = express(); const content = renderToString(<Home />); app.get('*', function(req, res) { res.send(` <html> <body>${content}</body> </html> `); }) var server = app.listen(3000);
# 重新打包 webpack --config webpack.server.js # 運(yùn)行服務(wù) node ./build/bundile.js
這時(shí)候頁(yè)面就顯示出了我們React組件的代碼。
React的服務(wù)端渲染是建立在虛擬DOM上的服務(wù)器端渲染,而且服務(wù)端渲染會(huì)讓頁(yè)面的首屏渲染速度大大加快。不過(guò)服務(wù)端渲染也有弊端,客戶端渲染React代碼在瀏覽器端執(zhí)行,他消耗的是用戶瀏覽器端的性能,但是服務(wù)器端渲染消耗的是服務(wù)器端的性能,因?yàn)镽eact代碼在服務(wù)器上運(yùn)行。極大的消耗了服務(wù)器的性能,因?yàn)镽eact代碼是很消耗計(jì)算性能的。
如果你的項(xiàng)目完全沒(méi)有必要使用SEO優(yōu)化并且你的項(xiàng)目訪問(wèn)速度已經(jīng)很快了的情況下,建議還是不要使用SSR的技術(shù)了,因?yàn)樗某杀鹃_(kāi)銷還是比較大的。
上面我們的代碼每次修改之后都需要重新執(zhí)行webpack打包和啟動(dòng)服務(wù)器,這樣調(diào)試起來(lái)太過(guò)麻煩,為了解決這個(gè)問(wèn)題我們需要做一下webpack的自動(dòng)打包和node的重啟。我們?cè)趐ackage.json中加入build命令,并且通過(guò)--watch監(jiān)聽(tīng)文件變化進(jìn)行自動(dòng)打包。
{ ... "scripts": { "build": "webpack --config webpack.server.js --watch" } ... }
只是重新打包還不夠,我們還需要重啟node服務(wù)器,這里我們需要借助nodemon模塊,這里我們使用全局安裝nodemon, 在package.json文件中添加一個(gè)start命令來(lái)啟動(dòng)我們的node服務(wù)器。使用nodemon監(jiān)聽(tīng)build文件并且發(fā)生改變之后重新exec運(yùn)行"node ./build/bundile.js", 這里需要保留雙引號(hào),轉(zhuǎn)譯一下就好了。
{ ... "scripts": { "start": "nodemon --watch build --exec node \"./build/bundile.js\"", "build": "webpack --config webpack.server.js --watch" } ... }
這時(shí)我們啟動(dòng)服務(wù)器,這里需要在兩個(gè)窗口運(yùn)行下面的命令,因?yàn)閎uild后不允許再輸入其他命令了。
npm run build npm run start
這個(gè)時(shí)候我們修改代碼之后頁(yè)面就會(huì)自動(dòng)更新了。
但是上面的流程還是有些麻煩,我們需要兩個(gè)窗口來(lái)執(zhí)行命令,我們想要一個(gè)窗口將兩個(gè)命令執(zhí)行完畢,我們需要借助一個(gè)第三方模塊npm-run-all,可以全局安裝這個(gè)模塊。然后再package.json中來(lái)修改一下。
我們?cè)诖虬驼{(diào)試應(yīng)該是在開(kāi)發(fā)環(huán)境,我們創(chuàng)建一個(gè)dev命令, 里面執(zhí)行npm-run-all, --parallel表示并行執(zhí)行, 執(zhí)行dev:開(kāi)頭的所有命令。我們將start和build前面追加一個(gè)dev:,這個(gè)時(shí)候我想啟動(dòng)服務(wù)器同時(shí)監(jiān)聽(tīng)文件改變運(yùn)行npm run dev就可以了。
{ ... "scripts": { "dev": "npm-run-all --parallel dev:**", "dev:start": "nodemon --watch build --exec node \"./build/bundile.js\"", "dev:build": "webpack --config webpack.server.js --watch" } ... }
比如下面的代碼,我們給div綁定一個(gè)click事件,希望點(diǎn)擊的時(shí)候可以彈出click提示。但是運(yùn)行之后我們會(huì)發(fā)現(xiàn)這個(gè)事件并沒(méi)有被綁定上,因?yàn)榉?wù)器端沒(méi)辦法綁定事件。
src/components/Home/index.js
import React from 'react'; const Home = () => { return <div onClick={() => { alert('click'); }}>home</div> } export default Home;
一般我們的做法是先將頁(yè)面渲染出來(lái),然后將相同的代碼在瀏覽器端像傳統(tǒng)的React項(xiàng)目一樣再去運(yùn)行一遍,這樣的話這個(gè)點(diǎn)擊事件就有了。
這就衍生出一個(gè)同構(gòu)的概念,我的理解是一套R(shí)eact代碼在服務(wù)器端執(zhí)行一次,在客戶端再執(zhí)行一次。
同構(gòu)就可以解決點(diǎn)擊事件無(wú)效的問(wèn)題,首先服務(wù)器端執(zhí)行一次能夠正常的展示頁(yè)面,客戶端再執(zhí)行一次就可以綁定上事件。
我們可以在頁(yè)面渲染的時(shí)候加載一個(gè)index.js, 使用app.use創(chuàng)建靜態(tài)文件的訪問(wèn)路徑, 這樣訪問(wèn)的index.js就會(huì)請(qǐng)求到/public/index.js文件中。
app.use(express.static('public')); app.get('/', function(req, res) { res.send(` <html> <body> <div id="root">${content}</div> <script src="/index.js"></script> </body> </html> `); })
public/index.js
console.log('public');
基于這種情況我們就可以將React代碼在瀏覽器中執(zhí)行一次,我們這里新建一個(gè)/src/client/index.js。將客戶端執(zhí)行的代碼帖進(jìn)去。這里我們同構(gòu)代碼使用hydrate代替render。
import React from 'react'; import ReactDOM from 'react-dom'; import Home from '../Components/Home'; ReactDOM.hydrate(<Home />, document.getElementById('root'));
然后我們還需要在根目錄創(chuàng)建一個(gè)webpack.client.js文件。入口文件為./src/client/index.js,出口文件到public/index.js
const Path = require('path'); module.exports = { mode: 'development', entry: './src/client/index.js', output: { filename: 'index.js', path: Path.resolve(__dirname, 'public') }, module: { rules: [ { test: /.js?$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['react', 'stage-0', ['env', { targets: { browsers: ['last 2 versions'] } }]] } } ] } }
package.json文件中添加一條打包c(diǎn)lient目錄的命令
{ ... "scripts": { "dev": "npm-run-all --parallel dev:**", "dev:start": "nodemon --watch build --exec node \"./build/bundile.js\"", "dev:build": "webpack --config webpack.server.js --watch", "dev:build": "webpack --config webpack.client.js --watch", } ... }
這樣我們啟動(dòng)的時(shí)候會(huì)編譯client運(yùn)行的文件。再去訪問(wèn)頁(yè)面的時(shí)候就可以綁定好事件了。
下面我們對(duì)上面工程的代碼進(jìn)行整理,上面webpack.server.js和webpack.client.js文件有很多重復(fù)的地方,我們可以使用webpack-merge插件對(duì)內(nèi)容進(jìn)行合并。
webpack.base.js
module.exports = { module: { rules: [ { test: /.js?$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['react', 'stage-0', ['env', { targets: { browsers: ['last 2 versions'] } }]] } } ] } }
webpack.server.js
const Path = require('path'); const NodeExternals = require('webpack-node-externals'); // 服務(wù)端運(yùn)行webpack需要運(yùn)行NodeExternals, 他的作用是將express這類node模塊不被打包到j(luò)s里。 const merge = require('webpack-merge'); const config = require('./webpack.base.js'); const serverConfig = { target: 'node', mode: 'development', entry: './src/server/index.js', output: { filename: 'bundle.js', path: Path.resolve(__dirname, 'build') }, externals: [NodeExternals()], } module.exports = merge(config, serverConfig);
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);
src/server中放置的是服務(wù)端運(yùn)行的代碼,src/client放置的是瀏覽器端運(yùn)行的js。
看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。