在找一份相對完整的Webpack項目配置指南麼?這裏有

 

Webpack已經出來好久了,相關的文章也有不少,然而比較完整的例子卻不是不少,讓不少新手不知如何下腳,下腳了又遍地坑javascript

說實話,官方文檔是蠻亂的,並且有些仍是錯的錯的。。不少配置問題只有爬過坑才知道css

本文首先介紹Webpack(3)的一些基礎知識,而後以一個已經完成的小Demo,逐一介紹如何在項目中進行配置html

該Demo主要包含編譯Sass/ES6,提取(多個)CSS文件,提取公共文件,模塊熱更新替換,開發與線上環境區分,使用jQuery插件的方式、頁面資源引入路徑自動生成(可指定生成位置),熱更新編譯模版文件自動生成webpack服務器中的資源路徑,編寫一個簡單的插件,異步加載模塊 等基礎功能前端

應該能幫助你們更好地在項目中使用Webpack3來管理前端資源java

本文比較囉嗦,能夠直接看第四部分Webpack3配置在Demo中的應用,或者直接去Fork這個Demo邊看邊玩node

 

Webpack已升級爲v4版本,優化以後性能提高好幾倍,請移步這個 webpack4項目配置Demo,以及 這篇升級優化點 python

 

首先,學習Webpack,仍是推薦去看官方文檔,仍是挺全面的,包括中文的和英文的,以及GitHub上關於webpack的項目issues,還有就是一些完整了例子,最後就是得本身練手配置,才能在過程當中掌握好這枯燥的配置。react

 

 

一 、爲何要用Webpack

首先,得知道爲何要用webpackjquery

前端本能夠直接HTML、CSS、Javascript就上了,不過若是要處理文件依賴、文件合併壓縮、資源管理、使用新技術改善生活的時候,就得利用工具來輔助了。webpack

以往有常見的模塊化工具RequireJS,SeaJS等,構建工具Grunt、Gulp等,新的技術Sass、React、ES六、Vue等,要在項目中使用這些東西,不用工具的話就略麻煩了。

其實簡單地說要聚焦兩點:模塊化以及自動構建。

模塊化可使用RequireJS來處理依賴,使用Gulp來進行構建;也可使用ES6新特性來處理模塊化依賴,使用webpack來構建

兩種方式都狠不錯,但潮流所驅,後者變得越來越強大,固然也不是說後者就替代了前者,只是大部分狀況下,後者更好

 

2、什麼是Webpack

如其名,Web+Pack 即web的打包,主要用於web項目中打包資源進行自動構建。

Webpack將全部資源視爲JS的模塊來進行構建,因此對於CSS,Image等非JS類型的文件,Webpack會使用相應的加載器來加載成其可識別的JS模塊資源

經過配置一些信息,就能將資源進行打包構建,更好地實現前端的工程化

 

3、Webpack的基礎配置

能夠認爲Webpack的配置是4+n模式,四個基本的 entry(入口設置)output(輸出設置)loader(加載器設置)、plugin(插件設置),而後加上一些特殊功能的配置。

使用Webpack首先須要安裝好NodeJS

node -v
npm -v

確保已經可使用node,使用NPM包管理工具來安裝相應依賴包(網絡環境差可使用淘寶鏡像CNPM來安裝)

npm install -g cnpm --registry=https://registry.npm.taobao.org
cnpm -v

全局安裝好webpack包

npm i -g webpack
webpack -v

 

1. webpack的配置方式主要有三種

1. 經過cli命令行傳入參數 

webpack ./src.js -o ./dest.js --watch --color 

2. 經過在一個配置文件設置相應配置,導出使用

// ./webpack.config.js文件
module.exports = {
   context: ... entry: { }, output: { } };
// 命令行調用(不指定文件時默認查找webpack.config.js) webpack [--config webpack.config.js]

3. 經過使用NodeJS的API配置

這個和第二點有點相似,區別主要是第二種基本都是使用{key: value}的形式配置的,API則主要是一些調用

另外,某些插件的在這兩種方式的配置上也有一些區別

 

最經常使用的是第二種,其次第三種,第一種不太建議單獨使用(由於相對麻煩,功能相對簡單)

 

2. 常見的幾個配置屬性

1. context  絕對路徑

通常當作入口文件(包括但不限於JS、HTML模板等文件)的上下文位置,

默認使用當前目錄,不過建議仍是填上一個

// 上下文位置
context: path.resolve(__dirname, 'static')

2. entry  模塊入口文件設置

能夠接受字符串表示一個入口文件,不過通常來講是多頁應用多,就設置成每頁一個入口文件得了

好比home對應於一個./src/js/home模塊,這裏的key會被設置成webpack的一個chunk,即最終webpack會又三個chunkname:home | detail | common

也能夠對應於多個模塊,用數組形式指定,好比這裏把jquery設置在common的chunk中

也能夠設置成匿名函數,用於動態添加的模塊

// 文件入口配置
    entry: {
        home: './src/js/home',
        detail: './src/js/detail',
        // 提取jquery入公共文件
        common: ['jquery']
    },

3. resolve 處理資源的查找引用方式

如上方實際上是省略了後JS綴,又好比想在項目中引入util.js 能夠省略後綴

import {showMsg} from './components/util';
// 處理相關文件的檢索及引用方式
    resolve: {
        extensions: ['.js', '.jsx', '.json'],
        modules: ['node_modules'],
        alias: {

        }
    },

4. output 設置文件的輸出

最基礎的就是這三個了

path指定輸出目錄,要注意的是這個目錄影響範圍是比較大,與該chunk相關的資源生成路徑是會基於這個路徑的

filename指定生成的文件名,可使用[name] [id]來指定相應chunk的名稱,如上的home和detail,用[hash]來指定本次webpack編譯的標記來防緩存,不過建議是使用[chunkhash]來依據每一個chunk單獨來設置,這樣不改變的chunk就不會變了

