webpack實戰(一):真實專案中一個完整的webpack配置

大司馬愛學習發表於2019-03-04

字數:3700

閱讀時間:15分鐘

環境:webpack3.8.1

前言

前段時間,使用webpack查閱資料時發現要麼是入門級文章,要麼是如何優化打包速度或如何使用某個plugin、loader的文章。找不到一個在真實專案中的使用webpack的完整方案用以參考,所以花了許多精力去整合資料、檢視程式碼和踩坑。

因此我將自己摸索的一個配置方案,分享出來,希望能提供一點借鑑。不足之處,歡迎大夥指正。

說明一下,本文旨在講述思路和遇到的問題,不會涉及到基礎的講解。如果想了解基礎,這裡可以給大夥推薦兩個非常好的入門資料:

入門Webpack,看這篇就夠了:初步地瞭解webpack的用法已經簡單地練手。

webpack中文文件:這一版的官方文件,相對之前大家詬病已久的文件混亂問題有了很大的改善,最好不過的學習資料了。

正文

為增加程式碼的可讀性和維護性,我將配置拆分為以下五個配置檔案:

webpack.common.config.js 公共配置
webpack.dev.config.js 開發環境配置
webpack.prod.config.js 生產環境配置
webpack.dll.config.js 公共庫配置
webpack.alias.js 模組地址配置

為提升打包效率,我會將一些變化較小的程式碼和第三方庫都打包成公共庫,webpack.dll.config.js就是打包公共庫的配置檔案,如果其中的內容沒有變化,之後的打包不會再處理這些檔案了,極大地增加了打包效率。如果使用了較多第三方庫,強烈建議使用這種方式打包。

因為開發環境配置和生產環境配置有許多差異,因此分別做配置,分別對應著webpack.dev.config.jswebpack.prod.config.js配置檔案。然後提取其中的公共部分,這就是我們的公共配置檔案webpack.common.config.js

最後,筆者對每一個模組都做了別名配置,以解耦程式碼對程式碼目錄的依賴,對應著我們的webpack.alias.js配置檔案。

下面,我們就一起探討一下這五個配置檔案的具體內容。

1.webpack.common.config.js

公共配置,先上程式碼:

const wepackMerge = require('webpack-merge');
const Path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
const GTLoaderFilesPlugin = require('./plugins/gt-file-loader-plugin');

const ProdConfig = require('./webpack.prod.config');
const DevConfig = require('./webpack.dev.config');
const alias = require('./webpack.alias');
const dlls = require('./webpack.dll.config');

