優化單頁面開發環境:webpack與react的運行時打包與熱更新

這是Webpack+React系列配置過程記錄的第三篇。其餘內容請參考:css

前面兩篇文章介紹初步搭建單頁面應用的開發環境,這篇文章將基於前面兩篇文章進一步優化開發環境,實現單頁面開發時的運行時打包與熱更新。html

調整文件佈局

在第二篇文章中發現了框架代碼文件的命名有些衝突,這裏咱們須要作一下調整,以便接下來的講述不易出現問題。調整時須要小小地改動配置文件幾個路徑。文件佈局調整先後對好比下:前端

項目佈局調整

圖片基本已經說明了狀況。咱們將在src目錄下開發代碼,而編譯後的代碼將存放在public目錄中。開發過程當中,咱們使用server.js配置的服務器進行測試。node

接下來開始本文的正題。react

配置運行時打包

前面兩篇文章中,咱們每次改動代碼都須要使用下面兩條命令webpack

npm run build  
npm start  

編譯和運行代碼。這讓每次build都須要輸入這麼多字;並且每次都須要掃描全部文件,效率十分低。git

因此此次咱們要配置運行時打包,只要測試服務器啓動後,就可讓每次改動的內容都被webpack監測到而且自動打包。webpack-dev-middleware這個express的中間件能夠實現該需求。github

安裝

安裝webpack-dev-middleware:web

npm install --save-dev webpack-dev-middleware
配置與啓用webpack-dev-middleware

這是express的中間件,所以須要配置測試服務器端的代碼server.js:express

var express = require('express');  
var app = express();

app.use('/', require('connect-history-api-fallback')());  
app.use('/', express.static('public'));

if (process.env.NODE_ENV !== 'production') {  
  var webpack = require('webpack');
  var webpackConfig = require('./webpack.config.js');
  var webpackCompiled = webpack(webpackConfig);
  // 配置運行時打包
  var webpackDevMiddleware = require('webpack-dev-middleware');
  app.use(webpackDevMiddleware(webpackCompiled, {
      publicPath: "/",
    stats: {colors: true},
    lazy: false,
    watchOptions: {
        aggregateTimeout: 300,
        poll: true
    },
  }));
}

var server = app.listen(2000, function() {  
  var port = server.address().port;
  console.log('Open http://localhost:%s', port);
});

server.js把webpack和express鏈接到了一塊兒實現了運行時打包。我這裏簡單使用了webpack-dev-middleware的幾個配置項:

  • publicPath:這個插件的惟一必填項。因爲index.html請求的out.js存放的位置映射到服務器的URI路徑是根,即「/」,因此我賦予了publicPath爲:「/」。
  • stats:我設置了console統計日誌帶顏色輸出。
  • lazy:指示是否懶人加載模式。true表示不監控源碼修改狀態,收到請求才執行webpack的build。false表示監控源碼狀態,配套使用的watchOptions能夠設置與之相關的參數。

還有其餘配置項,能夠經過官網查閱按需配置。

接下來,咱們須要刪除以前使用npm run build命令生成的out.js。不然在驗證效果時,因爲server.js中靜態服務器的static中間件優先捕獲到關於out.js的請求,將直接返回結果給客戶端,致使看不到運行時打包的效果。

那麼index.html引用的out.js文件是哪裏來的呢?就是webpack-dev-middleware這個中間件利用緩存方式生成的。

驗證

使用npm start命令啓動服務器,在瀏覽器訪問index.html,能夠看到頁面正常顯示。

修改src/index.js文件中的內容並保存。這時服務器後臺執行自動打包,能夠看到控制檯輸出了打包的日誌,並不須要你再花時間敲那兩行代碼了。手動刷新瀏覽器頁面就能夠看到剛剛改動的內容。這告訴咱們服務器已經能夠實現運行時加載。

配置熱更新

咱們會注意到每次改動後仍是須要咱們刷新瀏覽器頁面才能看到結果,仍是未能讓人滿意。這時候能夠配置熱更新,讓瀏覽器自動刷新頁面。

熱更新利用到的是名叫webpack-hot-middleware的依賴。它提供了用於express的中間件用於創建鏈接和傳輸更新;也提供了webpack的插件用於生成更新內容;同時還提供了用戶端接口用於嵌入到js腳本中用於與express創建鏈接和應用更新。更詳細的原理描述能夠參考這裏