hash放在?號以後的好處是,不會生成新的文件(只是文件內容被更改了),同時hash會附在引用該資源的URL後(如script標籤中的引用)

publicPath指定所引用資源的目錄,如在html中的引用方式,建議設置一個

 

// 文件輸出配置
    output: {
        // 輸出所在目錄
        path: path.resolve(__dirname, 'static/dist/js'),
        filename: '[name].js?[chunkhash:8]'// 設置文件引用主路徑
        publicPath: '/public/static/dist/js/'
    }

5.devtool指定sourceMap的配置

若是開啓了,就能夠在瀏覽器開發者工具查看源文件

// 啓用sourceMap
    devtool: 'cheap-module-source-map',

好比這裏就是對應的一個source Map,建議在開發環境下開啓,幫助調試每一個模塊的代碼

這個配置的選項是滿多的,並且還能夠各類組合,按照本身的選擇來吧

6. module指定模塊如何被加載

經過設置一些規則,使用相應的loader來加載

主要就是配置modulerules規則組,經過use字段指定loader,若是隻有一個loader,能夠直接用字符串,loader要設置options的就換成數組的方式吧

或者使用多個loader的時候,也用數組的形式,規則不要用{ }留空,在windows下雖然正常,但在Mac下會報錯提示找不到loader

多個loader遵循從右到左的pipe 的方式,以下 eslint-loader是先於babel-loader執行的

經過excludeinclude等屬性再肯定規則的匹配位置