//根據條件處理相關配置
const genarateConfig = env => {
    //樣式loader
    let cssLoader = [{
        loader: 'css-loader',
        options: {
            sourceMap: true
        }
    }, {
        loader: 'postcss-loader',
        options: {
            ident: 'postcss',
            plugins: [
                require('postcss-cssnext')()
            ],
            sourceMap: true
        }
    }, {
        loader: 'less-loader',
        options: {
            sourceMap: true
        }
    }];
    let styleLoader = [{
        test: /\.(css|less)$/,
        use: env === 'prod' ? ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: cssLoader
        }) : [{
            loader: 'style-loader',
            options: {
                sourceMap: true
            }
        }].concat(cssLoader)
    }];

    //指令碼loader
    let jsLoader = [{
        test: /\.js$/,
        exclude: /(node_modules|bower_components|libs)/,
        use: [{
            loader: 'babel-loader'
        }].concat(env === 'dev' ? [{
            loader: 'eslint-loader'
        }] : [])
    }];

    //檔案處理loader
    let fileLoaderOptions = {
        useRelativePath: false,
        name: '[name]-[hash:5].[ext]'
    };
    if (env === 'prod') {
        fileLoaderOptions.limit = 10000;
    }
    let fileLoader = [{
        test: /\.(jpg|jpeg|png|icon)$/,
        use: [{
            loader: env === 'dev' ? 'file-loader' : 'url-loader',
            options: env === 'dev' ? fileLoaderOptions : Object.assign({}, fileLoaderOptions, {
                outputPath: '../dist/img'
            })
        }]
    }, {
        //解析字型檔案
        test: /\.(eot|svg|ttf|woff2?)$/,
        use: [{
            loader: env === 'dev' ? 'file-loader' : 'url-loader',
            options: env === 'dev' ? fileLoaderOptions : Object.assign({}, fileLoaderOptions, {
                outputPath: '../dist/fonts'
            })
        }]
    }, {
        //解析主頁面和頁面上的圖片
        test: /\.html$/,
        exclude: /(node_modules|bower_components)/,
        use: {
            loader: 'html-loader',
            options: {
                attrs: ['img:src', 'img:data-src'],
                minimize: true
            }
        }
    }];

    //webpack外掛
    let plugins = [];

    //組織第三方庫外掛
    for (let key in dlls.entry) {
        //組織DllReferencePlugin
        let dllPlugin = new Webpack.DllReferencePlugin({
            manifest: require('../dll/manifest/' + key + '.manifest.json')
        });
        plugins.push(dllPlugin);
    }

    //載入js
    plugins.push(new AddAssetHtmlPlugin({
        filepath: Path.join(__dirname, '../dll/*.js'),
        hash: true,
        includeSourcemap: false,
        publicPath: './dll/',
        outputPath: '../dist/dll/'
    }));

    //載入css
    plugins.push(new AddAssetHtmlPlugin({
        filepath: Path.join(__dirname, '../dll/*.css'),
        hash: true,
        typeOfAsset: 'css',
        includeSourcemap: false,
        publicPath: './dll/',
        outputPath: '../dist/dll/'
    }));

    //入口html外掛
    plugins.push(new HtmlWebpackPlugin({
        template: Path.join(__dirname, '../src/control.html'),
        filename: 'index.html',
        inject: true,
        chunks: ['vendor', 'example']
    }));

    //拷貝檔案
    plugins.push(new CopyWebpackPlugin([{
        // 第三方的字型檔案
        from: './dll/fonts',
        to: '../dist/fonts'
    }, {
        //表單頁面檔案
        from: './src/form/core/views',
        to: '../dist/core-views'
    }, {
        //表單頁面檔案
        from: './src/form/office/views',
        to: '../dist/office-views'
    }], {
        ignore: ['**/.svn/**']
    }));

    //友好提示外掛
    plugins.push(new FriendlyErrorsPlugin());

    //不打包預設載入項
    plugins.push(new Webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));

    //將載入項寫入loader.js中
    plugins.push(new GTLoaderFilesPlugin());

    let config = {
        devtool: 'source-map',
        output: {
            path: Path.join(__dirname, '../dist/'),
            filename: env === 'dev' ? '[name]-[hash:5].bundle.js' : '[name]-[chunkhash:5].bundle.js'
        },
        module: {
            rules: [].concat(styleLoader).concat(jsLoader).concat(fileLoader)
        },
        plugins: plugins,
        resolve: {
            alias: alias
        }
    };

    return config;
};

module.exports = env => {
    let config = env === 'dev' ? DevConfig : ProdConfig;
    let result = wepackMerge(genarateConfig(env), config);
    return result;
};

複製程式碼

入口

開發環境和生產環境皆使用這個配置檔案執行webpack,通過執行CLI命令時傳入的環境變數來區分。開發環境傳入dev,生產環境傳入prod,藉助於npm的scripts命令,可更便利地實現,程式碼如下:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --env prod --config build/webpack.common.config.js",
    "server": "webpack-dev-server --env dev --config build/webpack.common.config.js --open",
    "dll": "webpack --config build/webpack.dll.config.js"
  }
複製程式碼

執行npm run server就可以開啟開發環境了。

公共配置中,生成配置程式碼如下:

module.exports = env => {
    let config = env === 'dev' ? DevConfig : ProdConfig;
    let result = wepackMerge(genarateConfig(env), config);
    return result;
};
複製程式碼

使用webpack-merge外掛,根據傳入的引數將不同的配置與公共配置進行融合。注意,由於loader是倒序執行的,所以loader相關的配置無法使用這個方式融合,只能在程式碼中自行處理。這裡genarateConfig就是處理公共配置的函式。

