webpack 插件拾趣 (1) —— webpack-dev-server

結束了一季的忙碌,我這封筆已久的博客也終究該從春困的咒印中復甦,想來寫些實用易讀的做爲開篇,天然是最好不過。css

新開個 webpack 插件/工具介紹的文章系列,約莫每週更新一篇篇幅適中的文章聊以共勉,興許合適。html

本來指望每篇文章裏能夠介紹若干個插件,但鑑於部分插件略爲複雜,且單篇內容不想寫的脣焦舌敝惹人倦煩,因此像本文要介紹的 webpack-dev-server 就獨立一文了。node

迴歸主題,今天你或許會花上30分鐘的時間讀完本章,並掌握 webpack-dev-server 的使用方法、理清一些容易困惑的配置(諸如 publicPath)或概念(如HMR)webpack

另外,本章涉及的相關用例,能夠在個人github(https://github.com/VaJoy/webpack-plugins/tree/master/char1)上下載到。git

一. webpack-dev-server 他爹和他爹的朋友github

咱們並不急着把 webpack-dev-server 直接拉出來介紹一通,咱們先了解下他的兩位長輩 —— 他爹 webpack-dev-middleware,以及他爹的朋友 webpack-hot-middlewareweb

他們三人有着某些親密的聯繫,很多讀者可能會對其身份存在認知混亂,因此頗有必要按輩分次序來分別介紹。express

 

1.1 webpack-dev-middlewarejson

假設咱們在服務端使用 express 開發一個站點,同時也想利用 webpack 對靜態資源進行打包編譯,那麼在開發環節,每次修改完文件後,都得先執行一遍 webpack 的編譯命令,等待新的文件打包到本地,再作進一步調試。雖然我們能夠利用 webpack 的 watch mode 來監聽變動、自動打包,但等待 webpack 從新執行的過程每每很耗時。後端

而 webpack-dev-middleware 的出現很好地解決了上述問題 —— 做爲一個 webpack 中間件,它會開啓 watch mode 監聽文件變動,並自動地在內存中快速地從新打包、提供新的 bundle。

說白了就是 —— 自動編譯(watch mode)+速度快(所有走內存)

webpack-dev-middleware 的配置與使用其實很輕鬆,咱們經過一個很是簡單的項目來示例(能夠點這裏獲取):

PROJECT
│  app.js  //應用入口文件
│  express.config.js   // express 服務啓動配置
│  package.json
│  webpack.config.js  // webpack 配置
│  
└─src
    ├─html
    │      index.html  //首頁
    │      
    └─js
        └─page
                index.js  //首頁腳本模塊

它的 webpack.config.js 配置文件以下:

module.exports = {
    entry: './app.js',
    output: {
        publicPath: "/assets/",
        filename: 'bundle.js',
        //path: '/'   //只使用 dev-middleware 的話能夠忽略本屬性
    }
};

這裏有一個很是關鍵的配置 —— publicPath,熟悉 webpack 的同窗都知道,它是生成的新文件所指向的路徑,能夠用於模擬 CDN 資源引用。

打個比方,當咱們使用 url-loader 來處理圖片時,把 publickPath 設爲「http://abcd/assets/」,則最終打包後,樣式文件裏所引用的圖片地址會加上這個前綴:

/**-------------webpack配置項--------------**/
    output: {
        publicPath: "http://abcd/assets/",   //模擬CDN地址
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist/')
    },
    module: {
        rules: [{
                test: /\.css$/,
                loader: ['style-loader', 'css-loader']
            },
            {
                test: /\.(png|jpg|gif)$/,
                loader: 'url-loader'
            }]
    }

/**-------------頁面引入的樣式模塊 index.css--------------**/
section{
    width:300px;
    height: 300px;
    background-image: url(a.jpg);
}

打包後(dist/bundle.js 裏的樣式執行效果)

固然若是你沒把資源(好比這張md5化後的圖片)託管到CDN上,是請求不到的,不過經過Fiddler配置代理映射,能夠解決這個問題。

然而,在使用 webpack-dev-middleware (或其它走內存的工具)的狀況下,publicPath 只建議配置相對路徑 —— 由於 webpack-dev-middleware 在使用的時候,也須要再配置一個 publicPath(見下文 express.config.js 的配置),用於標記從內存的哪一個路徑去存放和查找資源,這意味着 webpack-dev-middleware 的 publicPath 必須是相對路徑。

而若是 webpack.config.js 裏的 publicPath 跟 webpack-dev-middleware 的 publicPath 不一致的話(好比前者配置了 http 的路徑),會致使資源請求到了內存外的地方去了(本地也沒這個文件,也無法走 Fiddler 代理來解決),從而返回404~

若是上面這段話瞧着糊塗,建議暫時擱置它,後續回過頭再來咀嚼,咱們先了解下所謂的「webpack-dev-middleware 的 publicPath」是什麼。

以下是 express.config.js 文件:

const path = require('path');
const express = require("express");
const ejs = require('ejs');
const app = express();
const webpack = require('webpack');
const webpackMiddleware = require("webpack-dev-middleware");
let webpackConf = require('./webpack.config.js');

app.engine('html', ejs.renderFile);
app.set('views', path.join(__dirname, 'src/html'));
app.set("view engine", "html");

var compiler = webpack(webpackConf);

app.use(webpackMiddleware(compiler, {  //使用 webpack-dev-middleware
    publicPath: webpackConf.output.publicPath  //保持和 webpack.config.js 裏的 publicPath 一致
}));

app.get("/", function(req, res) {
    res.render("index");
});

app.listen(3333);

可見 webpack-dev-middleware 的使用語法其實就這麼簡練,不外乎是:

var webpackMiddleware = require("webpack-dev-middleware");

app.use(webpackMiddleware(webpack(webpackConfig), options));

其中 options 是 webpack-dev-middleware 的配置對象,詳盡的可選項可參考官方文檔,限於篇幅,此處只介紹 publicPath —— 它用於決定 webpack 打包編譯後的文件,要存放在內存中的哪個虛擬路徑,並提供一個 SERVER,將路徑和文件映射起來(即便它們都是虛擬的,但依舊可請求的到)

當前的例子,是將內存路徑配置爲 /assets/,這意味着打包後的 bundle.js 會存放在虛擬內存路徑 SERVERROOT/assets/ 下(這裏的「SERVERROOT」實際上即 html 文件的訪問路徑),也意味着咱們能夠直接在 src/html/index.html 中經過 src='assets/bundle.js' 的形式引用和訪問內存中的 bundle 文件:

<body>
    <div></div>
    <script src="assets/bundle.js"></script>
</body>

咱們執行一遍 node express.config,而後訪問 http://localhost:3333,便能正常訪問頁面、請求和執行 bundle.js:

同時,只要咱們修改了頁面的腳本模塊(好比 src/js/index.js),webpack-dev-middleware 便會自行從新打包到內存,替換掉舊的 bundle,咱們只須要刷新頁面便可看到剛纔的變動。

這裏寫個關於 webpack-dev-middleware 的小 tips:

1. webpack-dev-middleware 配置項裏的 publicPath 要與 webpack.config 裏的 output.publicPath 保持一致(而且只能是相對路徑),否則會出現問題;
2. 使用 webpack-dev-middleware 的時候,其實能夠徹底無視 webpack.config 裏的 output.path,甚至不寫也能夠,由於走的純內存,output.publicPath 纔是實際的 controller;
3. publicPath 配置的相對路徑,實際是相對於 html 文件的訪問路徑。

1.2 HMR

機智的小夥伴們在讀完 webpack-dev-middleware 的介紹後,會洞悉出它的一處弱點 —— 雖然 webpack-dev-middleware 會在文件變動後快速地從新打包,可是每次都得手動刷新客戶端頁面來訪問新的內容,仍是略爲麻煩。這是由於 webpack-dev-middleware 在應用執行的時候,沒辦法感知到模塊的變化。

那麼是否有辦法可讓頁面也能自動更新呢?webpack-hot-middleware 即是幫忙填這個坑的人,因此我在前文稱之爲 —— webpack-dev-middleware 的好朋友。

webpack-hot-middleware 提供的這種能力稱爲 HMR,因此在介紹 webpack-hot-middleware 以前,咱們先來科普一下 HMR。

HMR 即模塊熱替換(hot module replacement)的簡稱,它能夠在應用運行的時候,不須要刷新頁面,就能夠直接替換、增刪模塊。

webpack 能夠經過配置 webpack.HotModuleReplacementPlugin 插件來開啓全局的 HMR 能力,開啓後 bundle 文件會變大一些,由於它加入了一個小型的 HMR 運行時(runtime),當你的應用在運行的時候,webpack 監聽到文件變動並從新打包模塊時,HMR 會判斷這些模塊是否接受 update,若容許,則發信號通知應用進行熱替換。

這裏說起的「判斷模塊是否接受 update」是指判斷模塊裏是否執行了 module.hot.accept(), 這裏舉個小例子:

如圖,白色的部分是編譯後的模塊依賴樹,這時候咱們修改了 B 模塊,致使 B 模塊以及依賴它的 A 模塊都出現了變化(綠色部分)

模塊變動的時候,webpack 會順着依賴樹一層一層往上冒泡,查詢哪一個模塊是接受 update 的,查詢到了則終止冒泡,並通知 SERVER 更新其爬過的模塊。

假設咱們把 module.hot.accept() 放在 B 模塊執行,則 webpack 會查找到 B` 模塊的變動就中止繼續往上冒泡查找了(A`是不容許變動的模塊)—— 若是 B 的內容變動,是直接在 B 模塊調用的,那頁面就能直接展現出新的內容出來,這樣效率也高(繞過了A模塊);但若是 B 的內容,其實是要通過 A 來調用,才能在頁面上展現出來,那此時頁面就不會刷新(即便 B 的內容變了)