// 模塊的處理配置,匹配規則對應文件,使用相應loader配置成可識別的模塊
    module: {
        rules: [{
           test: /\.css$/,
           use: 'css-loader'
        }, {
            test: /\.jsx?$/,
            // 編譯js或jsx文件,使用babel-loader轉換es6爲es5
            exclude: /node_modules/,
            use: [{
                loader: 'babel-loader',
                options: {

                }
            }, {
                loader: 'eslint-loader'
            }]
        }

7.  plugins設置webpack配置過程當中所用到的插件

好比下方爲使用webpack自帶的提取公共JS模塊的插件

// 插件配置
    plugins: [
        // 提取公共模塊文件
        new webpack.optimize.CommonsChunkPlugin({
            chunks: ['home', 'detail'],
            filename: '[name].js',
            name: 'common'
        }),
       new ...

]

這就是webpack最基礎的東西了,看起來內容不多,固然還有其餘不少,但複雜的地方在於如何真正去使用這些配置

 

4、Webpack配置在Demo中的應用

下面以一個相對完整的基礎Demo着手,介紹一下幾個基本功能該如何配置

Demo項目地址   建議拿來練練

 

1. 搭建個服務器

既然是Demo,至少就得有一個服務器,用node來搭建一個簡單的服務器,處理各類資源的請求返回

新建一個服務器文件server.js,以及頁面文件目錄views,其餘資源文件目錄public

服務器文件很簡單,請求什麼就返回什麼,外加了一個gzip的功能

let http = require('http'),
    fs = require('fs'),
    path = require('path'),
    url = require('url'),
    zlib = require('zlib');

http.createServer((req, res) => {
    let {pathname} = url.parse(req.url),
        acceptEncoding = req.headers['accept-encoding'] || '',
        referer = req.headers['Referer'] || '',
        raw;

    console.log('Request: ', req.url);

    try {
        raw = fs.createReadStream(path.resolve(__dirname, pathname.replace(/^\//, '')));

        raw.on('error', (err) => {
            console.log(err);

            if (err.code === 'ENOENT') {
                res.writeHeader(404, {'content-type': 'text/html;charset="utf-8"'});
                res.write('<h1>404錯誤</h1><p>你要找的頁面不存在</p>');
                res.end();
            }
        });

        if (acceptEncoding.match(/\bgzip\b/)) {
            res.writeHead(200, { 'Content-Encoding': 'gzip' });
            raw.pipe(zlib.createGzip()).pipe(res);
        } else if (acceptEncoding.match(/\bdeflate\b/)) {
            res.writeHead(200, { 'Content-Encoding': 'deflate' });
            raw.pipe(zlib.createDeflate()).pipe(res);
        } else {
            res.writeHead(200, {});
            raw.pipe(res);
        }
    } catch (e) {
        console.log(e);
    }
}).listen(8088);

console.log('服務器開啓成功', 'localhost:8088/');

2. 設置基礎項目目錄

頁面文件假設採用每一類一個目錄,目錄下的tpl爲源文件,另一個爲生成的目標頁面文件

/public目錄下,基本配置文件就放在根目錄下,JS,CSS,Image等資源文件就放在/public/static目錄下

咱們要利用package.json文件來管理編譯構建的包依賴,以及設置快捷的腳本啓動方式,因此,先在/public目錄下執行 npm init

public/static/dist目錄用來放置編譯後的文件目錄,最終頁面引用的將是這裏的資源

public/static/imgs目錄用來放置圖片源文件,有些圖片會生成到dist中

public/static/libs目錄主要用來放置第三方文件,也包括那些不多改動的文件

public/static/src 用來放置js和css的源文件,相應根目錄下暴露一個文件出來,公共文件放到相應子目錄下(如js/componentsscss/util

 

最後文件結構看起來是這樣的,那就能夠開幹了

 

 

3. 開發和生產環境的Webpack配置文件區分

首先在項目目錄下安裝webpack吧

npm i webpack --save-dev

用Webpack來構建,在開發環境和生產環境的配置仍是有一些區別的,構建是耗時的,好比在開發環境下就不須要壓縮文件、計算文件hash、提取css文件、清理文件目錄這些輔助功能了,而能夠引入熱更新替換來加快開發時的模塊更新效率。

因此建議區分一下兩個環境,同時將二者的共同部分提取出來便於維護

NODE_ENV是nodejs在執行時的環境變量,webpack在運行構建期間也能夠訪問這個變量,因此咱們能夠在devprod下配置相應的環境變量

這個配置寫在package.json裏的scripts字段就行了,好比

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build:dev": "export  NODE_ENV=development && webpack-dev-server --config webpack.config.dev.js",
    "build:prod": "export NODE_ENV=production && webpack --config webpack.config.prod.js --watch "
  },

這樣一來,咱們就能夠直接用 npm run build:prod來執行生產環境的配置命令(設置了production的環境變量,使用prod.js)

直接用npm run build:dev來執行開發環境的配置命令(設置了development的環境變量,使用dev.js,這裏還使用了devServer,後面說)

注意這裏是Unix系統配置環境變量的寫法,在windows下,記得改爲 SET NODE_ENV=development&& webpack-dev-server.......(&&前不要空格)

 

而後就能夠在common.js配置文件中獲取環境變量

// 是否生產環境
    isProduction = process.env.NODE_ENV === 'production',

而後能夠在plugins中定義一個變量提供個編譯中的模塊文件使用

// 插件配置
    plugins: [
        // 定義變量,此處定義NODE_ENV環境變量,提供給生成的模塊內部使用
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify(process.env.NODE_ENV)
            }
        }),

這樣一來,咱們能夠在home.js中判斷是否爲開發環境來引入一些文件

// 開發環境時,引入頁面文件,方便改變頁面文件後及時模塊熱更新
if (process.env.NODE_ENV === 'development') {
    require('../../../../views/home/home.html');
}

 

而後咱們使用webpack-merge工具來合併公共配置文件和開發|生產配置文件

npm i webpack-merge --save-dev


merge = require('webpack-merge')

commonConfig = require('./webpack.config.common.js')


/**
 * 生產環境Webpack打包配置,整合公共部分
 * @type {[type]}
 */
module.exports = merge(commonConfig, {
    // 生產環境不開啓sourceMap
    devtool: false,

    // 文件輸出配置
    output: {
        // 設置文件引用主路徑
        publicPath: '/public/static/dist/js/'
    },

    // 模塊的處理配置

 

4. 設置公共模塊

公共模塊其實能夠分爲JS和CSS兩部分(若是有提取CSS文件的話)

在公共文件的plugin中加入

// 提取公共模塊文件
        new webpack.optimize.CommonsChunkPlugin({
            chunks: ['home', 'detail'],
            // 開發環境下須要使用熱更新替換,而此時common用chunkhash會出錯,能夠直接不用hash
            filename: '[name].js' + (isProduction ? '?[chunkhash:8]' : ''),
            name: 'common'
        }),

設置公共文件的提取源模塊chunks,以及最終的公共文件模塊名

公共模塊的文件的提取規則是chunks中的模塊公共部分,若是沒有公共的就不會提取,因此最好是在entry中就指定common模塊初始包含的第三方模塊,如jquery,react等

 // 文件入口配置
    entry: {
        home: './src/js/home',
        detail: './src/js/detail',
        // 提取jquery入公共文件
        common: ['jquery']
    },

5. 編譯ES6成ES5

要講ES6轉換爲ES5,固然首用babel了,先安裝loader及相關的包

npm i babel-core babel-loader babel-preset-env babel-polyfill babel-plugin-transform-runtime --save-dev

-env包主要用來配置語法支持度

-polyfill用來支持一些ES6拓展的但babel轉換不了的方法(Array.from Generator等)

-runtime用來防止重複的ES6編譯文件所需生成(能夠減少文件大小)

而後在/public根目錄下新建 .babelrc文件,寫入配置

{
    "presets": [
        "env"
    ],
    "plugins": ["transform-runtime"]
}

而後在common.js的配置文件中新增一條loader配置就好了,注意使用exclude排除掉不須要轉換的目錄,不然可能會出錯哦

{
            test: /\.jsx?$/,
            // 編譯js或jsx文件,使用babel-loader轉換es6爲es5
            exclude: /node_modules/,
            use: [{
                loader: 'babel-loader',
                options: {

                }
            }]
        }

 

 6. 編譯Sass成CSS,嵌入到頁面<style>標籤中,或將其提取出(多個)CSS文件來用<link>引入

sass的編譯node-sass須要python2.7的環境,先肯定已經安裝並設置了環境變量

npm i sass-loader node-sass style-loader css-loader --save-dev

相似的,設置一下loader規則

不過這裏要設置成使用提取CSS文件的插件設置了,由於它的disable屬性能夠快速切換是否提取CSS(這裏設置成生產環境才提取)

好好看這個栗子,其實分三步:設置(new)兩個實例,loader匹配css和sass兩種文件規則,在插件中引入這兩個實例

提取多個CSS文件實際上是比較麻煩的,但也不是不能夠,方法就是設置多個實例和對應的幾個loader規則

這裏把引入的sass當作是本身寫的文件,提取成一個文件[name].css,把引入的css當作是第三方的文件,提取成一個[name]_vendor.css,既作到了合併,也作到了拆分,目前還沒想到更好的方案

上面提到過,output的path設置成了/public/static/dist/js ,因此這裏的filename 生成是基於上面的路徑,能夠用../來更換生成的css目錄

[contenthash]是css文件內容的hash,在引用它的地方有體現

fallback表示不可提取時的代替方案,即上述所說的使用style-loader嵌入到<style>標籤

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


ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')

/ 對import 引入css(如第三方css)的提取
    cssExtractor = new ExtractTextWebpackPlugin({
        // 開發環境下不須要提取,禁用
        disable: !isProduction,
        filename: '../css/[name]_vendor.css?[contenthash:8]',
        allChunks: true
    })

    // 對import 引入sass(如本身寫的sass)的提取
    sassExtractor = new ExtractTextWebpackPlugin({
        // 開發環境下不須要提取,禁用
        disable: !isProduction,
        filename: '../css/[name].css?[contenthash:8]',
        allChunks: true
    });




// 插件配置
    plugins: [
        // 從模塊中提取CSS文件的配置
        cssExtractor,
        sassExtractor
    ]


    



module: {
        rules: [{
            test: /\.css$/,
            // 提取CSS文件
            use: cssExtractor.extract({
                // 若是配置成不提取,則此類文件使用style-loader插入到<head>標籤中
                fallback: 'style-loader',
                use: [{
                        loader: 'css-loader',
                        options: {
                            // url: false,
                            minimize: true
                        }
                    },
                    // 'postcss-loader'
                ]
            })
        }, {
            test: /\.scss$/,
            // 編譯Sass文件 提取CSS文件
            use: sassExtractor.extract({
                // 若是配置成不提取,則此類文件使用style-loader插入到<head>標籤中
                fallback: 'style-loader',
                use: [
                    'css-loader',
                    // 'postcss-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            sourceMap: true,
                            outputStyle: 'compressed'
                        }
                    }
                ]
            })
        }

這樣一來,若是在不一樣文件中引入不一樣的文件,生成的css可能長這樣

// ./home.js
import '../../libs/bootstrap-datepicker/datepicker3.css';

import '../../libs/chosen/chosen.1.0.0.css';

import '../../libs/layer/skin/layer.css';

import '../../libs/font-awesome/css/font-awesome.min.css';


import '../scss/detail.scss';




// ./detail.js
import '../../libs/bootstrap-datepicker/datepicker3.css';

import '../../libs/chosen/chosen.1.0.0.css';

import '../../libs/layer/skin/layer.css';

import '../scss/detail.scss';

// ./home.html
<link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet">
<link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet">
<link href="/public/static/dist/js/../css/home_vendor.css?12a314c8" rel="stylesheet">
<link href="/public/static/dist/js/../css/home.css?c196fc33" rel="stylesheet">




// ./detail.html
<link href="/public/static/dist/js/../css/common_vendor.css?66cb1f48" rel="stylesheet">
<link href="/public/static/dist/js/../css/common.css?618d2a04" rel="stylesheet">

能夠看到,公共文件也被提取出來了,利用HtmlWebpackPlugin就能將其置入了

另外,能夠看到這裏的絕對路徑,其實就是由於在output中設置了publicPath爲/public/static/dist/js/

 

固然了,也不是說必定得在js中引入這些css資源文件,你能夠直接在頁面中手動<link>引入第三方CSS

我這裏主要是基於模塊化文件依賴,以及多CSS文件的合併壓縮的考慮才用這種引入方式的

 

7. jQuery插件的引入方式 

 目前來講,jQuery及其插件在項目中仍是很經常使用到的,那麼就要考慮如何在Webpack中使用它

第一種方法,就是直接頁面中<script>標籤引入了,但這種方式不受模塊化的管理,好像有些不妥

第二種方法,就是直接在模塊中引入所須要的jQuery插件,而jQuery自己由Webpack插件提供,經過ProvidePlugin提供模塊可以使用的變量$|jQuery|window.jQuery

不過這種方法好像也有不妥,把全部第三方JS都引入了,可能會下降編譯效率,生成的文件也可能比較臃腫

npm i jquery --save



// 
plugins: [
    new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery',
            'window.jQuery': 'jquery'
        }),

]



// ./home.js


import '../../libs/bootstrap-datepicker/bootstrap-datepicker.js';
console.log('.header__img length', jQuery('.header__img').length);

第三種辦法,能夠在模塊內部直接引入jQuery插件,也能夠直接在頁面經過<script>標籤引入jQuery插件,而jQuery自己由Webpack的loader導出爲全局可用

上述ProvidePlugin定義的變量只能在模塊內部使用,咱們可使用expose-loader將jQuery設置爲全局可見

npm i expose-loader --save



// 添加一條規則
{
            test: require.resolve('jquery'),
            // 將jQuery插件變量導出至全局,提供外部引用jQuery插件使用
            use: [{
                loader: 'expose-loader',
                options: '$'
            }, {
                loader: 'expose-loader',
                options: 'jQuery'
            }]
        }

要注意在Webpack3中不能使用webpack.NamedModulesPlugin()來獲取模塊名字,它會致使expose 出錯失效(bug)

 

不過如今問題又來了,這個應該是屬於HtmlWebpackPlugin的不夠機智的問題,先說說它怎麼用吧

 

8. HtmlWebpackPlugin將頁面模板編譯成最終的頁面文件,包含JS及CSS資源的引用

第一個重要的功能就是生成對資源的引入了,第二個就是幫助咱們填入資源的chunkhash值防止瀏覽器緩存

這個在生產環境使用就好了,開發環境是不須要的

npm i html-webpack-plugin --save-dev


HtmlWebpackPlugin = require('html-webpack-plugin')


plugins: [

 // 設置編譯文件頁面文件資源模塊的引入
        new HtmlWebpackPlugin({
            // 模版源文件
            template: '../../views/home/home_tpl.html',
            // 編譯後的目標文件
            filename: '../../../../views/home/home.html',
            // 要處理的模塊文件
            chunks: ['common', 'home'],
            // 插入到<body>標籤底部
            inject: true
        }),
        new HtmlWebpackPlugin({
            template: '../../views/detail/detail_tpl.html',
            filename: '../../../../views/detail/detail.html',
            chunks: ['common', 'detail'],
            inject: true
        }),



]

使用方式是配置成插件的形式,想對多少個模板進行操做就設置多少個實例

注意template是基於context配置中的上下文的,filename是基於output中的path路徑的

// ./home_tpl.html

    <script src="/public/static/libs/magicsearch/jquery.magicsearch2.js"></script>
</body>



// ./home.html

<script src=/public/static/libs/magicsearch/jquery.magicsearch2.js></script>
<script type="text/javascript" src="/public/static/dist/js/common.js?cc867232"></script>
<script type="text/javascript" src="/public/static/dist/js/home.js?5d4a7836"></script>
</body>

它會編譯成這樣,然而,然而,要注意到這裏是有問題的

這裏有個jQuery插件,而Webpack使用expose是將jQuery導出到了全局中,咱們經過entry設置把jQuery提取到了公共文件common中

因此正確的作法是common.js文件先於jQuery插件加載

而這個插件只能作到在<head> 或<body>標籤尾部插入,咱們只好手動挪動一下<script>的位置

 

不過,咱們還能夠基於這個插件,再寫一個插件來實現自動提高公共文件 <script>標籤到最開始

HtmlWebpackPlugin運行時有一些事件

    html-webpack-plugin-before-html-generation
    html-webpack-plugin-before-html-processing
    html-webpack-plugin-alter-asset-tags
    html-webpack-plugin-after-html-processing
    html-webpack-plugin-after-emit
    html-webpack-plugin-alter-chunks

在編譯完成時,正則匹配到<script>標籤,找到所設置的公共模塊(可能設置了多個公共模塊),按實際順序提高這些公共模塊便可

完整代碼以下:

 1 // ./webpack.myPlugin.js
 2 
 3 
 4 let extend = require('util')._extend;
 5 
 6 
 7 // HtmlWebpackPlugin 運行後調整公共script文件在html中的位置,主要用於jQuery插件的引入
 8 function HtmlOrderCommonScriptPlugin(options) {
 9     this.options = extend({
10         commonName: 'common'
11     }, options);
12 }
13 
14 HtmlOrderCommonScriptPlugin.prototype.apply = function(compiler) {
15     compiler.plugin('compilation', compilation => {
16         compilation.plugin('html-webpack-plugin-after-html-processing', (htmlPluginData, callback) => {
17             // console.log(htmlPluginData.assets);
18 
19             // 組裝數組,反轉保證順序
20             this.options.commonName = [].concat(this.options.commonName).reverse();
21 
22             let str = htmlPluginData.html,
23                 scripts = [],
24                 commonScript,
25                 commonIndex,
26                 commonJS;
27 
28             //獲取編譯後html的腳本標籤,同時在原html中清除
29             str = str.replace(/(<script[^>]*>(\s|\S)*?<\/script>)/gi, ($, $1) => {
30                 scripts.push($1);
31                 return '';
32             });
33 
34             this.options.commonName.forEach(common => {
35                 if (htmlPluginData.assets.chunks[common]) {
36                     // 找到公共JS標籤位置
37                     commonIndex = scripts.findIndex(item => {
38                         return item.includes(htmlPluginData.assets.chunks[common].entry);
39                     });
40 
41                     // 提高該公共JS標籤至頂部
42                     if (commonIndex !== -1) {
43                         commonScript = scripts[commonIndex];
44                         scripts.splice(commonIndex, 1);
45                         scripts.unshift(commonScript);
46                     }
47                 }
48             });
49 
50             // 從新插入html中
51             htmlPluginData.html = str.replace('</body>', scripts.join('\r\n') + '\r\n</body>');
52 
53             callback(null, htmlPluginData);
54         });
55     });
56 };
57 
58 
59 module.exports = {
60     HtmlOrderCommonScriptPlugin,
61 };

 

而後,就能夠在配置中經過插件引入了

{HtmlOrderCommonScriptPlugin} = require('./webpack.myPlugin.js');


// HtmlWebpackPlugin 運行後調整公共script文件在html中的位置,主要用於jQuery插件的引入
        new HtmlOrderCommonScriptPlugin({
            // commonName: 'vendor'
        })

親測仍是蠻好用的,能夠應對簡單的需求了

 

9. 使用url-loader和file-loader和html-loader來處理圖片、字體等文件的資源引入路徑問題

這個配置開發環境和生產環境是不一樣的,先看看生產環境的,主要的特色是有目錄結構的設置,設置了一些生成的路徑以及名字信息

開發環境由於是使用了devServer,不須要控制目錄結構

npm i url-loader file-loader@0.10.0 html-loader --save-dev

這裏要注意的是file-loader就不要用0.10版本以上的了,會出現奇怪的bug,主要是下面設置的outputPath和publicPath和[path]會不按套路出牌

致使生成的頁面引用資源變成奇怪的相對路徑

rules: [{
            test: /\.(png|gif|jpg)$/,
            use: {
                loader: 'url-loader',
                // 處理圖片,當大小在範圍以內時,圖片轉換成Base64編碼,不然將使用file-loader引入
                options: {
                    limit: 8192,
                    // 設置生成圖片的路徑名字信息 [path]相對context,outputPath輸出的路徑,publicPath相應引用的路徑
                    name: '[path][name].[ext]?[hash:8]',
                    outputPath: '../',
                    publicPath: '/public/static/dist/',
                }
            }
        }, {
            test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/,
            use: [{
                loader: 'file-loader',
                options: {
                    // 設置生成字體文件的路徑名字信息 [path]相對context,outputPath輸出的路徑,publicPath相應引用的主路徑
                    name: '[path][name].[ext]?[hash:8]',
                    outputPath: '../',
                    publicPath: '/public/static/dist/',
                    // 使用文件的相對路徑,這裏先不用這種方式
                    // useRelativePath: isProduction
                }
            }],
        }, {
            test: /\.html$/,
            // 處理html源文件,包括html中圖片路徑加載、監聽html文件改變從新編譯等
            use: [{
                loader: 'html-loader',
                options: {
                    minimize: true,
                    removeComments: false,
                    collapseWhitespace: false
                }
            }]
        }]
    

比較生澀難懂,看個栗子吧

scrat.png是大於8192的,最終頁面引入會被替換成絕對路徑,而且帶有hash防止緩存,而輸出的圖片所在位置也是用着相應的目錄,便於管理

// ./home_tpl.html

        <img class="header__img" src="../../public/static/imgs/kl/scrat.png" width="200" height="200">


// ./home.html

        <img class=header__img src=/public/static/dist/imgs/kl/scrat.png?8ad54ef5 width=200 height=200>

若是換個小圖,就會替換成base64編碼了,在css中的引入也同樣

<img class=header__img src=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAA6CAYAAABrnUYFAAAaVElEQVR4Xu1df3wdVZX/npnkNUlbWn6sFhAELSgibSYtCIhagXXB0vdSsOwu4CKiXbAglk8zAVwloELzwoqIUsFFRXQVaiEvRcAfLD9EfqxtXtqKS239AQoFRUlb+tK+5N2znzNvJp1M34+ZeZOElLn/QPPuPffcc2e+c+75dQlxiyUQSyCWwChJgEaJbkw2lkAsgVg

 

再來看看開發環境的 

rules: [{
            test: /\.(png|gif|jpg)$/,
            // 處理圖片,當大小在範圍以內時,圖片轉換成Base64編碼,不然將使用file-loader引入
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }, {
            test: /\.(eot|svg|ttf|otf|woff|woff2)\w*/,
            // 引入文件
            use: 'file-loader'
        }]

 

 10. 模塊熱更新替換的正確姿式

在開發環境下,若是作到模塊的熱更新替換,效果確定是棒棒的。生成環境就先不用了

在最初的時候,只是作到了熱更新,並無作到熱替換,其實都是坑在做祟

熱更新,須要一個配置服務器,Webpack集成了devServer的nodejs服務器,配置一下它

// 開發環境設置本地服務器,實現熱更新
    devServer: {
        contentBase: path.resolve(__dirname, 'static'),
        // 提供給外部訪問
        host: '0.0.0.0',
        port: 8188,
        // 設置頁面引入
        inline: true
    },

正常的話,啓動服務應該就能夠了吧

webpack-dev-server --config webpack.config.dev.js

要記住,devServer編譯的模塊是輸出在服務器上的(默認根目錄),不會影響到本地文件,因此要在頁面上手動設置一下引用的資源

<script src="http://localhost:8188/common.js"></script>
<script src="http://localhost:8188/home.js"></script>

瀏覽器訪問,改動一下home.js文件,這時應該能夠看到頁面自動刷新,這就是熱更新了😁

固然了,熱更新還不夠,得作到熱替換,即頁面不刷新替換模塊

能夠呀,多配置一下

// 開發環境設置本地服務器,實現熱更新
    devServer: {
        ...
        // 設置熱替換
        hot: true,
        ...
    },



 // 插件配置
    plugins: [
        // 熱更新替換
        new webpack.HotModuleReplacementPlugin(),
]

再去瀏覽器試試,改個文件,正常的話應該也能看到

但就是一直停留在App hot update...不動了,驚不驚喜,意不意外

緣由是還沒在當前項目中安裝webpack-dev-server,HMR的消息接收不到,命令沒報錯只是由於在全局安裝了webpack有那命令

npm i webpack-dev-server --save-dev

再試試,然而你發現,纔剛開始編譯,就不停地重複編譯了

你得設置一下publicPath 好比

output: {
        publicPath: '/dist/js/',
    },

再試試,更改模塊,你又會發現頁面仍是從新刷新了

要善於用Preserve log來看看刷新以前發生了什麼

已經有進展了,這時HMR在獲取JSON文件時404了,並且訪問的域名端口是localhost:8088是咱們本身node服務器的端口

devServer的端口是8188的,看起來這JSON文件時devServer生成的,多是路徑被識別成相對路徑了

那就設置成絕對路徑吧

output: {
        // 設置路徑,防止訪問本地服務器相關資源時,被開發服務器認爲時相對其的路徑
        publicPath: 'http://localhost:8188/dist/js/',
    },

再來,恭喜 又錯了,跨域訪問

那就在devServer再配置一下header讓8088能夠訪問,能夠暴力一點設置*

devServer: {
       ...
        // 容許開發服務器訪問本地服務器的包JSON文件,防止跨域
        headers: {
            'Access-Control-Allow-Origin': '*'
        },
        ...
    },

再來,額😆呵呵,又從新刷新了

指明瞭模塊沒有被設置成accepted,那它就不知道要熱替換哪一個模塊了,只好整個刷新。

須要在模塊中設置一下,機智是冒泡型的,因此在主入口設置就好了,好比這裏的模塊入口home.js

// 設置容許模塊熱替換
if (module.hot) {
    module.hot.accept();
}

這就成功了,這裏建議的NamedModulesPlugin是用不了了,由於和espose-loader衝突了

 

是否是很囉嗦呢,總結一下

1. 在本項目總安裝webpack-dev-server

2. devServer配置中設置hot: true

3. plugins配置中設置new webpack.HotModuleReplacementPlugin() 

4. output配置中設置publicPath: 'http://localhost:8188/dist/js/'

5. devServer配置中設置header容許跨域訪問

6. 模塊中設置接受熱替換module.hot.accept()

7. 不要在命令行加參數 --hot 和 new webpack.HotModuleReplacementPlugin() 同時使用,會棧溢出錯誤,只用配置文件的就好了

 

另外,默認是隻能模塊熱替換,若是也想監聽頁面文件改變來實現HTML頁面的熱替換,該怎麼作呢

把HTML也當作模塊引入就好了(開發環境下),在以前已經使用了html-loader能處理html後綴資源的狀況下

// ./home.js

// 開發環境時,引入頁面文件,方便改變頁面文件後及時模塊熱更新
if (process.env.NODE_ENV === 'development') {
    require('../../../../views/home/home_tpl.html');
}

記得import不能放在if語句塊裏面,因此這裏用require來代替

有點奇怪,在最開始的時候,這樣是能實現熱替換的,但這段時間卻一直不行了,顯示已更新,但內容卻沒更新

只好暫時用第二步熱更新來替換,接收到改變時頁面自動刷新

//  ./home.js

// 開發環境時,引入頁面文件,方便改變頁面文件後及時模塊熱更新
if (process.env.NODE_ENV === 'development') {
    require('../../../../views/home/home_tpl.html');
}

// 設置容許模塊熱替換
if (module.hot) {
    module.hot.accept();

    // 頁面文件更新 自動刷新頁面
    module.hot.accept('../../../../views/home/home_tpl.html', () => {
        location.reload();
    });
}

 

11. 壓縮模塊代碼

壓縮JS代碼就用自帶的插件就好了

壓縮CSS代碼用相應的loader options

// 壓縮代碼
        new webpack.optimize.UglifyJsPlugin({
            sourceMap: true,
            compress: {
                warnings: false
            }
        }),

 

12. 異步加載模塊 require.ensure

異步加載模塊,在不少時候是須要的。好比在首頁的時候,不該該要求用戶就下載了其餘不須要的資源。

而webpack中異步加載模塊是比較方便的,主要是require.ensure這個方法

require.ensure(dependencies: String[], callback: function(require), chunkName: String)

好比,在home.html頁面中,我想點擊某個元素以後,再異步加載某個模塊來執行

// 添加一個模塊  ./async.js

// 這個模塊用於檢測異步加載
function log() {
    console.log('log from async.js');
}

export {
    log
};




// 在 ./home.js模塊中設置點擊以後異步引入

$('.bg-input').click(() => {
    console.log('clicked, loading async.js');

    require.ensure([], require => {
        require('./components/async').log();
        console.log('loading async.js done');
    });
});

能夠看到,點擊以後,異步請求了這個模塊

webpack 在編譯的時候分析在require.ensure中定義的依賴模塊,將其生成到一個新的chunk中(不在home.js裏),以後按需拉取下來

另外,要注意的是,若是模塊已經被引入了,那它是不會單獨被打包出去的

// require('./components/async2').log();

$('.bg-input').click(() => {
    console.log('clicked, loading async.js')

    require.ensure([], require => {

        require('./components/async2').log();
        require('./components/async1').log();
        console.log('loading async.js done');
    });
});

兩個依賴都會放到一塊兒,若是把註釋去掉的話,那異步的模塊就只有async-1.js了

require.ensure的第一個參數是依賴,這裏的依賴加載完成後,纔會執行回調函數(在裏頭咱們能夠再次設置依賴)

因此,若是隻是想加載一個模塊,咱們能夠直接這麼寫。可是,這只是下載了,它是執行不了的

$('.bg-input').click(() => {
    console.log('clicked, loading async.js')

    require(['./components/async1']);
});

因此通常來講,第一個參數更可能是用作回調裏模塊的依賴,通常執行的操做都是放到回調裏

第三個參數是定義這個chunk的名字,要同時在output中設置chunkFilename

// 文件輸出配置
    output: {
        // 異步加載模塊名
        chunkFilename: '[name].js'
    },



  require.ensure([], require => {
          ...

    }, 'async_chunk');        

 

13. 其餘配置

再來稍微配一下react的環境

npm i react react-dom babel-preset-react --save-dev

在home.js文件中加入

let React = require('react');
let ReactDOM = require('react-dom')

class Info extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            name: this.props.name || 'myName'
        };
    }

    showYear(e) {
        console.log(this);

        let elem = ReactDOM.findDOMNode(e.target);
        console.log('year ' + elem.getAttribute('data-year'));
    }

    render() {
        return <p onClick={this.showYear} data-year={this.props.year}>{this.state.name}</p>
    }
}