咱們須要根據這幾個方面嵌入webpack-hot-middleware到咱們的開發框架中。

安裝

使用下面命令安裝:

npm install --save-dev webpack-hot-middleware
配置服務器端

改動server.js文件,在express中增長一箇中間件便可,改動後以下:

var express = require('express');  
var app = express();

app.use('/', require('connect-history-api-fallback')());  
app.use('/', express.static('public'));

if (process.env.NODE_ENV !== 'production') {  
  var webpack = require('webpack');
  var webpackConfig = require('./webpack.config.js');
  var webpackCompiled = webpack(webpackConfig);
  // 配置運行時打包
  var webpackDevMiddleware = require('webpack-dev-middleware');
  app.use(webpackDevMiddleware(webpackCompiled, {
      publicPath: "/",
    stats: {colors: true},
    lazy: false,
    watchOptions: {
        aggregateTimeout: 300,
        poll: true
    },
  }));

  // 配置熱更新
  var webpackHotMiddleware = require('webpack-hot-middleware');
  app.use(webpackHotMiddleware(webpackCompiled));
}

var server = app.listen(2000, function() {  
  var port = server.address().port;
  console.log('Open http://localhost:%s', port);
});
在webpack中應用插件

修改webpack.config.js文件:

var path = require('path');  
var webpack = require('webpack');

module.exports = {  
  entry: ['webpack-hot-middleware/client', './src/index.js'],
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env', 'stage-0', 'react'],
            plugins: [['import', {"libraryName": "antd", "style": "css"}]]
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
};

 

注意改動中首先引入了webpack對象,而後修改了entry節點,最後添加了兩個插件。這裏兩個插件中,webpack.HotModleReplacementPlugin是關於熱更新的,webpack.NoEmitOnErrorsPlugin能夠保證出錯時頁面不阻塞,且會在編譯結束後報錯。

前端腳本中配置熱更新處理邏輯

熱更新的處理邏輯webpack已經封裝好了,只要在應用的入口文件中添加如下代碼

...
if (module.hot) {  
  module.hot.accept();
}

便可。我配置的是src/index.js。

驗證

npm start啓動服務器,瀏覽器訪問index.html。頁面顯示正常,打開開發者工具能夠看到發送了一個叫_webpackhmr的請求(請求路徑能夠配置,咱們使用了默認值)。

修改src/index.js中的某個內容並保存,將會看到控制檯輸出了打包日誌,而後瀏覽器頁面自動更新頁面內容。效果以下:

熱更新對比圖

到這裏熱更新配置完畢。

讓熱更新後保留React的組件狀態

React組件的狀態對熱更新有什麼影響?咱們先來看下面的一個例子。

在src目錄下添加Counter.js文件,內容以下:

import React from 'react';

const COUNT_STEP = 1;

export default class Counter extends React.Component {

  constructor(props) {
    super(props);
    this.state = {value: 1};
  }

  componentDidMount() {
    this.timeout = setTimeout(this.handleTimeoutEvent.bind(this), 1000);
  }

  componentWillUnmount() {
    this.timeout && clearTimeout(this.timeout);
  }

  handleTimeoutEvent() {
    this.setState({value: this.state.value + COUNT_STEP}, () => {
      this.timeout = setTimeout(this.handleTimeoutEvent.bind(this), 1000);
    });
  }

  render() {
    return (
      <div>
        <p> This is a counter: {this.state.value} </p>
      </div>
    );
  }
}

 

Counter.js定義了一個React組件,這個組件擁有一個狀態值叫value,初始值爲1。實際上,React組件的狀態指的是存儲在組件的成員變量state中的內容,value不過是咱們測試的一個實例。

在組件掛在的時候創建了一個計時器,每秒鐘增長如下value的值,增長量爲COUNT_STEP。

而後咱們修改一下index.js文件,修改內容以下:

...
import Counter from './Counter';

const BasicExample = () => (  
  <Router>
    <div>
      <ul>
        <li><Link to="/">Home111</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/topics">Topics</Link></li>
        <li><Link to="/counter">Counter</Link></li>
      </ul>

      <hr/>

      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>
      <Route path="/counter" component={Counter}/>
    </div>
  </Router>
)
...

