Redux服務端渲染及webpack優化
把之前寫的放上來,排版不怎么好,建議看原文,原文地址: http://galen-yip.com
</div>
前言
在上一篇文章中對redux的基本用法以及一些原理的性的東西進行了分析
還沒看的童鞋可以看這里
這一篇主要是webpack、redux、react-router和server rendering的應用,主要面向有了一定redux基礎和webpack基礎的,當做學習記錄,如有不當,望不吝賜教
客戶端渲染VS服務端渲染
現如今的SPA,backbone、ember、vue、angular、react這些包括各家的前端輪子都biubiubiu冒出來,帶來的好處真的太多,以前由server控制的轉移到客戶端。頁面渲染、路由跳轉、數據拉取等等等等JS完全控制了,也大大提高了用戶體驗,這里講的客戶端渲染,基本上就是客戶端ajax拉取數據,然后渲染,之后js操控全部的邏輯。但是這也就主要造成了兩個問題:
1、SEO問題,爬蟲抓不到內容。目前這個也是有五花八門的解決方案。
2、客戶端初始化渲染比服務端頁面直出還是慢,需要等js加載完之后才能渲染。
因此為了解決上面兩個問題,我們就有了 服務端渲染
服務端渲染
作用:用于用戶首次請求,便于SEO,加快頁面顯示
原理:
-
server跟client共享一部分代碼
-
server拿到首次渲染需要的數據initialState
-
server根據initialState把html render出來
-
server把html和initialState發往客戶端
其實服務端渲染以前就有了,只是react的出現讓這個重新被提起,因為react能讓它實現起來更優雅
終于要進入redux要點了
-
服務端
-
客戶端
服務端
那結合上一篇文章中的應用,redux在服務端應該如何使用呢?按照上面服務端渲染的流程:
- 取得store
- 獲取initialState
- 用renderToString渲染html
- 把html和initialState注入到模板中,initialState用script的方式寫在window對象下,客戶端就可以用window. initial_state 取得
- 發送注入模板后的字符串到客戶端
新建一個server.js在根目錄,服務端用express做服務代碼形如下:
import path from 'path'; import express from 'express'; import compression from 'compression';import React from 'react'; import { renderToString } from 'react-dom/server'; import { match, RoutingContext } from 'react-router'; import { createStore, applyMiddleware, compose } from 'redux'; import { Provider } from 'react-redux'; import createLocation from 'history/lib/createLocation'; import createMemoryHistory from 'history/lib/createMemoryHistory';
import rootReducer from './app/reducers'; import middleware from './app/middleware'; import createRoutes from './app/routes/routes';
const app = express(); const port = process.env.PORT || 3000;
app.set('views', path.join(__dirname, 'views'));
app.use(compression()); app.use(bodyParser.json()); app.use('/build', express.static(path.join(__dirname, 'build'))); //設置build文件夾為存放靜態文件的目錄 app.use(handleRender);
function handleRender(req, res) {
const history = createMemoryHistory(); const routes = createRoutes(history) const location = createLocation(req.url); // req.url is the full url match({ routes, location }, (err, redirectLocation, renderProps) => { if(err) { return res.status(500).send(err.message) } if(!renderProps) { return res.status(404).send('not found') } const store = compose( applyMiddleware.apply(this, middleware) )(createStore)(rootReducer) // render the component to string const initialView = renderToString( <div> <Provider store={store}> { <RoutingContext {...renderProps} /> } </Provider> </div> ) const initialState = store.getState(); res.status(200).send(renderFullPage(initialView, initialState)) })
}
function renderFullPage(html, initialState) { const assets = require('./stats.json');
return ` <!DOCTYPE html> <!--[if lt IE 7 ]> <html lang="en" class="ie6" > <![endif]--> <!--[if IE 7 ]> <html lang="en" class="ie7" > <![endif]--> <!--[if IE 8 ]> <html lang="en" class="ie8" > <![endif]--> <!--[if IE 9 ]> <html lang="en" class="ie9" > <![endif]--> <!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="" > <!--<![endif]--> <head> <meta charset="utf-8"> <title>react-redux-router</title> <link href="./build/${assets.assetsByChunkName.app[1]}" rel="stylesheet"> </head> <body> <div id="app">${html}</div> <script> window.__INITIAL_STATE__ = ${JSON.stringify(initialState)} </script> <script src="http://cdn.bootcss.com/react/0.14.7/react.min.js"></script> <script src="http://cdn.bootcss.com/react/0.14.7/react-dom.min.js"></script> <script src="./build/${assets.assetsByChunkName.vendors}"></script> <script src="./build/${assets.assetsByChunkName.app[0]}"></script> </body> </html> `
}
app.listen(port, () => { console.log('this server is running on ' + port) });</pre>
在實踐中遇到以下幾個有點坑爹的問題:
1、寫一個server.js,用了es6的語法,要運行的話需要用babel-node( node —harmony 不支持import ) 。但一直執行babel-node都提示沒有presets。最后發現是全局的babel是5.x.x版本,需要升級。因為babel6進行了拆分,先卸載了babel,然后安裝babel-cli。然后在package.son加了一條:
scripts: { "server": "nodemon server.js --exec babel-node", ... }2、服務端的react-router和客戶端的用法完全不同,看了它的github主頁才知道 地址
用到了 match 和 RouterContext 這兩個api
</div>
3、服務端用了express。然后發現怎么都加載不進靜態資源,然后想起要加入 app.use(express.static(path.join(__dirname, 'build')));
重新執行命令,發現能夠加載靜態文件了。但是發現服務端渲染的內容不對,渲染的內容跟客戶端渲染的一樣。然后發現是我的build目錄下有生成的Index.html文件,express默認去加載了那個文件,而沒有用吐出的內容。找到原因就好做了,在app.use中加入第一個參數路徑即可 app.use('/build', express.static(path.join(__dirname, 'build')));
4、 this.props.items.toArray is not a function 的報錯,原因定位是靠 window.__INITIAL_STATE__ 獲取的state是原始的js對象,因為項目用了immutableJs,需要轉換成immutable對象。
直接 Immutable.fromJS(window.__INITIAL_STATE__) 發現還是不行,redux只接受鍵值跟原本一樣的。于是要改成這樣:
if(initialState) { Object.keys(initialState).forEach(key => { initialState[key] = fromJS(initialState[key]) }) }頂級的不變,只把值變成immutable data
5、 Warning: React attempted to reuse markup in a container but the checksum was invalid......
(client) "><a class="" href="#Home" data-reactid= (server) "><a class="" href="Home" data-reactid="如上的錯誤提示,是服務端渲染出來的跟客戶端預期渲染的不一樣,最后定位是客戶端用的是 'history/lib/createHashHistory'導致的,換成'history/lib/createBrowserHistory'問題解決
換成createBrowserHistory后,在瀏覽器直接打開index.html會報 Failed to execute 'replaceState' on 'History'
看來是谷歌新版的安全策略6、控制臺中打印的 Download the React DevTools for a better development experience: https://fb.me/react-devtools
如果需要去掉,則在webpack.config.js中加入plugins: [ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }) ]客戶端
客戶端的改動并不大,在createStore前,獲取window. INITIAL_STATE 作為initialState。其他不用做改動
const initialState = window.__INITIAL_STATE__; if(initialState) { Object.keys(initialState).forEach(key => { initialState[key] = fromJS(initialState[key]) }) } const store = configureStore(initialState);以上大體就是redux在服務端渲染的應用,但目前還沒有對于api請求這一點,之后會再慢慢完善
webpack的優化
babel5到6之后,用es6寫東西,webpack打包起來慢啊,webpack真的可以說是前端的帶薪編譯......我們只能盡量去優化一些了
1、用alias別名
alias: { 'react': path.resolve(PATHS.node_modules, 'react/dist/react.js'), 'react-dom': path.resolve(PATHS.node_modules, 'react-dom/dist/react-dom.js'), 'react-redux': path.resolve(PATHS.node_modules, 'react-redux/dist/react-redux.js'), 'react-router': path.resolve(PATHS.node_modules, 'react-router/lib/index.js'), 'redux': path.resolve(PATHS.node_modules, 'redux/dist/redux.js') }直接指明路徑,免去硬盤搜索的時間浪費。其實這里本應該指向壓縮后的文件,但是在 additional chunk assets 這一步會卡到20多秒,去google了下,發現是說 UglifyJSPlugin 接受壓縮的文件會讓webpack執行得十分慢。所以alias這里沒引用壓縮后的文件
2、不打包react和react-dom,而是全局引用
去掉alias中的react和react-dom
resolve: { alias: { // 'react': path.resolve(PATHS.node_modules, 'react/dist/react.js'), // 'react-dom': path.resolve(PATHS.node_modules, 'react-dom/dist/react-dom.js'), 'react-redux': path.resolve(PATHS.node_modules, 'react-redux/dist/react-redux.js'), 'react-router': path.resolve(PATHS.node_modules, 'react-router/lib/index.js'), 'redux': path.resolve(PATHS.node_modules, 'redux/dist/redux.js') } }webpack配置文件中加上
externals: { 'react': 'React', 'react-dom': 'ReactDOM' }然后去node_modules/html-webpack-template/index.html中加上react和react-dom的CDN引用(服務端渲染的也要記得加上)
vendors由原來的376KB變成了247KB.
3、提取css
用 ExtractTextPlugin 這個插件,至于怎么使用,可以看github項目文件了
4、清理build文件夾
用 CleanPlugin ,每次build的時候,清理一下build文件夾
服務端改進了下,加了compress,用了ejs的模板引擎,這個項目會慢慢地去完善它,再次祭出地址 github傳送門