Info.defaultProps = {
    year: new Date().getFullYear()
};

ReactDOM.render(<Info />, document.querySelector('#box'));

修改.bablerc文件

{
    "presets": [
        "env",
        "react"
    ],
    "plugins": ["transform-runtime"]
}

 

 

其餘配置,好比eslint代碼檢查、postcss支持等就不在這說了,用到了就用相似的方式添加進去吧 

 

14. 自定義HtmlWebpackPlugin插件編譯模版文件生成的JS/CSS插入位置

HtmlWebpackPlugin主要用來編譯模版文件,生成新的頁面文件

new HtmlWebpackPlugin({
            template: '../../parent/parent_index_src.html',
            filename: '../../../../parent/parent_index.tpl',
            chunks: ['common', 'parent'],
            inject: true
        }),

通常來講會這樣用,能夠同時將JS資源與CSS資源插入到頁面中(可自動配hash值),很是方便

可是修改inject屬性只會不插入或插入到</head>或</body>標籤以前,自定義不了插入位置

上述第八點提到了利用插件來調整生成<script>標籤,其實還有更便捷的方法能夠實現:使用其支持的模版引擎

假設如今是smarty頁面,有個公共父模版文件,不少子頁面套用這個文件,那麼它能夠長成這個樣子

<!-- 父頁面  -->
<!DOCTYPE html>
<html>
    <head>
        <title>某個系統</title>
        <meta charset="utf-8">
        <meta lang="zh-CN">

        <% for(var key in htmlWebpackPlugin.files.css) { %>
        <link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[key] %>">
        <% } %>

        <{block name="page_css"}><{/block}>
    </head>
    <body>
        <section class="container">
            父頁面
        </section>

        <{block name="page_content"}><{/block}>

        <script src="/public/static/js/jquery.min.js"></script>

        <% for(var key in htmlWebpackPlugin.files.js) { %>
        <script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
        <% } %>

        <{block name="page_js"}><{/block}>
        <script src="http://localhost:8188/dist/js/common.js"></script>
        <script src="http://localhost:8188/dist/js/parent.js"></script>
    </body>
