上文講到使用react進行客戶端渲染頁面,此次講解在服務端利用前端react的代碼來渲染頁面並輸出到客戶端,即構建同構應用。css
PS:同構,我是這樣理解的,同一份代碼能夠同時運行在客戶端和服務端。
html
當咱們的組件不包含樣式,圖片等服務端沒法直接解析處理的時候,咱們能夠直接利用ts的tsc命令將組件編譯成相應的js,服務端則能夠直接運行該js獲得渲染的結果,固然這種狀況實際並不存在,這裏只是做爲例子來說解。前端
咱們在server目錄下新建bundle.tsx將其做爲前端react組件的一個打包入口文件。咱們經過將它打包,並在服務端執行獲得咱們須要的渲染結果。vue
// ./src/server/bundle.tsx import * as React from 'react'; /* tslint:disable-next-line no-submodule-imports */ import { renderToString } from 'react-dom/server'; import App from '../client/component/app'; export default { render() { return renderToString(<App />); }, };
能夠看到,咱們直接將客戶端的App組件引入,並輸出一個擁有render方法的對象,在服務端入口文件中咱們只須要引入該bundle對象,並調用其render方法就能夠獲得渲染出的html字符串了。node
PS:tslint:disable相似於eslint的對應語法,用來使得相應的規則不生效
react
// ./src/server/index.tsx ... router.get('/*', (ctx: Koa.Context, next) => { // 配置一個簡單的get通配路由 const html = bundle.render(); // 得到渲染出的html字符串 ctx.type = 'html'; ctx.body = ` ... <div id="app">${html}</div> ... `; next(); }); ...
PS:...表明代碼省略
webpack
在chrome中打開localhost:3344
後能夠看的頁面上的hello world,咱們右鍵頁面選擇View Page Source
,能夠看到兩種方法渲染的不一樣:git
客戶端渲染:
github
服務端渲染:
web
顯而易見,服務端渲染會直接輸出組件渲染的內容,瀏覽器在接收到這些內容後就會直接繪製呈現給咱們,而客戶端渲染會在react框架初始化完畢以後再進行,因此對比兩種狀況,客戶端渲染時白屏時間會更長一些,且刷新頁面時會有閃爍的感受。
在咱們實際開發環境中,必然存在組件裏引用樣式文件,引用圖片的狀況,這種狀況下ts並不具有webpack相應的將這些資源轉換爲js可處理的功能,因此咱們須要使用webpack來處理服務端的bundle.tsx文件,使得服務端能夠運行打包後的js文件。
在客戶端,像react這樣的庫,webpack會把它打包到輸出的js文件裏,而在服務端咱們並不須要這麼作,因此配置文件和客戶端有很大不一樣。
// ./src/webpack/server.ts import * as path from 'path'; import * as webpack from 'webpack'; import * as nodeExternals from 'webpack-node-externals'; import { cloneDeep } from 'lodash'; // lodash提供的深度複製方法cloneDeep // 客戶端+服務端全環境公共配置baseConfig,項目根目錄路徑baseDir,獲取tsRule的方法getTsRule import baseConfig, { baseDir, getTsRule } from './base'; const serverBaseConfig: webpack.Configuration = cloneDeep(baseConfig); // 服務端全環境公共配置 serverBaseConfig.entry = { // 入口屬性配置 'server-bundle': [ './src/server/bundle.tsx', ], }; serverBaseConfig.externals = [nodeExternals()], serverBaseConfig.node = { __dirname: true, __filename: true, }; serverBaseConfig.target = 'node'; serverBaseConfig.output.libraryTarget = 'commonjs2'; const serverDevConfig: webpack.Configuration = cloneDeep(serverBaseConfig); // 服務端開發環境配置 serverDevConfig.cache = false; // 禁用緩存 serverDevConfig.output.filename = '[name].js'; // 使用源文件名做爲打包後文件名 (serverDevConfig.module as webpack.NewModule).rules.push( getTsRule('./src/webpack/tsconfig.server.json'), ); serverDevConfig.plugins.push( new webpack.NoEmitOnErrorsPlugin(), // 編譯出錯時跳過輸出階段,以保證輸出的資源不包含錯誤。 ); const serverProdConfig: webpack.Configuration = cloneDeep(serverBaseConfig); // 服務端生產環境配置 // TODO 服務端生產環境配置暫不處理和使用 export default { development: serverDevConfig, production: serverProdConfig, };
疑問一:webpack-node-externals是幹啥用的?
答:該庫的做用是讓webpack忽略node_modules裏的庫,避免將他們打包到輸出文件中去。
疑問二:target爲什麼要設置爲node?
答:這是爲了讓webpack打包時忽略node內建的庫,好比fs。
疑問三:配置的node屬性設置__dirname和__filename爲true是什麼意思?
答:這是爲了讓webpack使用真實的相對當前上下文的路徑,能夠避免打包出的文件里路徑錯誤。簡單點說就是在源文件裏使用__dirname,在打包後這個__dirname會被替換爲源文件的相對路徑值,而不是打包輸出的文件的相對路徑值。
疑問四:libraryTarget設置爲commonjs2是什麼意思?
答:將入口起點的返回值將分配給 module.exports 對象,參見官方文檔詳解:output-librarytarget
相較客戶端配置,服務端須要多include一個入口文件即bundle.tsx
// ./src/webpack/tsconfig.server.json { "compilerOptions": { "target": "es5", "jsx": "react" }, "include": [ "../../src/client/**/*", "../../src/server/bundle.tsx" ] }
目前咱們準備好了服務端的webpack配置文件,如今要選擇一個時機將其執行,那就在客戶端webpack打包完畢以後吧,這樣在一塊兒有序的執行也好管理哈。
// ./src/webpack/webpack-dev-server.ts ... import webpackServerConfig from './server'; export default (app: Koa, serverCompilerDone) => { const clientDevConfig = webpackClientConfig.development; const serverDevConfig = webpackServerConfig.development; const clientCompiler = webpack(clientDevConfig); clientCompiler.plugin('done', () => { const serverCompiler = webpack(serverDevConfig); serverCompiler.plugin('done', serverCompilerDone); serverCompiler.run((err, stats) => { if (err) { console.error(stats); } }); }); ... };
咱們經過complier.plugin方法,來實現打包完成後的回調操做,咱們改造了webpack-dev-server.ts輸出的函數,接收第二個參數做爲服務端webpack打包完成後的回調函數。
// ./src/server/index.ts ... let bundle; const bundleFile = path.join(__dirname, '../../bundle/server-bundle.js'); ... if (isDev) { webpackDevServer(app, () => { delete require.cache[require.resolve(bundleFile)]; bundle = require(bundleFile).default; }); // 僅在開發環境使用 } ...
咱們定義bundle變量用於接收server-bundle.js的輸出結果,也就是咱們上面提到的擁有一個render方法的對象。因爲node的require緩存機制,因此咱們每次打包完server-bundle.js後都須要先刪除緩存,再給bundle賦值。
疑問五:require.cache的鍵值爲什麼要使用require.resolve包裹文件名?
答:require源碼在進行緩存時以絕對路徑(使用其內部resolve方法得到)爲key,因此這裏須要包裹一下以得到真實的key。
疑問六:bundle爲什麼是require(bundleFile)的default值?
答:由於bundle.tsx輸出的就是default,export default xxx至關於exports.default = xxx。
雖然在上述第二種方法裏,咱們沒有實際引入樣式、圖片等文件,可是這個操做我想應該不難,加一個對應的loader(file-loader, css-loader等)便可實現。在寫這篇文章以前,上述第二種方法裏關於bundle的動態更新方法我一直是參考使用vue裏的create-bundle-runner(利用vm實現本身的require),寫文章的時候發現其實我目前的應用場景並無那麼複雜,效率性能也沒有那麼高要求,因此就使用了原生的require方法來實現。
參見:vuejs:create-bundle-runner
By devlee