說白了就是 module.hot.accept() 放的好,就能夠繞過一些沒必要要的模塊變動檢查來提高效率,不過對於懶人來講,直接置於最頂層的模塊(好比入口模塊)最爲省心。

關於更多的 HMR 的知識點,能夠參考官方文檔

1.3 webpack-hot-middleware 

聊完了 HMR,咱們回頭瞭解下 webpack-hot-middleware 的使用。

咱們試着對前文使用的項目來作一番改造 —— 引入 webpack-hot-middleware 來提高開發體驗。

首先往 express.config.js 加上一小段代碼:

app.engine('html', ejs.renderFile);
app.set('views', path.join(__dirname, 'src/html'));
app.set("view engine", "html");

var compiler = webpack(webpackConf);

app.use(webpackMiddleware(compiler, {
    publicPath: webpackConf.output.publicPath
}));

//添加的代碼段,引入和使用 webpack-hot-middleware
app.use(require("webpack-hot-middleware")(compiler, {
    path: '/__webpack_hmr'
}));

app.get("/", function(req, res) {
    res.render("index");
});

app.listen(3333);

即在原先的基礎上引入了 webpack-hot-middleware:

app.use(require("webpack-hot-middleware")(webpackCompiler, options));

這裏的 options 是 webpack-hot-middleware 的配置項,詳細見官方文檔,這裏我們只填一個必要的 path —— 它表示 webpack-hot-middleware 會在哪一個路徑生成熱更新的事件流服務,且訪問的頁面會自動與這個路徑經過 EventSource 進行通信,來拉取更新的數據從新粉飾本身。