</html>
<!-- 子頁面 -->

<{extends file="../parent/parent_index.tpl"}>


<{block name="page_css"}>
<% for(var key in htmlWebpackPlugin.files.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[key] %>">
<% } %>
<{/block}>


<{block name="page_content"}>
<h1>子頁面</h1>
<div>

</div>
<{/block}>



<{block name="page_js"}>
<% for(var key in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
<% } %>
<{/block}>

這裏,爲了實現子頁面插入到父頁面以後,還能保持CSS與JS資源放在正確的位置,須要指定一個編譯後的生成位置

使用到了Webpack內置支持的ejs模版,並使用到了其htmlWebpackPlugin變量,裏面攜帶了本次編譯的一些信息,咱們能夠直接輸出來插入資源,而後再設置 inject: false就好了

下面是一個例子的輸出,更多的就去看文檔吧

"htmlWebpackPlugin": {
  "files": {
    publicPath : "", 
    "css": [],
    "js": [ "js/main.ae8647e767cd76e54693.bundle.js"],
    "chunks": {
      "main": {
        "size":23,
        "entry": "js/main.ae8647e767cd76e54693.bundle.js", 
        "css": [],
        "hash":"ae8647e767cd76e54693",
      }
    },
    manifest : ""
  },
  "options":{
        template : "C:\\dev\\webpack-demo\\node_modules\\.2.28.0@html-webpack-plugin\\lib\\loader.js!c:\\dev\\webpack-demo\\index.html",    
    filename : "index.html",    
    hash : false,    
    inject : false,    
    compile : true,    
    favicon : false,    
    minify : false,        
    cache : true,    
    showErrors : true,    
    chunks : ["main"],    
    excludeChunks : [],    
    title : "I am title",    
    xhtml : false    
    }
}

 

 15. 熱更新編譯模版文件自動生成webpack服務器中的資源路徑