樣式

生產環境:less-loader→postcss-loader→css-loader→extract-text-webpack-plugin

開發環境:less-loader→postcss-loader→css-loader→style-loader

less-loader:webpack自帶loader,預編譯less檔案,將less語法編譯成css語法。

postcss-loader:webpack自帶loader,css轉換工具,自動新增瀏覽器字首、壓縮css、支援未來語法。

css-loader:webpack自帶loader,編譯css。

extract-text-webpack-plugin:webpack自帶外掛,extract()函式會返回一個loader,輸出獨立樣式檔案,一般用於生產環境。

style-loader:webpack自帶loader,將樣式以style標籤的方式直接插入到html文件中。

注意: postcss-loader中引入postcss-cssnext模組,可以支援未來語法。所有loader設定sourceMap: true才能在控制檯看到樣式原始碼。

指令碼

生產環境:babel-loader

開發環境:eslint-loader→babel-loader

eslint-loader:webpack自帶loader,需要依賴Eslint工具,做靜態程式碼檢查只用。可以配合webpack-dev-server的overlay使用。

babel-loader:編譯js,相容新特性。

檔案

生產環境:html-loader→url-loader

開發環境:html-loader→file-loader

html-loader:webpack自帶loader,編譯html檔案,將其中載入的圖片等資源作為模組處理。

url-loader:webpack自帶loader,解析圖片、字型等檔案資源,可以將超過限制的資源解析成base64編碼字串,達到減少請求數的優化效果。

file-loader:webpack自帶loader,同url-loader,只是不會將檔案解析成base64編碼字串。

外掛

DllReferencePlugin:webpack自帶外掛,配合公共庫webpack.dll.config.js使用。

add-asset-html-webpack-plugin:在生成的html頁面中自動插入資源,這裡使用它引入了公共庫中的js和css資源。

html-webpack-plugin:根據模板生成html檔案,預設支援ejs模板語法。需要注意:與html-loader共用時,由於html-loader會先將html檔案編譯成字串,從而導致ejs語法解析失效。我使用的解決方案如下:所有使用到ejs語法的字尾改為.ejs,其中載入的圖片等資原始檔手動載入模組。例:<img src="${require('./assets/img/6.jpg')}" alt="">。然後html-loader不解析以ejs為字尾的檔案。

copy-webpack-plugin:webpack自帶外掛,用以複製檔案,主要複製不作為模組引入的資原始檔,例如:一些圖片字型等檔案,沒必要編譯,直接複製過來打包速度更快。

friendly-errors-webpack-plugin:友好提示外掛,CLI中提示資訊視覺化更加友好。如果使用 git bash 或者mac 的 Terminal 則沒必要安裝該外掛。

IgnorePlugin:webpack自帶外掛,不打包預設載入項,webpack會預設打包locale、moment等模組,如果專案不需要,可以使用該外掛遮蔽。

GTLoaderFilesPlugin:這是我自定義的資源載入外掛,可忽略。

主配置

context:配置entry和loader的參考路徑。

resolve.alias:模組別名配置,配合webpack.alias.js使用

2.webpack.dev.config.js

開發環境配置,先上程式碼:

const Webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    entry: {
        example: './examples/index.js'
    },
    devServer: {
        port: '9091',
        overlay: true,
        //設定為false則會在頁面中顯示當前webpack的狀態
        inline: true,
        historyApiFallback: true,
        //代理配置
        proxy: {
        },
        hot: true
        //強制頁面不通過重新整理頁面更新檔案
        // hotOnly: true
    },
    plugins: [
        //分析外掛
        // new BundleAnalyzerPlugin(),
        //模組熱更新外掛
        new Webpack.HotModuleReplacementPlugin(),
        //使用HMR時顯示模組的相對路徑
        new Webpack.NamedModulesPlugin()
    ]
};

複製程式碼

