react、redux什麼的都用起來 【4】生產部署和優化

如今項目已經有了,可是要把它放到生產環境中仍是有些事情要作,在這最後一節,來把它們一一搞定。javascript

這一節其實更可能是關於webpack的內容。不過要想把react用得很爽,咱們須要一個現代化的構建工具。在前面幾節webpack都在默默地工做着。react全都是關於組件的,組件意味着模塊化,webpack讓前端模塊化得淋漓盡致。咱們的目標是要把react用起來,而且是很舒坦的用起來,因此我以爲這節並沒跑題,並且很重要。

打包部署文件

咱們的源代碼是無法直接跑起來的。ES6語法大部分瀏覽器還不徹底支持,有些瀏覽器徹底不支持。而less、sass這些樣式框架就更不用說了。另外對這些代碼最好進行壓縮,以得到更快的訪問速度。因此在正式發佈這些代碼前必須先要編譯打包。webpack但是幹這個的以大能手,看名字就知道了。那要怎麼打包呢?終端執行:css

npm run dist

搞定。html

如今咱們的項目目錄裏多出了一個名爲dist的文件夾,這裏面就是要部署的所有內容。因爲generator-react-webpack-redux已經爲咱們作好了webpack的一些配置,因此咱們看到打包好的文件已經通過了壓縮混淆。前端

服務器設置

若是咱們在使用react-router的時候選擇了瀏覽器歷史管理方式,那麼服務器必需要可以正確處理各類路徑。實際上咱們的應用只有一個頁面文件,在訪問各類有效路徑的時候,服務都應該返回那惟一的頁面。在開發過程當中,咱們經過npm start指令啓動了一個node服務,它已經處理好了這些路由。可是在實際生產環境中,咱們每每會使用一個靜態服務器,好比nginx或apache。若是把剛纔打包好的dist目錄扔給nginx,你會發現只有根路徑能夠訪問,經過點擊跳轉到各個路由沒問題(也就是經過react-router控制的跳轉),要直接在瀏覽器的地址欄輸入"http://localhost/news"這樣的自路徑就404了。如今以nginx爲例來配置好適合咱們應用的路由。java

咱們所需配置的內容都在http > server節點下。node

首先考慮對諸如/news這樣的路徑並不存在對應的頁面文件,因此對於未知路徑要都給打發到根路徑下:react

location / {
  root   /Users/someone/my-project/dist;
  index  index.html index.htm;
  try_files $uri /index.html;
}

這樣,咱們在地址欄輸入"http://localhost/news"之後,nginx沒有找到news.html,它就嘗試找index.html,inedex.html打開後,咱們的代碼就生效了,react-router看到地址欄裏的路徑是/news,它就會在一開始去匹配/news,並改變狀態。webpack

至於腳本、圖片這些靜態文件咱們不用處理,由於nginx按照路徑就能夠直接找到這些文件。另外就是把後端服務的接口處理好,nginx代理tomcat這些後端服務是很常見的配置,只要注意在路徑上服務和頁面要能明顯區分開,好比全部的後端服務接口都有.do後綴,這樣配置就好了:nginx

location ~*.do$ {
  proxy_pass   http://192.168.1.1:8088;
}

分離樣式文件

儘管在示例代碼裏我把樣式都寫成內聯形式的了,但我仍是建議寫單獨的樣式文件。前面也提到過,樣式文件能夠直接在js代碼中引入,這對於構造獨立的模塊很是方便。可是在默認狀態下,咱們會發現導出的文件沒有css文件,實際上導入的樣式是在代碼運行時加到頁面上的style標籤裏的。這樣頁面渲染性能不太好,並且會增大js文件的體積,最好仍是把它拿出來。萬能的npm裏有專幹這個的webpack插件,來把它裝上先:web

npm install extract-text-webpack-plugin --save-dev

而後要修改一下webpack的配置文件。因爲這個插件只有在打包的時候纔會用到,因此咱們只改cfg/dist.js文件。引入這個插件,而後在plugins數組裏添加相應的項目:

