Express結合Webpack的全棧自動刷新

在之前的一篇文章BrowserSync,迅捷從免F5開始中,我介紹了BrowserSync這樣一個出色的開發工具。經過BrowserSync我感覺到了這樣一個理念:若是在一次ctrl + s保存後能夠自動刷新,而後當即看到新的頁面效果,那會是很棒的開發體驗。javascript

如今,webpack能夠說是最流行的模塊加載器(module bundler)。一方面,它爲前端靜態資源的組織和管理提供了相對較完善的解決方案,另外一方面,它也很大程度上改變了前端開發的工做流程。在應用了webpack的開發流程中,想要繼續「自動刷新」的爽快體驗,就可能得額外作一些事情。css

webpack與自動刷新

本文並不打算介紹webpack,若是你還不清楚它是什麼,推薦閱讀下面幾篇入門文章:html

webpack要求靜態資源在被真正拿來訪問以前,都要先完成一次編譯,即運行完成一次webpack命令。所以,自動刷新須要調整到適當的時間點。也就是說,修改了css等源碼並保存後,應該先觸發一次webpack編譯,在編譯完成後,再通知瀏覽器去刷新。react

開發Express項目的問題

如今有這樣的一個應用了webpack的Express項目,目錄結構以下:webpack

Express應用的目錄結構

其中,client內是前端的靜態資源文件,好比css、圖片以及瀏覽器內使用的javascript。server內是後端的文件,好比express的routes、views以及其餘用node執行的javascript。根目錄的app.js,就是啓動express的入口文件了。git

開發的時候咱們會怎樣作呢?github

先啓動Express服務器,而後在瀏覽器中打開某個頁面,接下來再編輯源文件。那麼,問題就來了,好比我編輯.scss源文件,即便我只改了一小點,我也得在命令行裏輸入webpack等它編譯完,而後再切到瀏覽器裏按一下F5,才能看到修改後的效果。

再好比,我修改了routes裏的.js文件想看看結果,我須要到命令行裏重啓一次Express服務器,而後一樣切到瀏覽器裏按一下F5。

這可真是太費事了。

因此,咱們要讓開發過程愉快起來。

改進目標

咱們但願的Express&Webpack項目的開發過程是:

  • 若是修改的是client裏的css文件(包括.scss等),保存後,瀏覽器不會整頁刷新,新的樣式效果直接更新到頁面內。

  • 若是修改的是client裏的javascript文件,保存後,瀏覽器會自動整頁刷新,獲得更新後的效果。

  • 若是修改的是server裏的文件,保存後,服務器將自動重啓,瀏覽器會在服務器重啓完畢後自動刷新。

通過屢次嘗試,我最終獲得了一個實現了以上這些目標的項目配置。接下來,本文將說明這個配置是如何作出來的。

從webpack-dev-server開始

首先,webpack已經想到了開發流程中的自動刷新,這就是webpack-dev-server。它是一個靜態資源服務器,只用於開發環境。

通常來講,對於純前端的項目(所有由靜態html文件組成),簡單地在項目根目錄運行webpack-dev-server,而後打開html,修改任意關聯的源文件並保存,webpack編譯就會運行,並在運行完成後通知瀏覽器刷新。

和直接在命令行裏運行webpack不一樣的是,webpack-dev-server會把編譯後的靜態文件所有保存在內存裏,而不會寫入到文件目錄內。這樣,少了那個每次都在變的webpack輸出目錄,會不會以爲更清爽呢?

若是在請求某個靜態資源的時候,webpack編譯尚未運行完畢,webpack-dev-server不會讓這個請求失敗,而是會一直阻塞它,直到webpack編譯完畢。這個對應的效果是,若是你在不恰當的時候刷新了頁面,不會看到錯誤,而是會在等待一段時間後從新看到正常的頁面,就好像「網速很慢」。

webpack-dev-server的功能看上去就是咱們須要的,但如何把它加入到包含後端服務器的Express項目裏呢?

webpack-dev-middleware和webpack-hot-middleware

Express本質是一系列middleware的集合,所以,適合Express的webpack開發工具是webpack-dev-middlewarewebpack-hot-middleware

webpack-dev-middleware是一個處理靜態資源的middleware。前面說的webpack-dev-server,其實是一個小型Express服務器,它也是用webpack-dev-middleware來處理webpack編譯後的輸出。

webpack-hot-middleware是一個結合webpack-dev-middleware使用的middleware,它能夠實現瀏覽器的無刷新更新(hot reload)。這也是webpack文檔裏常說的HMR(Hot Module Replacement)。

參考webpack-hot-middleware的文檔示例,咱們把這2個middleware添加到Express中。

webpack配置文件部分

首先,修改webpack的配置文件(爲了方便查看,這裏貼出了webpack.config.js的所有代碼):

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

var publicPath = 'http://localhost:3000/';
var hotMiddlewareScript = 'webpack-hot-middleware/client?reload=true';

var devConfig = {
    entry: {
        page1: ['./client/page1', hotMiddlewareScript],
        page2: ['./client/page2', hotMiddlewareScript]
    },
    output: {
        filename: './[name]/bundle.js',
        path: path.resolve('./public'),
        publicPath: publicPath
    },
    devtool: 'source-map',
    module: {
        loaders: [{
            test: /\.(png|jpg)$/,
            loader: 'url?limit=8192&context=client&name=[path][name].[ext]'
        }, {
            test: /\.scss$/,
            loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'
        }]
    },
    plugins: [
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin()
    ]
};

module.exports = devConfig;

這是一個包含多個entry的較複雜的例子。其中和webpack-hot-middleware有關的有兩處。一是plugins的位置,增長3個插件,二是entry的位置,每個entry後都增長一個hotMiddlewareScript