devServer:配置webpack自帶的伺服器,以作除錯只用。需要安裝webpack-dev-server外掛,注意,只能安裝V3之前的版本,V3版本是相容webpack4的,無法在webpack3中使用。

webpack-bundle-analyzer:第三方的分析外掛,可以對打包結果進行分析。也可以使用官方的分析方案:結合外掛stats-webpack-plugin生成的分析結果檔案和官方提供的線上工具官方工具來分析打包結果。

HotModuleReplacementPlugin:webpack自帶工具,模組熱更新必須外掛。

NamedModulesPlugin:webpack自帶外掛,用模組的路徑命名模組,執行結果更清晰。不使用這個外掛,webpack就會預設使用一個隨機ID命名。利於除錯,官方推薦開發環境需要使用的外掛。

3.webpack.prod.config.js

生產環境配置,先上程式碼:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const Webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ZipPlugin = require('zip-webpack-plugin');
const StatsPlugin = require('stats-webpack-plugin');
const SvnInfo = require('svn-info').sync('https://218.106.122.66/svn/framework/trunk/gt-ui', 'HEAD');

const Path = require('path');
const pkg = require('../package.json');

module.exports = {
    entry: {
        frame0: 'frame',
        frame2: 'frame2',
        frame3: 'frame3',
        center1: 'center1',
        center2: 'center2',
        center3: 'center3',
        login1: 'login1',
        login2: 'login2',
        form: 'form',
        example: './examples/index.js'
    },
    plugins: [
        //模組分析頁面
        // new BundleAnalyzerPlugin(),
        new Webpack.optimize.CommonsChunkPlugin({
            names: ['vendor'],
            minChunks: 2
        }),
        //混淆程式碼
        new UglifyJsPlugin({
            sourceMap: true,
            //多執行緒處理
            parallel: true,
            //使用快取
            cache: true
        }),
        //提取css檔案
        new ExtractTextPlugin({
            filename: '[name]-[hash:5].css'
        }),
        new CleanWebpackPlugin(['dist', 'package'], {
            root: Path.join(__dirname, '../')
        }),
        new Webpack.NamedChunksPlugin(),
        new Webpack.NamedModulesPlugin(),
        //版本資訊
        new Webpack.BannerPlugin({
            banner: `Name: ${pkg.name}\nSVNVersion: ${SvnInfo.revision}\nDate: ${new Date().toISOString().slice(0, 10)}\nDescription: ${pkg.description}`,
            raw: false,
            entryOnly: true,
            include: /\.js/g
        }),
        //分析結果
        new StatsPlugin('../stats.json', {
            chunkModules: true,
            exclude: [/node_modules/]
        }),
        //複製文件頁面
        new CopyWebpackPlugin([{
            // 第三方的字型檔案
            from: './examples',
            to: '../dist/examples'
        }, {
            //表單頁面檔案
            from: './docs',
            to: '../dist/docs'
        }], {
            ignore: ['**/.svn/**']
        }),
        //打包生成包的主頁
        new HtmlWebpackPlugin({
            template: Path.join(__dirname, '../src/index.html'),
            filename: '../index.html',
            inject: true
        }),
        //壓縮資料夾
        new ZipPlugin({
            filename: 'gt-ui.zip',
            path: '../package/',
            pathPrefix: 'dist'
        })
    ],
    profile: true
};
複製程式碼

CommonsChunkPlugin:webpack自帶外掛,提取多個入口中公共程式碼,webpack最開始的核心優勢codesplting的實現之一。

uglifyjs-webpack-plugin:程式碼壓縮混淆外掛,開啟多執行緒和快取可以加快打包速度。

clean-webpack-plugin:清空資料夾外掛,每次打包前先清空之前打包殘留檔案。

NamedChunksPlugin:webpack自帶外掛,開發環境配置中有說過,這裡旨在長效快取之用。如果不使用這個外掛,webpack生成的隨機ID會導致最終生成的程式碼檔案對應的hash值變化,導致長效快取失效。

NamedModulesPlugin:同上,這裡作用於chunk依賴的模組。

BannerPlugin:webpack自帶外掛,在程式碼中新增程式碼段,如程式碼版本、版權等資訊。