這裏要了解下,實際上 webpack-hot-middleware 最大的能力,是讓 SERVER 可以和 HMR 運行時進行通信,從而對模塊進行熱更新。

而後是 webpack.config.js 文件:

const path = require('path');
const webpack = require('webpack');
module.exports = {
    entry: ['webpack-hot-middleware/client', './app.js'],  //修改點1
    output: {
        publicPath: "/assets/",
        filename: 'bundle.js'
    },
    plugins: [  //修改點2
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()   //出錯時只打印錯誤,但不從新加載頁面
    ]
};

首先是 entry 裏要多加上 'webpack-hot-middleware/client',此舉是與 server 建立鏈接。

接着加上兩個相關的插件來打通 webpack HMR 的任督二脈,其中的 webpack.HotModuleReplacementPlugin 咱們在上一節說起過,它是 HMR 的功能提供者。

最後一步很重要,不少新手容易漏掉。咱們須要在入口文件 app.js 里加上一小段代碼:

import {init} from './src/js/page/index';

//灰常重要,知會 webpack 容許此模塊的熱更新
if (module.hot) {
    module.hot.accept();
}

init();

此處的 module.hot.accept() 是知會 webpack 接受此模塊的 HMR update,在上一節已經說起屢次。

補充好上述的代碼,執行 node express.config 並訪問 http://localhost:3333,以後的模塊修改,都會自動打包並更新客戶端頁面模塊:

1.4 webpack-dev-server

雖然 webpack-dev-middleware + webpack-hot-middleware 的組合爲開發過程提供了便利,但它們僅適用於服務側開發的場景。

不少時候咱們僅僅對客戶端頁面作開發,沒有直接的 server 來提供支持,這時候就須要 webpack-dev-server 來解囊相助了。

顧名思義,webpack-dev-server 相對前兩個工具多了個「server」,實際上它的確也是在 webpack-dev-middleware 的基礎上多套了一層殼來提供 CLI 及 server 能力(這也是爲什麼我稱 webpack-dev-middleware 是 webpack-dev-server 他爹)

此處依舊以一個簡單的項目來展現如何配置、使用 webpack-dev-server,你能夠點這裏獲取相關代碼。

脫離了 express,咱們再也不需求配置後端腳本,不過對於 webpack.config.js,須要多加一個名爲「devServer」的  webpack-dev-server 配置項:

const path = require('path');
module.exports = {
    entry: './app.js',
    output: {
        publicPath: "/assets/",
        filename: 'bundle.js'
    },
    devServer: { //新增配置項
        contentBase: path.join(__dirname, "src/html"),
        port: 3333
    }
};

其中 devServer.port 表示 SERVER 的監聽端口,即運行後咱們能夠經過 http://localhost:3333 來訪問應用;