hotMiddlewareScript的值是webpack-hot-middleware/client?reload=true,其中?後的內容至關於爲webpack-hot-middleware設置參數,這裏reload=true的意思是,若是碰到不能hot reload的狀況,就整頁刷新。

在這個配置文件中,還有一個要點是publicPath不是/這樣的值,而是http://localhost:3000/這樣的絕對地址。這是由於,在使用?sourceMap的時候,style-loader會把css的引入作成這樣:

style-loader的效果

這種blob的形式可能會使得css裏的url()引用的圖片失效,所以建議用帶http的絕對地址(這也只有開發環境會用到)。有關這個問題的詳情,你能夠查看github上的issue

Express啓動文件部分

接下來是Express啓動文件內添加如下代碼:

var webpack = require('webpack'),
    webpackDevMiddleware = require('webpack-dev-middleware'),
    webpackHotMiddleware = require('webpack-hot-middleware'),
    webpackDevConfig = require('./webpack.config.js');

var compiler = webpack(webpackDevConfig);

// attach to the compiler & the server
app.use(webpackDevMiddleware(compiler, {

    // public path should be the same with webpack config
    publicPath: webpackDevConfig.output.publicPath,
    noInfo: true,
    stats: {
        colors: true
    }
}));
app.use(webpackHotMiddleware(compiler));

以上這段代碼應該位於Express的routes代碼以前。其中,webpack-dev-middleware配置的publicPath應該和webpack配置文件裏的一致。

webpack-dev-middleware和webpack-hot-middleware的靜態資源服務只用於開發環境。到了線上環境,應該使用express.static()

到此,client部分的目標就完成了。如今到網頁裏打開控制檯,應該能夠看到[HMR] connected的提示。這個項目中我只要求css使用HMR,若是你但願javascript也使用HMR,一個簡單的作法是在entry文件內添加如下代碼:

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

這樣,與這個entry相關的全部.js文件都會使用hot reload的形式。關於這一點的更多詳情,請參考hot module replacement

接下來是server部分。

reload和supervisor

server部分的自動刷新,會面臨一個問題:自動刷新的消息通知依靠的是瀏覽器和服務器之間的web socket鏈接,但在server部分修改代碼的話,通常都要重啓服務器來使變動生效(好比修改routes),這就會斷開web socket鏈接。

因此,這須要一個變通的策略:瀏覽器這邊增長一個對web socket斷開的處理,若是web socket斷開,則開啓一個稍長於服務器重啓時間的定時任務(setTimeout),至關於等到服務器重啓完畢後,再進行一次整頁刷新。

reload是一個應用此策略的組件,它能夠幫咱們處理服務器重啓時的瀏覽器刷新。

如今,還差一個監聽server文件,若是有變動就重啓服務器的組件。參考reload的推薦,咱們選用supervisor

下面將reload和supervisor引入到Express項目內。

監聽文件以重啓服務器

經過如下代碼安裝supervisor(是的,必須-g):

npm install supervisor -g

而後,在package.json裏設置新的scripts

"scripts": {
    "start": "cross-env NODE_ENV=dev supervisor -i client app"
}

這裏的主要變化是從node app改成supervisor -i client app。其中-i等於--ignore,這裏表示忽略client,顯然,咱們可不但願在改前端代碼的時候服務器也重啓。

這裏的cross-env也是一個npm組件,它能夠處理windows和其餘Unix系統在設置環境變量的寫法上不一致的問題。

把會重啓的服務器和瀏覽器關聯起來

把Express啓動文件最後的部分作這樣的修改:

var reload = require('reload');
var http = require('http');

var server = http.createServer(app);
reload(server, app);

server.listen(3000, function(){
    console.log('App (dev) is now running on port 3000!');
});

Express啓動文件的最後通常是app.listen()。參照reload的說明,須要這樣用http再增長一層服務。

而後,再到Express的視圖文件views裏,在底部增長一個<script>

<% if (env !== "production") { %>
    <script src="/reload/reload.js"></script>
<% } %>

全部的views都須要這樣一段代碼,所以最好藉助模板引擎用include或extends的方式添加到公共位置。

這裏的reload.js和前面webpack的開發環境bundle.js並不衝突,它們一個負責前端源文件變動後進行編譯和刷新,另外一個負責在服務器發生重啓時觸發延時刷新。

到此,server也完成了。如今,修改項目內的任意源文件,按下ctrl + s,瀏覽器裏的頁面都會對應地作一次「適當」的刷新。

完整示例

完整示例已經提交到github:express-webpack-full-live-reload-example

效果以下:

示例效果

附加的可選方案

前面說的server部分,分爲views和routes,若是隻修改views,那麼服務器並不須要重啓,直接刷新瀏覽器就能夠了。

針對這樣的開發情景,能夠把views文件的修改刷新變得更快。這時候咱們不用reload和supervisor,改成用browsersync,在Express的啓動文件內作以下修改:

var bs = require('browser-sync').create();
app.listen(3000, function(){
    bs.init({
        open: false,
        ui: false,
        notify: false,
        proxy: 'localhost:3000',
        files: ['./server/views/**'],
        port: 8080
    });
    console.log('App (dev) is going to be running on port 8080 (by browsersync).');
});

而後,使用browsersync提供的新的訪問地址就能夠了。這樣,修改views(html)的時候,由browsersync幫忙直接刷新,修改css和javascript的時候繼續由webpack的middleware來執行編譯和刷新。

結語

有了webpack後,沒有自動刷新怎麼幹活?

提及來,能作出像這樣的全棧刷新,大概也是得益於Express和Webpack都是javascript,能夠很容易地結合、協做的緣故吧。

(從新編輯自個人博客,原文地址:http://acgtofe.com/posts/2016...

相關文章
相關標籤/搜索