從新啓動服務器,使用瀏覽器訪問index.html。點擊連接Counter頁面顯示了咱們定義的Counter組件,發現內容逐步在遞增1。

修改Counter.js文件中的COUNT_STEP爲10,瀏覽器由於熱更新而更新了頁面,可是咱們會發現Counter組件的狀態值會被重置爲1,而後從新開始遞增10。

這是個小問題。可是放大這個問題到其餘場景下,咱們能夠猜想,若是熱更新後頁面刷新了,那更新前的狀態會被重置,更新前被打斷的業務邏輯也沒法繼續,這明顯是個bug。

解決這個問題可使用react-hot-loader。

安裝react-hot-loader

使用下面命令安裝,官方文檔強調要增長@next指定版本。我不太理解爲何。安裝後看到添加的版本是3.0.0-beta.6

npm install --save-dev react-hot-loader@next  
配置webpack使用react-hot-loader

須要修改webpack.config.js文件。

注意基於webpack2和react-hot-loader3的配置方式跟舊版本有所不一樣。我在舊的配置方式上被坑了好久,看這裏才解決問題。

修改後的內容:

var path = require('path');  
var webpack = require('webpack');

module.exports = {  
  entry: [
    'react-hot-loader/patch',
    'webpack-hot-middleware/client',
    './src/index.js'
  ],
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env', 'stage-0', 'react'],
            plugins: [
              ['react-hot-loader/babel'],
              ['import', {"libraryName": "antd", "style": "css"}]
            ]
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
};
配置前端使用react-hot-loader

這裏有個坑,且看我直接修改index.js文件:

...
import { AppContainer } from 'react-hot-loader';  
import Counter from './Counter';  
...
//ReactDOM.render(<BasicExample/>, document.getElementById('main'));
ReactDOM.render(  
  <AppContainer>
    <BasicExample/>
  </AppContainer>,
  document.getElementById('main')
);
...

啓動服務器,訪問index.html,發現控制檯出現下面錯誤:

react-hot-loader配置錯誤

提示告訴咱們:不能在index.js中直接定義組件,而後又用AppContainer封裝組件。方法很簡單,把BasicExample抽離出來定義就能夠了。

src目錄下建立BasicExample.js文件,作一下簡單的修改,內容以下:

import React from 'react';  
import {  
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom';
import Counter from './Counter';

export default class BasicExample extends React.Component {  
  render() {
    return (
      <Router>
        <div>
          <ul>
            <li><Link to="/">Home122</Link></li>
            <li><Link to="/topics">Topics</Link></li>
            <li><Link to="/counter">Counter</Link></li>
          </ul>
          <hr/>
          <Route exact path="/" component={Home}/>
          <Route path="/topics" component={Topics}/>
          <Route path="/counter" component={Counter}/>
        </div>
      </Router>
    );
  }
}

const Home = () => (  
  <div>
    <h2>Home</h2>
  </div>
)
const Topics = ({ match }) => (  
  <div>
    <h2>Topics</h2>
    <ul>
      <li>
        <Link to={`${match.url}/props-v-state`}>
          Props v. State
        </Link>
      </li>
    </ul>

    <Route path={`${match.url}/:topicId`} component={Topic}/>
    <Route exact path={match.url} render={() => (
      <h3>Please select a topic.</h3>
    )}/>
  </div>
)
const Topic = ({ match }) => (  
  <div>
    <h3>{match.params.topicId}</h3>
  </div>
)

 

index.js文件修改成:

import React from 'react';  // 必須引入  
import ReactDOM from 'react-dom';  
import { AppContainer } from 'react-hot-loader';  
import BasicExample from './BasicExample';

ReactDOM.render(  
  <AppContainer>
    <BasicExample/>
  </AppContainer>,
  document.getElementById('main')
);

if (module.hot) {  
  module.hot.accept();
}

注意儘管index.js中沒有使用直接到React,咱們仍必須引入React,否則會報錯。猜想是後面引入的內容間接使用到了它。

驗證

設置Counter.js中的COUNT_STEP爲1。從新啓動服務器,瀏覽器訪問index.html,點擊切換到counter頁面,能夠看到頁面數值在遞增1。

修改COUNT_STEP爲10,看到頁面數值沒有重置爲1,而是直接在原來的數值上遞增10。說明組件狀態沒有被重置。

完畢。

 

同步博客原文連接

相關文章
相關標籤/搜索