而 devServer.contentBase 表示 SERVER 將從哪一個目錄去查找內容文件(即頁面文件,好比 HTML)

確保安裝好 webpack-dev-server 後執行其 CLI 命令來召喚支持熱更新的 SERVER:

webpack-dev-server

接着訪問 http://localhost:3333,彷佛便能得到前文 webpack-dev-middleware + webpack-hot-middleware 的熱更新能力~

不過事實並不是如此,雖然在咱們修改模塊後,頁面的確自動刷新了。但截止此處,webpack-dev-server 跑起來其實只至關於捎上了 SERVER 的 webpack-dev-middleware,而沒有 HMR —— 在咱們修改應用模塊後,頁面是整個刷新了一遍,而並不是熱更新。

但願讀者們能夠記住,HMR 提供了局部更新應用模塊的能力,而不須要刷新整個應用頁面

這塊的驗證也很簡單,直接在 index.html 里加個 script 打印 Date.now() 便可,若刷新頁面,打印的值直接會變。 

要讓 webpack-dev-server 加上 HMR 的翅膀,其實就得像前面 webpack-hot-middleware 的配置那樣,把 HMR 相關的東西統統加上,同時將 devServer.hot 設爲 true:

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
    entry: './app.js',
    output: {
        publicPath: "/assets/",
        filename: 'bundle.js'
    },
    devServer: {
        contentBase: path.join(__dirname, "src/html"),
        port: 3333,
        hot: true  // 讓 dev-server 開啓 HMR
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()  //讓 webpack 啓動全局 HMR
    ]
};
// 入口文件 app.js

import {init} from './src/js/page/index';
if (module.hot) {
    // 知會 webpack 該模塊接受 HMR update
    module.hot.accept();
}
init();

這時候,再執行 webpack-dev-server,纔是正宗的有 HMR 加持的 SERVER。 

關於完整的 devServer 配置項可參考官方文檔,在文章的最後,咱們羅列幾個經常使用項作簡單介紹。

1. contentBase

即 SERVERROOT,如上方示例配置爲 「path.join(__dirname, "src/html")」,後續訪問 http://localhost:3333/index.html 時,SERVER 會從 src/html 下去查找 index.html 文件。

它能夠是單個或多個地址的形式:

contentBase: path.join(__dirname, "public")
//多個:
contentBase: [path.join(__dirname, "public"), path.join(__dirname, "assets")]

若不填寫該項,默認爲項目根目錄。

2. port

即監聽端口,默認爲8080。

3. compress

傳入一個 boolean 值,通知 SERVER 是否啓用 gzip。

4. hot

傳入一個 boolean 值,通知 SERVER 是否啓用 HMR。

5. https

能夠傳入 true 來支持 https 訪問,也支持傳入自定義的證書:

https: true
//也能夠傳入一個對象,來支持自定義證書
https: {
  key: fs.readFileSync("/path/to/server.key"),
  cert: fs.readFileSync("/path/to/server.crt"),
  ca: fs.readFileSync("/path/to/ca.pem"),
}

6. proxy

代理配置,適用場景是,除了 webpack-dev-server 的 SERVER(SERVER A) 以外,還有另外一個在運行的 SERVER(SERVER B),而咱們但願能經過 SERVER A 的相對路徑來訪問到 SERVER B 上的東西。

舉個例子:

    devServer: {
        contentBase: path.join(__dirname, "src/html"),
        port: 3333,
        hot: true,
        proxy: {
            "/api": "http://localhost:5050"
        }
    }

運行 webpack-dev-server 後,你若訪問 http://localhost:3333/api/user,則至關於訪問 http://localhost:5050/api/user。

更多可行的 proxy 配置見 https://webpack.js.org/configuration/dev-server/#devserver-proxy,這裏不贅述。

7. publicPath

如同 webpack-dev-middleware 的 publicPath 同樣,表示從內存中的哪一個路徑去存放和檢索靜態文件。

不過官方文檔有一處錯誤須要堪正 —— 當沒有配置 devServer.publicPath 時,默認的 devServer.publicPath 並不是根目錄,而是 output.publicPath:

這也是爲什麼我們的例子裏壓根沒寫 devServer.publicPath,但還能正常請求到 https://localhost:3333/assets/bundle.js。

8. setup

webpack-dev-server 的服務應用層使用了 express,故能夠經過 express app 的能力來模擬數據回包,devServer.setup 方法就是幹這事的:

    devServer: {
        contentBase: path.join(__dirname, "src/html"),
        port: 3333,
        hot: true,
        setup(app){  //模擬數據
            app.get('/getJSON', function(req, res) {
                res.json({ name: 'vajoy' });
            });
        }
    }

而後咱們能夠經過請求 http://localhost:3333/getJSON 來取得模擬的數據:

相關文章
相關標籤/搜索