熱更新時,webpack的devServer默認只會將模塊編譯到內存中,編譯到咱們設置的服務器裏,不會編譯生成到本地開發目錄中

這並不算什麼問題,問題是咱們須要在頁面中手動引入服務器的模塊,好比

<script src="http://localhost:8188/dist/js/common.js"></script>
        
        <script src="http://localhost:8188/dist/js/parent.js"></script>

使用熱更新時手動添加,不使用時手動刪掉才上傳代碼,這還好,可是,咱們有模版文件

假設模版文件爲a_src.html ,須要編譯成a.html,咱們實際項目中要訪問的文件是編譯後的a.html文件,而咱們只能在源文件a_src.html中作改動

使用熱更新的時候,並不能將源文件編譯寫到新文件上,咱們只能換着法子訪問源文件或者直接改動新文件並複製一份到源文件中,並且還得手動添加熱更新的服務器模塊路徑

太麻煩了,那就在熱更新的時候也編譯模版文件吧,使用HtmlWebpackHarddiskPlugin 插件自動生成資源引用路徑,同時在源文件的更改能夠自動編譯寫到新文件中

// 安裝
 npm install --save-dev html-webpack-harddisk-plugin



var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');


// 配合htmlWebpackPlugin使用,加上參數alwaysWriteToDisk
new HtmlWebpackPlugin({
            template: '../../main/protected/views/flow/a_src.html',
            filename: '../../../../main/protected/views/flow/a.htmll',
            chunks: ['a'],
            inject: false,
            alwaysWriteToDisk: true
        }),
        new HtmlWebpackPlugin({
            template: '../../main/protected/views/parent/parent_src.html',
            filename: '../../../../main/protected/views/parent/parent.html',
            chunks: ['common', 'parent'],
            inject: false,
            alwaysWriteToDisk: true
        }),
    // 調用
        new HtmlWebpackHarddiskPlugin()

