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

来源:http://www.cnblogs.com/tolg/archive/2016/04/06/5359957.html
-Advertisement-
Play Games

現在項目已經有了,但是要把它放到生產環境中還是有些事情要做,在這最後一節,來把它們一一搞定。 這一節其實更多是關於webpack的內容。不過要想把react用得很爽,我們需要一個現代化的構建工具。在前面幾節webpack都在默默地工作著。react全都是關於組件的,組件意味著模塊化,webpack讓 ...


現在項目已經有了,但是要把它放到生產環境中還是有些事情要做,在這最後一節,來把它們一一搞定。

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

打包部署文件

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

npm run dist

搞定。

現在我們的項目目錄里多出了一個名為dist的文件夾,這裡面就是要部署的全部內容。由於generator-react-webpack-redux已經為我們做好了webpack的一些配置,所以我們看到打包好的文件已經經過了壓縮混淆。

伺服器設置

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

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

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

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,並改變狀態。

至於腳本、圖片這些靜態文件我們不用處理,因為nginx按照路徑就可以直接找到這些文件。另外就是把後端服務的介面處理好,nginx代理tomcat這些後端服務是很常見的配置,只要註意在路徑上服務和頁面要能明顯區分開,比如所有的後端服務介面都有.do尾碼,這樣配置就行了:

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

分離樣式文件

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

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的這種模式非常利於測試,不過我還在琢磨測試當中,等琢磨得差不多了也許會補上一篇。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 上一篇文章介紹了VMWare12虛擬機、Linux(CentOS7)系統安裝、部署Nginx1.6.3代理服務做負載均衡。接下來介紹通過Nginx將請求分發到各web應用處理服務。 一、Web應用開發 1、asp.net mvc5開發 (1)新建一個MVC5工程,新建一個Controller,在In ...
  • Atitit..css的體繫結構 1. Oocss 與 bem標準化1 1.1. 四種樣式表及六種選擇器1 1.2. 常用的css框架 amazeui bootstrap1 1.3. Css圖標1 1.4. Css的操作api1 1.5. Css ide ::2 1. Oocss 與 bem標準化 ...
  • Atitit.判斷元素是否顯示隱藏在父元素 overflow 1.1. scrollTop 指的是元素的滾動條頂端距離原生基線的高度...1 1.2. 判斷元素是否顯示隱藏在父元素 $(next).position().top》》curBaseTop= $(".listBlock_main").sc ...
  • AMD是RequireJS在推廣過程中對模塊定義的規範化產出。 非同步載入模塊,依賴前置,提前執行。 Define定義模塊 define([‘require’,’foo’],function(){return}); Require載入模塊(依賴前置) require([‘foo’,’bar’],fun... ...
  • Atitit.js模塊化 atiImport 的新特性javascript import 1. 常見的js import規範amd ,cmd ,umd1 1.1. Require更多流行3 2. atiImport3 2.1. Base url的設置4 2.2. Atiimport的使用4 2.3. ...
  • 前言 上一節我們已經把環境給搭建起來了,現在我們通過一個快速案例把angular 2.0 初步瞭解一下,後續我們會深入每一個細節,這個案例主要是一個【英雄(Hero)】列表的展示,創建,編輯。這個案例我打算分五個章節來做,第一個章節我們可以學習到angular2.0一下內容: 單項數據綁定 雙向數據 ...
  • 動畫的思路很簡單,點擊頁面上一個元素,頁面滾動到指定位置。下麵介紹一下我3個小時百度的研究成果: 首先是html部分: 先添加兩個<a>元素作為按鈕。然後對<a>元素進行補充: 接著引入jquery和寫入代碼: 需要註意: 1、寫入的代碼最好在引入的jquery語句下方 2、id一定要和<a>元素對 ...
  • 前臺代碼: @using Models; @{ Layout = "~/Views/Shared/_Layout.cshtml"; } <link type="text/css" href="~/Scripts/zTree/css/zTreeStyle/zTreeStyle.css" rel="st ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...