svn-info:獲取當前程式碼的SVN資訊。

stats-webpack-plugin:生成打包分析檔案,以在官方提供的線上析工具上使用。

zip-webpack-plugin:將打包結果壓縮成壓縮包。在使用這個外掛時,遇到了該外掛的執行順序錯誤,導致打包失敗的問題。相同的配置,有時該外掛會先於其他外掛執行,出現需要壓縮的檔案還沒生成導致打包中斷問題,有時卻不會。查了一些資料,發現除非是外掛本身處理了執行順序的問題,否則webpack的外掛的執行順序其實是不定的(略坑,相比而言gulp就要確定地多)。這裡有一個替代外掛,filemanager-webpack-plugin

4.webpack.dll.config.js

公共庫配置,先上程式碼:

const Path = require('path');
const Webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const alias = require('./webpack.alias');

module.exports = {
    entry: {
        ngs: ['angular', 'angular-resource', 'angular-sanitize', '@uirouter/angularjs',
            'angular-animate', 'angular-touch', 'angular-cookies'
        ],
        ngui: ['jquery', 'sweetalert', 'datetimepickerCN', 'datetimepicker', 'angular-loading-bar', 'angular-strap', 'angular-ui-grid', 'ui-select',
            'angular-ui-tour', 'angular-ui-tree', 'angular-validation', 'angular-carousel'
        ],
        base: ['babel-polyfill', 'lodash']
    },
    output: {
        path: Path.join(__dirname, '../dll'),
        filename: '[name].dll.js',
        library: '[name]'
    },
    resolve: {
        alias: alias
    },
    plugins: [
        new Webpack.DllPlugin({
            path: Path.join(__dirname, '../dll/manifest/', '[name].manifest.json'),
            name: '[name]'
        }),
        new CopyWebpackPlugin([{
            from: './src/libs/bootstrap-datetimepicker-master/css/bootstrap-datetimepicker.min.css'
        }, {
            from: './node_modules/angular-loading-bar/build/loading-bar.css'
        }, {
            from: './node_modules/ui-select/dist/select.css'
        }, {
            from: './node_modules/angular-ui-tree/dist/angular-ui-tree.min.css'
        }, {
            from: './node_modules/angular-carousel/dist/angular-carousel.min.css'
        }])
    ]
};

複製程式碼

這裡,為了加快打包速度,我將一些不需要編譯的檔案直接用拷貝外掛拷貝,用載入到程式碼中。

DllPlugin :webpack自帶外掛,生成庫定義檔案的,配合DllReferencePlugin使用。

5.webpack.alias.js

模組別名配置,先上程式碼:

var Path = require('path');

module.exports = {
    api: Path.join(__dirname, '../src/common/api'),
    //自定義控制元件
    ngControl: Path.join(__dirname, '../src/custom/controls/control'),
    //框架
    frame: Path.join(__dirname, '../src/custom/frame/frame'),
    frame1: Path.join(__dirname, '../src/custom/frame/frame1/frame'),
    frame2: Path.join(__dirname, '../src/custom/frame/frame2/frame'),
    frame3: Path.join(__dirname, '../src/custom/frame/frame3/frame'),
    login1: Path.join(__dirname, '../src/custom/login/login1/login'),
    login2: Path.join(__dirname, '../src/custom/login/login2/login'),
    center1: Path.join(__dirname, '../src/custom/system-center/center1/system-center'),
    center2: Path.join(__dirname, '../src/custom/system-center/center2/system-center'),
    center3: Path.join(__dirname, '../src/custom/system-center/center3/system-center'),
    frameManager: Path.join(__dirname, '../src/custom/frame-manager')
};

複製程式碼

這裡面就是配置不同模組對應的具體檔案地址,以便維護。

我們的構建配置是分為兩大類的:框架配置和應用系統配置。以上是前端框架的構建配置,應用系統的構建配置我在下一篇文章和大夥分享!

*歡迎關注我的微信公眾號:*

webpack實戰(一):真實專案中一個完整的webpack配置

相關文章