而後在源模版文件裏,配合上一點的ejs模版生成出來就好了,能夠自動檢測是生成環境的路徑仍是開發環境的熱更新路徑 解放了勞動力

源模版文件:

<!-- 編譯後腳本 -->
        <% for(var key in htmlWebpackPlugin.files.js) { %>
        <script src="<%= htmlWebpackPlugin.files.js[key] %>"></script>
        <% } %>

development:

// 文件輸出配置
    output: {
        publicPath: 'http://localhost:8188/dist/js/',
    },
 <!-- 編譯後腳本 -->
        
        <script src="http://localhost:8188/dist/js/common.js"></script>
        
        <script src="http://localhost:8188/dist/js/parent.js"></script>

production:

// 文件輸出配置
    output: {
        publicPath: '/public/assets/dist/js/'
    },
 <!-- 編譯後腳本 -->
        
        <script src="/public/assets/dist/js/common.js?784109bb"></script>
        
        <script src="/public/assets/dist/js/parent.js?997487cf"></script>

 

 

16. 其餘常見問題整理

一個項目有多個webpack衝突的解決

若是一個項目中用多個webpack來編譯,並引入了多個文件,就會產生衝突了,這會致使webpack只會識別第一個引入的變量

這時候,須要配置output的jsonpFunction參數

// 文件輸出配置
    output: {
        // 輸出所在目錄
        path: path.resolve(__dirname, 'assets/dist/js'),
        // 開發環境使用熱更新,方便編譯,能夠直接不用hash
        filename: '[name].js',
        jsonpFunction: 'abcJSONP'
    },

 

Only one instance of babel-polyfill is allowed

引入多個polyfill致使衝突,不能重複引入

import 'babel-polyfill';

解決辦法是:引入的時候判斷一下(沒辦法,它本身沒判斷)

if (!global._babelPolyfill) {
   require('babel-polyfill');
}

 

 轉載請註明

相關文章
相關標籤/搜索