// ...
let ExtractTextPlugin = require('extract-text-webpack-plugin');
// ...
let config = _.merge({
  // ...
  plugins: [
    // ...
    new ExtractTextPlugin('app.css')
  ]
// ...

還要改一下loader。本來loader是寫在cfg/base.js裏面的,可是在開發環境中咱們用不到這個插件,而若是使用了插件提供的loader就會報錯,因此咱們在dist.js裏面把config.module.loaders數組覆蓋。假如咱們的項目裏用到了css和less兩種樣式文件,就在config.module.loaders.push這一段前面添加以下代碼:

config.module.loaders = [
  {
    test: /\.css$/,
    loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
  },
  {
    test: /\.less/,
    loader: ExtractTextPlugin.extract('style-loader', 'css-loader!less-loader')
  },
  {
    test: /\.(png|jpg|gif|woff|woff2)$/,
    loader: 'url-loader?limit=8192'
  }
]

這裏除了兩種樣式文件的loader之外,還把base裏的一個非樣式的loader給帶過來了,別把它忽略了,它頗有用,一下子再說。

如今再運行npm run dist,能夠看到asset文件夾裏多了一個app.css文件。別忘了在index.html文件裏面引入新生成的樣式文件。

加載圖片

webpack讓咱們能夠在js代碼中引入圖片並使用,引入圖片只需一個簡單的require語句:

let logo = require('../images/logo.png');

而後能夠像使用其它變量同樣來使用這個圖片:

render(){
  return <img src={logo}>
}

你可能以爲,一個圖片直接用它的路徑就好了,何須要裝模做樣的引入呢?我認爲有這麼作兩個好處:

首先仍是模塊化。若是一個組件須要用到圖片,在這個組件文件內引入圖片,圖片會在run dist時一併打包,不用擔憂圖片丟失。

其次不少服務器會對圖片進行CDN緩存,若是你替換了一張圖片,極可能它在一段時間內不會生效,而經過webpack引入的圖片是一內聯base64或者重命名爲惟一hash文件名的形式打包的,這樣就不會出現惱人的緩存狀況。

不僅是在js中引入圖片會被webpack處理,css裏的圖片也會被一樣的方式處理。

若是你已經在你的項目里加上了幾個小圖片,你可能會發現打包後並無看到圖片或者圖片比原來少,這是由於有一個臨界值,低於它的圖片會直接轉成base64寫在導出的js文件裏。這樣也好也很差,好處是圖片在一開始就被載入,後面不會出現圖片延後載入的效果,用戶體驗很好,很差就是base64比原圖片大小更大,若是圖片比較多,導出的js文件就會太大,讓用戶初始等待時間過長。因此咱們要權衡利弊設置一個合適的臨界值。前面咱們在dist.js配置文件中重寫loaders的時候把base裏的一個loader帶了過來,它就是幹這個用的,test屬性的正則表達式代表咱們想讓webpack處理什麼格式的圖片,loader屬性最後的數字就是內聯圖片臨界值,單位是字節。咱們把它設置成1K吧:

{
  test: /\.(png|jpg|gif|woff|woff2)$/,
  loader: 'url-loader?limit=1024'
}

多個入口

咱們的目標是單頁應用,可是當項目規模比較大的時候整個項目可能會被拆分紅多個單頁應用。拆分多個應用的關鍵在於要有多個入口文件。目前咱們的項目只有一個入口文件:src/index.js。來看cfg/dist.js文件,裏面的config對象中entry屬性的值如今是一個index.js路徑字符串。entry的值也能夠是一個對象,這樣就能夠聲明多個入口文件,對象的key對應着文件名。好比咱們想要增長一個入口文件src/test.js,先搞點很簡單的內容:

import React from 'react';
import { render } from 'react-dom';

render(
  <div>TEST</div>,
  document.getElementById('app')
);

把cfg/dist.js中的config.entry改爲這樣:

entry: {
  app: path.join(__dirname, '../src/index'),
  test: path.join(__dirname, '../src/test')
}

如今明確指定了兩個入口文件,而後還要修改config.output.filename:

config.output.filename = '[name].js'

輸出文件時,name會自動對應成entry中的key。執行npm run dist,如今asset目錄中多出了個test.js。

使用這個文件須要另外一個單獨的頁面,若是咱們用靜態html頁面的話,要把頁面路徑添加到項目根目錄下的package.json中,在scripts對象中有個copy屬性,加到裏面就好了,這樣才能在run dist的時候把它一併拷貝到dist目錄裏。

最後,也許你還要修改一下nginx配置,讓test路徑單獨匹配。

分離第三方庫

你可能發現了剛纔咱們把文件分紅多個入口時,新入口文件即便內容很是少,哪怕只渲染了一個div,生成的文件大小還有上百k。裏面其實主要都是第三方庫。這太不優雅了,既然這些第三方庫幾乎會被全部的應用重複使用,必定得把他們單拎出來。因而咱們須要一個插件:CommonsChunkPlugin。這個插件不用單獨安裝了,它被包含在webpact.optimize裏面。咱們打算再輸出一個叫commons.js的文件,包含所有第三方庫。在cfg/dist.js的plugins數組裏面添加這個插件:

new webpack.optimize.CommonsChunkPlugin('commons', 'commons.js')

而後在entry對象裏面再添加一個commons屬性,它的值是一個數組,包含全部咱們想要拎出來的庫:

entry: {
  app: path.join(__dirname, '../src/index'),
  test: path.join(__dirname, '../src/test'),
  commons: [
    'react',
    'react-dom',
    'react-redux',
    'react-router',
    'redux',
    'redux-thunk'
  ]
}

OK,輸出的文件多了個commons.js,而app.js和test.js比原來小了不少。這回優雅了。別忘了在全部的頁面裏都把commons.js引進去。

按需加載

當項目很是大的時候,拆分多個入口文件是一種方案,還有一種方案是按需加載,也就是懶加載或異步加載。咱們可讓用戶真正進入一個路由時才把對應的組件加載進來,要實現這個很是簡單,只須要一個webpack的loader:react-router-loader,先用npm把它安裝上,而後修改src/routs.js文件,好比咱們如今想讓登陸頁面懶加載,那就把登陸頁面的路由改爲這樣:

<Route path="login" component={require('react-router!./containers/Login')}/>

編譯打包後,又多出了一個1.1.js文件,這就是在進入登陸路由時要加載的文件,也就是單獨的登陸組件。其它的就不用咱們管了,代碼會自動處理的。

既然是按需加載,咱們必定是但願初始的時候加載的代碼儘可能少,儘量在進入某個路由時才載入相應的所有內容。咱們的代碼大體就三類東西:組件、action和reducer。組件很明顯能夠是獨立載入的。reducer恐怕沒辦法,由於它須要指導整個倉庫狀態的創建。至於action,咱們前面的示例代碼是不獨立的,由於reducer要依賴action文件裏面的常量,咱們只須要把全部的常量提出到一個公共的文件中,只有組件引用action文件。好比咱們新建一個src/consts.js文件,內容是:

export const INPUT_USERNAME = 'INPUT_USERNAME'
export const INPUT_PASSWORD = 'INPUT_PASSWORD'
export const RECEIVE_NEWS_LIST = 'RECEIVE_NEWS_LIST'
export const SET_KEYWORD = 'SET_KEYWORD'
// 全部action的常量...

而後還以login爲例,把src/reducers/login.js裏面引入常量的目標改成consts.js:

import {INPUT_USERNAME, INPUT_PASSWORD} from '../consts'

src/actions/login.js裏也這樣引入常量。run dist後,1.1.js文件就包含了actions/login.js裏面的內容。

添加hash後綴

在一個大型且須要頻繁升級的項目中,靜態文件每每須要添加hash後綴,這主要是出於兩個緣由:一個是全部版本的靜態文件能夠同時存在,而頁面由後端控制,後端根據接口的版本綁定js和css文件,這樣便於升級和回滾。另外一個是防止緩存,這和前面圖片重命名爲hash值是一個道理。

讓webpack爲文件名添加後綴很是簡單,只須要在輸出的文件名上加上[hash]就能夠了。好比咱們想讓app.js帶上hash後綴,只須要在cfg/dist.js最後一句前面加上一句:

config.output.filename = 'app.[hash].js'

而對於插件生成的樣式文件和公共js文件一樣也是在文件名上加上[hash]就好了。

如今關鍵的問題是怎麼應用這些有了hash後綴的文件。總不能每打一次包咱們就手動改一下index.html把。

webpack的配置文件是js,這就意味着這個配置文件是活的,咱們能夠很容易把想作的事情經過代碼實現。如今我要在每次打包後把index.html文件引入的js和css文件自動替換成帶hash尾巴的形式,只需添加一個本身寫的插件,其實就是一個函數。在cfg/dist.js裏面的plugins數組裏添加如下函數:

function() {
  this.plugin("done", function(stats) {
    let htmlPath = path.join(__dirname, '../dist/index.html')
    let htmlText = fs.readFileSync(htmlPath, {encoding:'utf-8'})
    let assets = stats.toJson().assetsByChunkName
    Object.keys(assets).forEach((key)=>{
      let fileNames = assets[key];
      ['js', 'css'].forEach(function(ext){
        htmlText = htmlText.replace(key+'.'+ext, fileNames.find(function(item){
          return new RegExp(key+'\\.\\w+\\.'+ext+'$').test(item)
        }))
      })
    })

    fs.writeFileSync( htmlPath, htmlText)
  });
}

很暴力,就是赤裸裸的node操做文件系統。這回dist文件夾中的index.html裏引入的腳本和樣式都是帶hash的了。

在不少項目中,咱們前端要提供的可能不是一個引用好js和css的html文件,而是一個map文件,裏面有靜態文件的版本信息(hash值),這樣後端就能直接把須要的靜態文件掛上。能夠本身寫一個跟上面代碼相似的插件輸出一個map文件,也可在萬能的npm找個插件,好比map-json-webpack-plugin。上面那個功能也能夠試試replace-webpack-plugin。

 

到這裏,這一系列關於react的博客就算告一段落了。其實我還想寫一個關於測試的,由於react+redux的這種模式很是利於測試,不過我還在琢磨測試當中,等琢磨得差很少了也許會補上一篇。

🖐

 

上一節 【3】穿越spa的路由

相關文章
相關標籤/搜索