Redux服務端渲染及webpack優化

zhujuned 8年前發布 | 37K 次閱讀 Redux 前端技術 webpack

來自: http://div.io/topic/1625

把之前寫的放上來,排版不怎么好,建議看原文,原文地址: 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傳送門

 本文由用戶 zhujuned 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!