webpack最全實(shí)用配置(收藏)

1. 入門(一起來用這些小例子讓你熟悉webpack的配置)

1.1 初始化項(xiàng)目

新建一個(gè)目錄,初始化npm

npm init

webpack是運(yùn)行在node環(huán)境中的,我們需要安裝以下兩個(gè)npm包

npm i -D webpack webpack-cli
  • npm i -D 為npm install --save-dev的縮寫
  • npm i -S 為npm install --save的縮寫

新建一個(gè)文件夾src ,然后新建一個(gè)文件main.js,寫一點(diǎn)代碼測(cè)試一下

console.log('call me 老yuan')

配置package.json命令

59f09701704cb2a02f14f738f13bd7b2.png

執(zhí)行

npm run build

此時(shí)如果生成了一個(gè)dist文件夾,并且內(nèi)部含有main.js說明已經(jīng)打包成功了

1.2 開始我們自己的配置

上面一個(gè)簡(jiǎn)單的例子只是webpack自己默認(rèn)的配置,下面我們要實(shí)現(xiàn)更加豐富的自定義配置

新建一個(gè)build文件夾,里面新建一個(gè)webpack.config.js

// webpack.config.js
const path = require('path');
module.exports = {
    mode: 'development', // 開發(fā)模式   
    entry: path.resolve(__dirname, '../src/main.js'),
    // 入口文件  
    output: {
        filename: 'output.js', // 打包后的文件名稱  
        path: path.resolve(__dirname, '../dist') // 打包后的目錄
    }
}

更改我們的打包命令

fe33a8e067a27311098fb56c59325eb6.png

執(zhí)行 npm run build
會(huì)發(fā)現(xiàn)生成了以下目錄(圖片)
其中dist文件夾中的main.js就是我們需要在瀏覽器中實(shí)際運(yùn)行的文件
當(dāng)然實(shí)際運(yùn)用中不會(huì)僅僅如此,下面讓我們通過實(shí)際案例帶你快速入手webpack

1.3 配置html模板

js文件打包好了,但是我們不可能每次在html文件中手動(dòng)引入打包好的js

這里可能有的朋友會(huì)認(rèn)為我們打包js文件名稱不是一直是固定的嘛( output.js)?這樣每次就不用改動(dòng)引入文件名稱了呀?實(shí)際上我們?nèi)粘i_發(fā)中往往會(huì)這樣配置:

module.exports = { // 省略其他配置   
    output: {
        filename: '[name].[hash:8].js', // 打包后的文件名稱  
        path: path.resolve(__dirname, '../dist') // 打包后的目錄
    }
}

這時(shí)候生成的dist目錄文件如下

40f2f617f29192c8903cc107f7433d08.png

為了緩存,你會(huì)發(fā)現(xiàn)打包好的js文件的名稱每次都不一樣。webpack打包出來的js文件我們需要引入到html中,但是每次我們都手動(dòng)修改js文件名顯得很麻煩,因此我們需要一個(gè)插件來幫我們完成這件事情

npm i -D html-webpack-plugin

新建一個(gè)build同級(jí)的文件夾public,里面新建一個(gè)index.html
具體配置文件如下

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = {
    mode: 'development', // 開發(fā)模式   
    entry: path.resolve(__dirname, '../src/main.js'), // 入口文件  
    output: {
        filename: '[name].[hash:8].js', // 打包后的文件名稱   
        path: path.resolve(__dirname, '../dist') // 打包后的目錄  
    },
    plugins: [new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../public/index.html')
    })]
}

生成目錄如下(圖片)

6da20cc5c3fda1c096fca0a2abb0a5a0.png

可以發(fā)現(xiàn)打包生成的js文件已經(jīng)被自動(dòng)引入html文件中

1.3.1. 多入口文件如何開發(fā)

生成多個(gè) html-webpack-plugin實(shí)例來解決這個(gè)問題

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode: 'development', // 開發(fā)模式   
    entry: {
        main: path.resolve(__dirname, '../src/main.js'),
        header: path.resolve(__dirname, '../src/header.js')
    },
    output: {
        filename: '[name].[hash:8].js', // 打包后的文件名稱  
        path: path.resolve(__dirname, '../dist') // 打包后的目錄 
    },
    plugins: [new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../public/index.html'),
        filename: 'index.html',
        chunks: ['main'] // 與入口文件對(duì)應(yīng)的模塊名    
    }), new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../public/header.html'),
        filename: 'header.html',
        chunks: ['header'] // 與入口文件對(duì)應(yīng)的模塊名  
    }), ]
}

此時(shí)會(huì)發(fā)現(xiàn)生成以下目錄

bc1ca7c754aedeeac89b038a5a3969bb.png

1.3.2 clean-webpack-plugin

每次執(zhí)行npm run build 會(huì)發(fā)現(xiàn)dist文件夾里會(huì)殘留上次打包的文件,這里我們推薦一個(gè)plugin來幫我們?cè)诖虬敵銮扒蹇瘴募A clean-webpack-plugin

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {    // ...省略其他配置    
plugins:[new CleanWebpackPlugin()]
}

1.4 引用CSS

我們的入口文件是js,所以我們?cè)谌肟趈s中引入我們的css文件

28caa7b6a2f26aabda40b239c0354350.png

同時(shí)我們也需要一些loader來解析我們的css文件

npm i -D style-loader css-loader

如果我們使用less來構(gòu)建樣式,則需要多安裝兩個(gè)

npm i -D less less-loader

配置文件如下

// webpack.config.js
module.exports = { // ...省略其他配置  
    module: {
        rules: [{
            test: /.css$/,
            use: ['style-loader', 'css-loader'] // 從右向左解析原則    
        }, {
            test: /.less$/,
            use: ['style-loader', 'css-loader', 'less-loader'] // 從右向左解析原則      
        }]
    }
}

瀏覽器打開html如下

5fdfdd2d6de878b325326e523d412346.png

1.4.1 為css添加瀏覽器前綴

npm i -D postcss-loader autoprefixer

配置如下

// webpack.config.js
module.exports = {
    module: {
        rules: [test / .less$ / , use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'] // 從右向左解析原則  
        ]
    }
}

接下來,我們還需要引入autoprefixer使其生效,這里有兩種方式

1,在項(xiàng)目根目錄下創(chuàng)建一個(gè)postcss.config.js文件,配置如下:

module.exports = {    plugins: [require('autoprefixer')]  // 引用該插件即可了}

2,直接在webpack.config.js里配置

// webpack.config.js
module.exports = { //...省略其他配置  
    module: {
        rules: [{
            test: /.less$/,
            use: ['style-loader', 'css-loader', {
                loader: 'postcss-loader',
                options: {
                    plugins: [require('autoprefixer')]
                }
            }, 'less-loader'] // 從右向左解析原則 
        }]
    }
}

這時(shí)候我們發(fā)現(xiàn)css通過style標(biāo)簽的方式添加到了html文件中,但是如果樣式文件很多,全部添加到html中,難免顯得混亂。這時(shí)候我們想用把css拆分出來用外鏈的形式引入css文件怎么做呢?這時(shí)候我們就需要借助插件來幫助我們

1.4.2 拆分css

npm i -D mini-css-extract-plugin

webpack 4.0以前,我們通過 extract-text-webpack-plugin插件,把css樣式從js文件中提取到單獨(dú)的css文件中。webpack4.0以后,官方推薦使用 mini-css-extract-plugin插件來打包c(diǎn)ss文件

配置文件如下

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = { //...省略其他配置  
    module: {
        rules: [{
            test: /.less$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
        }]
    },
    plugins: [new MiniCssExtractPlugin({
        filename: "[name].[hash].css",
        chunkFilename: "[id].css",
    })]
}

1.4.3 拆分多個(gè)css

這里需要說的細(xì)一點(diǎn),上面我們所用到的 mini-css-extract-plugin會(huì)將所有的css樣式合并為一個(gè)css文件。如果你想拆分為一一對(duì)應(yīng)的多個(gè)css文件,我們需要使用到 extract-text-webpack-plugin,而目前 mini-css-extract-plugin還不支持此功能。我們需要安裝@next版本的 extract-text-webpack-plugin

npm i - D extract - text - webpack - plugin @next // webpack.config.js
const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
    module: {
        rules: [{
            test: /.css$/,
            use: indexCss.extract({
                use: ['css-loader']
            })
        }, {
            test: /.less$/,
            use: indexLess.extract({
                use: ['css-loader', 'less-loader']
            })
        }]
    },
    plugins: [indexLess, indexCss]
}

1.5 打包 圖片、字體、媒體、等文件

file-loader就是將文件在進(jìn)行一些處理后(主要是處理文件名和路徑、解析文件url),并將文件移動(dòng)到輸出的目錄中url-loader 一般與file-loader搭配使用,功能與 file-loader 類似,如果文件小于限制的大小。則會(huì)返回 base64 編碼,否則使用 file-loader 將文件移動(dòng)到輸出的目錄中

// webpack.config.js
module.exports = { // 省略其它配置 ...  
    module: {
        rules: [ // ...     
            {
                test: /.(jpe?g|png|gif)$/i, //圖片文件 
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'img/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            }, {
                test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/, //媒體文件 
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'media/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            }, {
                test: /.(woff2?|eot|ttf|otf)(?.*)?$/i, // 字體
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'fonts/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
        ]
    }
}

1.6 用babel轉(zhuǎn)義js文件

為了使我們的js代碼兼容更多的環(huán)境我們需要安裝依賴

npm i babel-loader @babel/preset-env @babel/core
  • 注意

babel-loaderbabel-core的版本對(duì)應(yīng)關(guān)系

  1. babel-loader 8.x 對(duì)應(yīng)babel-core 7.x
  2. babel-loader 7.x 對(duì)應(yīng)babel-core 6.x

配置如下

// webpack.config.js
module.exports = { // 省略其它配置 ... 
    module: {
        rules: [{
            test: /.js$/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
            },
            exclude: /node_modules/
        }, ]
    }
}

上面的babel-loader只會(huì)將 ES6/7/8語法轉(zhuǎn)換為ES5語法,但是對(duì)新api并不會(huì)轉(zhuǎn)換 例如(promise、Generator、Set、Maps、Proxy等)
此時(shí)我們需要借助babel-polyfill來幫助我們轉(zhuǎn)換

npm i @babel/polyfill
// webpack.config.js
const path = require('path')module.exports = {  
  entry: ["@babel/polyfill,path.resolve(__dirname,'../src/index.js')"],    // 入口文件}
  • 手動(dòng)把上面的demo敲一遍對(duì)閱讀下面的文章更有益,建議入門的同學(xué)敲三遍以上

上面的實(shí)踐是我們對(duì)webpack的功能有了一個(gè)初步的了解,但是要想熟練應(yīng)用于開發(fā)中,我們需要一個(gè)系統(tǒng)的實(shí)戰(zhàn)。讓我們一起擺脫腳手架嘗試自己搭建一個(gè)vue開發(fā)環(huán)境

2. 搭建vue開發(fā)環(huán)境

上面的小例子已經(jīng)幫助而我們實(shí)現(xiàn)了打包c(diǎn)ss、圖片、js、html等文件。
但是我們還需要以下幾種配置

2.1 解析.vue文件

npm i -D vue-loader vue-template-compiler vue-style-loadernpm i -S vue

vue-loader 用于解析.vue文件vue-template-compiler 用于編譯模板
配置如下

const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    module: {
        rules: [{
            test: /.vue$/,
            use: ['vue-loader']
        }, ]
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.runtime.esm.js',
            ' @': path.resolve(__dirname, '../src')
        },
        extensions: ['*', '.js', '.json', '.vue']
    },
    plugins: [new vueLoaderPlugin()]
}

2.2 配置webpack-dev-server進(jìn)行熱更新

npm i -D webpack-dev-server

配置如下

const Webpack = require('webpack')
module.exports = { // ...省略其他配置 
    devServer: {
        port: 3000,
        hot: true,
        contentBase: '../dist'
    },
    plugins: [new Webpack.HotModuleReplacementPlugin()]
}

完整配置如下

// webpack.config.js
const path = require('path');
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack')
module.exports = {
    mode: 'development', // 開發(fā)模式   
    entry: {
        main: path.resolve(__dirname, '../src/main.js'),
    },
    output: {
        filename: '[name].[hash:8].js', // 打包后的文件名稱 
        path: path.resolve(__dirname, '../dist') // 打包后的目錄 
    },
    module: {
        rules: [{
            test: /.vue$/,
            use: ['vue-loader']
        }, {
            test: /.js$/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: [
                        ['@babel/preset-env']
                    ]
                }
            }
        }, {
            test: /.css$/,
            use: ['vue-style-loader', 'css-loader', {
                loader: 'postcss-loader',
                options: {
                    plugins: [require('autoprefixer')]
                }
            }]
        }, {
            test: /.less$/,
            use: ['vue-style-loader', 'css-loader', {
                loader: 'postcss-loader',
                options: {
                    plugins: [require('autoprefixer')]
                }
            }, 'less-loader']
        }]
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.runtime.esm.js',
            ' @': path.resolve(__dirname, '../src')
        },
        extensions: ['*', '.js', '.json', '.vue']
    },
    devServer: {
        port: 3000,
        hot: true,
        contentBase: '../dist'
    },
    plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../public/index.html'),
        filename: 'index.html'
    }), new vueLoaderPlugin(), new Webpack.HotModuleReplacementPlugin()]
}

2.3 配置打包命令

83e3eb20ea09be8d47edd1ab631e2cb3.png

打包文件已經(jīng)配置完畢,接下來讓我們測(cè)試一下
首先在src新建一個(gè)main.js

34b9342d1bd1b24e90dfecd45c4eb090.png

新建一個(gè)App.vue

421deeabdfc47804d536a62274ee7a8c.png

新建一個(gè)public文件夾,里面新建一個(gè)index.html

44f6ea02073654e737e34f3f7676dcd9.png

執(zhí)行npm run dev這時(shí)候如果瀏覽器出現(xiàn)Vue開發(fā)環(huán)境運(yùn)行成功,那么恭喜你,已經(jīng)成功邁出了第一步

2.4 區(qū)分開發(fā)環(huán)境與生產(chǎn)環(huán)境

實(shí)際應(yīng)用到項(xiàng)目中,我們需要區(qū)分開發(fā)環(huán)境與生產(chǎn)環(huán)境,我們?cè)谠瓉韜ebpack.config.js的基礎(chǔ)上再新增兩個(gè)文件

  • webpack.dev.js 開發(fā)環(huán)境配置文件
開發(fā)環(huán)境主要實(shí)現(xiàn)的是熱更新,不要壓縮代碼,完整的sourceMap
  • webpack.prod.js生產(chǎn)環(huán)境配置文件
生產(chǎn)環(huán)境主要實(shí)現(xiàn)的是壓縮代碼、提取css文件、合理的sourceMap、分割代碼需要安裝以下模塊:npm i -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
  • webpack-merge 合并配置
  • copy-webpack-plugin 拷貝靜態(tài)資源
  • optimize-css-assets-webpack-plugin 壓縮css
  • uglifyjs-webpack-plugin 壓縮js

webpack mode設(shè)置 production的時(shí)候會(huì)自動(dòng)壓縮js代碼。原則上不需要引入 uglifyjs-webpack-plugin進(jìn)行重復(fù)工作。但是 optimize-css-assets-webpack-plugin壓縮css的同時(shí)會(huì)破壞原有的js壓縮,所以這里我們引入 uglifyjs進(jìn)行壓縮

2.4.1 webpack.config.js

const path = require('path')
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const devMode = process.argv.indexOf('--mode=production') === -1;
module.exports = {
    entry: {
        main: path.resolve(__dirname, '../src/main.js')
    },
    output: {
        path: path.resolve(__dirname, '../dist'),

        filename: 'js/[name].[hash:8].js',
        chunkFilename: 'js/[name].[hash:8].js'
    },
    module: {
        rules: [{
            test: /.js$/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
            },
            exclude: /node_modules/
        }, {
            test: /.vue$/,
            use: ['cache-loader', 'thread-loader', {
                loader: 'vue-loader',
                options: {
                    compilerOptions: {
                        preserveWhitespace: false
                    }
                }
            }]
        }, {
            test: /.css$/,
            use: [{
                loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
                options: {
                    publicPath: "../dist/css/",
                    hmr: devMode
                }
            }, 'css-loader', {
                loader: 'postcss-loader',
                options: {
                    plugins: [require('autoprefixer')]
                }
            }]
        }, {
            test: /.less$/,
            use: [{
                loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
                options: {
                    publicPath: "../dist/css/",
                    hmr: devMode
                }
            }, 'css-loader', 'less-loader', {
                loader: 'postcss-loader',
                options: {
                    plugins: [require('autoprefixer')]
                }
            }]
        }, {
            test: /.(jep?g|png|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 10240,
                    fallback: {
                        loader: 'file-loader',
                        options: {
                            name: 'img/[name].[hash:8].[ext]'
                        }
                    }
                }
            }
        }, {
            test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 10240,
                    fallback: {
                        loader: 'file-loader',
                        options: {
                            name: 'media/[name].[hash:8].[ext]'
                        }
                    }
                }
            }
        }, {
            test: /.(woff2?|eot|ttf|otf)(?.*)?$/i,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 10240,
                    fallback: {
                        loader: 'file-loader',
                        options: {
                            name: 'media/[name].[hash:8].[ext]'
                        }
                    }
                }
            }
        }]
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.runtime.esm.js',
            ' @': path.resolve(__dirname, '../src')
        },
        extensions: ['*', '.js', '.json', '.vue']
    },
    plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../public/index.html')
    }), new vueLoaderPlugin(), new MiniCssExtractPlugin({
        filename: devMode ? '[name].css' : '[name].[hash].css',
        chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
    })]
}

2.4.2 webpack.dev.js

const Webpack = require('webpack')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
module.exports = WebpackMerge(webpackConfig, {
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    devServer: {
        port: 3000,
        hot: true,
        contentBase: '../dist'
    },
    plugins: [new Webpack.HotModuleReplacementPlugin()]
})

2.4.3 webpack.prod.js

const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = WebpackMerge(webpackConfig, {
    mode: 'production',
    devtool: 'cheap-module-source-map',
    plugins: [
        new CopyWebpackPlugin([{
            from: path.resolve(__dirname, '../public'),
            to: path.resolve(__dirname, '../dist')
        }]),
    ],
    optimization: {
        minimizer: [new UglifyJsPlugin({ //壓縮js 
            cache: true,
            parallel: true,
            sourceMap: true
        }), new OptimizeCssAssetsPlugin({})],
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                libs: {
                    name: "chunk-libs",
                    test: /[/]node_modules[/]/,
                    priority: 10,
                    chunks: "initial" // 只打包初始時(shí)依賴的第三方   
                }
            }
        }
    }
})

2.5 優(yōu)化webpack配置

看到這里你或許有些累了,但是要想獲取更好的offer,更高的薪水,下面必須繼續(xù)深入

2f2be2cd559266f3bed5d93d2d902510.png

優(yōu)化配置對(duì)我們來說非常有實(shí)際意義,這實(shí)際關(guān)系到你打包出來文件的大小,打包的速度等。
具體優(yōu)化可以分為以下幾點(diǎn):

2.5.1 優(yōu)化打包速度

構(gòu)建速度指的是我們每次修改代碼后熱更新的速度以及發(fā)布前打包文件的速度。

2.5.1.1 合理的配置mode參數(shù)與devtool參數(shù)

mode可設(shè)置development production兩個(gè)參數(shù)

如果沒有設(shè)置,webpack4 會(huì)將 mode 的默認(rèn)值設(shè)置為 production``production模式下會(huì)進(jìn)行tree shaking(去除無用代碼)和uglifyjs(代碼壓縮混淆)

2.5.1.2 縮小文件的搜索范圍(配置include exclude alias noParse extensions)

  • alias: 當(dāng)我們代碼中出現(xiàn) import 'vue'時(shí), webpack會(huì)采用向上遞歸搜索的方式去node_modules 目錄下找。為了減少搜索范圍我們可以直接告訴webpack去哪個(gè)路徑下查找。也就是別名(alias)的配置。
  • include exclude 同樣配置include exclude也可以減少webpack loader的搜索轉(zhuǎn)換時(shí)間。
  • noParse 當(dāng)我們代碼中使用到import jq from 'jquery'時(shí),webpack會(huì)去解析jq這個(gè)庫是否有依賴其他的包。但是我們對(duì)類似jquery這類依賴庫,一般會(huì)認(rèn)為不會(huì)引用其他的包(特殊除外,自行判斷)。增加noParse屬性,告訴webpack不必解析,以此增加打包速度。
  • extensions webpack會(huì)根據(jù)extensions定義的后綴查找文件(頻率較高的文件類型優(yōu)先寫在前面)
87bf48305eb3e59f5c08bfcc53fe301a.png

2.5.1.3 使用HappyPack開啟多進(jìn)程Loader轉(zhuǎn)換

在webpack構(gòu)建過程中,實(shí)際上耗費(fèi)時(shí)間大多數(shù)用在loader解析轉(zhuǎn)換以及代碼的壓縮中。日常開發(fā)中我們需要使用Loader對(duì)js,css,圖片,字體等文件做轉(zhuǎn)換操作,并且轉(zhuǎn)換的文件數(shù)據(jù)量也是非常大。由于js單線程的特性使得這些轉(zhuǎn)換操作不能并發(fā)處理文件,而是需要一個(gè)個(gè)文件進(jìn)行處理。HappyPack的基本原理是將這部分任務(wù)分解到多個(gè)子進(jìn)程中去并行處理,子進(jìn)程處理完成后把結(jié)果發(fā)送到主進(jìn)程中,從而減少總的構(gòu)建時(shí)間

npm i -D happypack
a74b7e241211c4e1508ce477547be910.png

2.5.1.4 使用webpack-parallel-uglify-plugin 增強(qiáng)代碼壓縮

上面對(duì)于loader轉(zhuǎn)換已經(jīng)做優(yōu)化,那么下面還有另一個(gè)難點(diǎn)就是優(yōu)化代碼的壓縮時(shí)間。

npm i -D webpack-parallel-uglify-plugin
54a7fed62d3a52b299e9850f2c40ec0d.png

2.5.1.5 抽離第三方模塊

對(duì)于開發(fā)項(xiàng)目中不經(jīng)常會(huì)變更的靜態(tài)依賴文件。類似于我們的 elementUi、vue全家桶等等。因?yàn)楹苌贂?huì)變更,所以我們不希望這些依賴要被集成到每一次的構(gòu)建邏輯中去。 這樣做的好處是每次更改我本地代碼的文件的時(shí)候, webpack只需要打包我項(xiàng)目本身的文件代碼,而不會(huì)再去編譯第三方庫。以后只要我們不升級(jí)第三方包的時(shí)候,那么 webpack就不會(huì)對(duì)這些庫去打包,這樣可以快速的提高打包的速度。

這里我們使用webpack內(nèi)置的DllPlugin DllReferencePlugin進(jìn)行抽離
在與webpack配置文件同級(jí)目錄下新建webpack.dll.config.js
代碼如下

// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = { // 你想要打包的模塊的數(shù)組  
    entry: {
        vendor: ['vue', 'element-ui']
    },
    output: {
        path: path.resolve(__dirname, 'static/js'), // 打包后文件輸出的位置 
        filename: '[name].dll.js',
        library: '[name]_library'
        // 這里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。 
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.resolve(__dirname, '[name]-manifest.json'),
            name: '[name]_library',
            context: __dirname
        })
    ]
};

package.json中配置如下命令

"dll": "webpack --config build/webpack.dll.config.js"

接下來在我們的webpack.config.js中增加以下代碼

module.exports = {
        plugins: [
                new webpack.DllReferencePlugin({
                    context: __dirname,
                    manifest: require('./vendor-manifest.json')
                }),
                new CopyWebpackPlugin([ // 拷貝生成的文件到dist目錄 這樣每次不必手動(dòng)去cv      {from: 'static', to:'static'}    ]),
                    ]
                };

執(zhí)行

npm run dll

會(huì)發(fā)現(xiàn)生成了我們需要的集合第三地方
代碼的vendor.dll.js
我們需要在html文件中手動(dòng)引入這個(gè)js文件

<!DOCTYPE html>
<html lang="en">
<head>  
<meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>老yuan</title>
  <script src="static/js/vendor.dll.js"></script>
</head>
<body> 
 <div id="app"></div>
</body>
</html>

這樣如果我們沒有更新第三方依賴包,就不必npm run dll。直接執(zhí)行npm run dev npm run build的時(shí)候會(huì)發(fā)現(xiàn)我們的打包速度明顯有所提升。因?yàn)槲覀円呀?jīng)通過dllPlugin將第三方依賴包抽離出來了。

2.5.1.6 配置緩存

我們每次執(zhí)行構(gòu)建都會(huì)把所有的文件都重復(fù)編譯一遍,這樣的重復(fù)工作是否可以被緩存下來呢,答案是可以的,目前大部分 loader 都提供了 cache 配置項(xiàng)。比如在 babel-loader 中,可以通過設(shè)置 cacheDirectory 來開啟緩存, babel-loader?cacheDirectory=true 就會(huì)將每次的編譯結(jié)果寫進(jìn)硬盤文件(默認(rèn)是在項(xiàng)目根目錄下的 node_modules/.cache/babel-loader目錄內(nèi),當(dāng)然你也可以自定義)
<p>但如果 loader 不支持緩存呢?我們也有方法,我們可以通過 cache-loader ,它所做的事情很簡(jiǎn)單,就是 babel-loader 開啟 cache 后做的事情,將 loader 的編譯結(jié)果寫入硬盤緩存。再次構(gòu)建會(huì)先比較一下,如果文件較之前的沒有發(fā)生變化則會(huì)直接使用緩存。使用方法如官方 demo 所示,在一些性能開銷較大的 loader 之前添加此 loader即可</p>

npm i -D cache-loader
4ef93fe7e1d065a305d87805ccb60a73.png

2.5.2 優(yōu)化打包文件體積

打包的速度我們是進(jìn)行了優(yōu)化,但是打包后的文件體積卻是十分大,造成了頁面加載緩慢,浪費(fèi)流量等,接下來讓我們從文件體積上繼續(xù)優(yōu)化

2.5.2.1 引入webpack-bundle-analyzer分析打包后的文件

webpack-bundle-analyzer將打包后的內(nèi)容束展示為方便交互的直觀樹狀圖,讓我們知道我們所構(gòu)建包中真正引入的內(nèi)容

npm i -D webpack-bundle-analyzer
773bf92c29463e1181cb37ae3aa82d66.png

接下來在package.json里配置啟動(dòng)命令

"analyz": "NODE_ENV=production npm_config_report=true npm run build"

windows請(qǐng)安裝npm i -D cross-env

"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"

接下來npm run analyz瀏覽器會(huì)自動(dòng)打開文件依賴圖的網(wǎng)頁

2.5.2.3 externals

按照官方文檔的解釋,如果我們想引用一個(gè)庫,但是又不想讓 webpack打包,并且又不影響我們?cè)诔绦蛑幸?CMD、AMD或者 window/global全局等方式進(jìn)行使用,那就可以通過配置 Externals。這個(gè)功能主要是用在創(chuàng)建一個(gè)庫的時(shí)候用的,但是也可以在我們項(xiàng)目開發(fā)中充分使用 Externals的方式,我們將這些不需要打包的靜態(tài)資源從構(gòu)建邏輯中剔除出去,而使用 CDN
的方式,去引用它們。
<p>有時(shí)我們希望我們通過 script引入的庫,如用CDN的方式引入的 jquery,我們?cè)谑褂脮r(shí),依舊用 require的方式來使用,但是卻不希望 webpack將它又編譯進(jìn)文件中。這里官網(wǎng)案例已經(jīng)足夠清晰明了,大家有興趣可以點(diǎn)擊了解 </p>

webpack
官網(wǎng)案例如下

<script  src="https://code.jquery.com/jquery-3.1.0.js"  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="  crossorigin="anonymous"></script>module.exports = { 
 //...  
externals: {    jquery: 'jQuery'  }};
import $ from 'jquery';
$('.my-element').animate(/* ... */);

2.5.2.3 Tree-shaking

這里單獨(dú)提一下 tree-shaking,是因?yàn)檫@里有個(gè)坑。 tree-shaking的主要作用是用來清除代碼中無用的部分。目前在 webpack4 我們?cè)O(shè)置 modeproduction的時(shí)候已經(jīng)自動(dòng)開啟了 tree-shaking。但是要想使其生效,生成的代碼必須是ES6模塊。不能使用其它類型的模塊如 CommonJS之流。如果使用 Babel的話,這里有一個(gè)小問題,因?yàn)?Babel的預(yù)案(preset)默認(rèn)會(huì)將任何模塊類型都轉(zhuǎn)譯成 CommonJS類型。修正這個(gè)問題也很簡(jiǎn)單,在 .babelrc文件或在 webpack.config.js文件中設(shè)置 modules: false就好了

// .babelrc{  "presets": [    ["@babel/preset-env",      {        "modules": false      }    ]  ]}

或者

// webpack.config.js 
module: {
    rules: [{
        test: /.js$/,
        use: {
            loader: 'babel-loader',
            options: {
                presets: ['@babel/preset-env', {
                    modules: false
                }]
            }
        },
        exclude: /(node_modules)/
    }]
}

經(jīng)歷過上面兩個(gè)系列的洗禮,到現(xiàn)在我們成為了一名合格的webpack配置工程師。但是光擰螺絲,自身的可替代性還是很高,下面我們將深入webpack的原理中去

65aec257be14fb24c610d2e6cdb4bd94.png

3.手寫webpack系列

經(jīng)歷過上面兩個(gè)部分,我們已經(jīng)可以熟練的運(yùn)用相關(guān)的loader和plugin對(duì)我們的代碼進(jìn)行轉(zhuǎn)換、解析。接下來我們自己手動(dòng)實(shí)現(xiàn)loader與plugin,使其在平時(shí)的開發(fā)中獲得更多的樂趣。

3.1 手寫webpack loader

loader從本質(zhì)上來說其實(shí)就是一個(gè) node模塊。相當(dāng)于一臺(tái)榨汁機(jī) (loader)將相關(guān)類型的文件代碼 (code)給它。根據(jù)我們?cè)O(shè)置的規(guī)則,經(jīng)過它的一系列加工后還給我們加工好的果汁 (code)。

loader編寫原則

  • 單一原則: 每個(gè) Loader 只做一件事;
  • 鏈?zhǔn)秸{(diào)用: Webpack 會(huì)按順序鏈?zhǔn)秸{(diào)用每個(gè) Loader;
  • 統(tǒng)一原則: 遵循 Webpack 制定的設(shè)計(jì)規(guī)則和結(jié)構(gòu),輸入與輸出均為字符串,各個(gè) Loader 完全獨(dú)立,即插即用;

在日常開發(fā)環(huán)境中,為了方便調(diào)試我們往往會(huì)加入許多console打印。但是我們不希望在生產(chǎn)環(huán)境中存在打印的值。那么這里我們自己實(shí)現(xiàn)一個(gè)loader去除代碼中的console

知識(shí)點(diǎn)普及之 AST。 AST通俗的來說,假設(shè)我們有一個(gè)文件 a.js,我們對(duì) a.js里面的1000行進(jìn)行一些操作處理,比如為所有的 await 增加 try catch,以及其他操作,但是 a.js里面的代碼本質(zhì)上來說就是一堆字符串。那我們?cè)趺崔k呢,那就是轉(zhuǎn)換為帶標(biāo)記信息的對(duì)象(抽象語法樹)我們方便進(jìn)行增刪改查。這個(gè)帶標(biāo)記的對(duì)象(抽象語法樹)就是 AST。這里推薦一篇不錯(cuò)的AST文章 AST快速入門

npm i -D @babel/parser @babel/traverse @babel/generator @babel/types
  • @babel/parser 將源代碼解析成 AST
  • @babel/traverse 對(duì)AST節(jié)點(diǎn)進(jìn)行遞歸遍歷,生成一個(gè)便于操作、轉(zhuǎn)換的path對(duì)象
  • @babel/generatorAST解碼生成js代碼
  • @babel/types通過該模塊對(duì)具體的AST節(jié)點(diǎn)進(jìn)行進(jìn)行增、刪、改、查

新建 drop-console.js

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports = function(source) {
    const ast = parser.parse(source, {
        sourceType: 'module'
    }) traverse(ast, {
        CallExpression(path) {
            if (t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {
                    name: "console"
                })) {
                path.remove()
            }
        }
    }) const output = generator(ast, {}, source);
    return output.code
}

如何使用

const path = require('path')
module.exports = {
    mode: 'development',
    entry: path.resolve(__dirname, 'index.js'),
    output: {
        filename: '[name].[contenthash].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [{
            test: /.js$/,
            use: path.resolve(__dirname, 'drop-console.js')
        }]
    }
}

實(shí)際上在 webpack4中已經(jīng)集成了去除 console功能,在 minimizer中可配置 去除console

附上官網(wǎng) 如何編寫一個(gè)loader

3.2 手寫webpack plugin

Webpack 運(yùn)行的生命周期中會(huì)廣播出許多事件, Plugin 可以監(jiān)聽這些事件,在合適的時(shí)機(jī)通過 Webpack提供的 API改變輸出結(jié)果。通俗來說:一盤美味的 鹽豆炒雞蛋 需要經(jīng)歷燒油 炒制 調(diào)味到最后的裝盤等過程,而 plugin相當(dāng)于可以監(jiān)控每個(gè)環(huán)節(jié)并進(jìn)行操作,比如可以寫一個(gè)少放胡椒粉 plugin,監(jiān)控 webpack暴露出的生命周期事件(調(diào)味),在調(diào)味的時(shí)候執(zhí)行少放胡椒粉操作。那么它與 loader的區(qū)別是什么呢?上面我們也提到了 loader的單一原則, loader只能一件事,比如說 less-loader,只能解析 less文件, plugin則是針對(duì)整個(gè)流程執(zhí)行廣泛的任務(wù)。

一個(gè)基本的plugin插件結(jié)構(gòu)如下

class firstPlugin {
    constructor(options) {
        console.log('firstPlugin options', options)
    }
    apply(compiler) {
        compiler.plugin('done', compilation => {
            console.log('firstPlugin')))
    }
}
module.exports = firstPlugin

compiler 、compilation是什么?

  • compiler 對(duì)象包含了 Webpack 環(huán)境所有的的配置信息。這個(gè)對(duì)象在啟動(dòng) webpack 時(shí)被一次性建立,并配置好所有可操作的設(shè)置,包括 optionsloaderplugin。當(dāng)在 webpack 環(huán)境中應(yīng)用一個(gè)插件時(shí),插件將收到此 compiler 對(duì)象的引用??梢允褂盟鼇碓L問 webpack 的主環(huán)境。
  • compilation對(duì)象包含了當(dāng)前的模塊資源、編譯生成資源、變化的文件等。當(dāng)運(yùn)行webpack 開發(fā)環(huán)境中間件時(shí),每當(dāng)檢測(cè)到一個(gè)文件變化,就會(huì)創(chuàng)建一個(gè)新的 compilation,從而生成一組新的編譯資源。compilation 對(duì)象也提供了很多關(guān)鍵時(shí)機(jī)的回調(diào),以供插件做自定義處理時(shí)選擇使用。

compiler和 compilation的區(qū)別在于

  • compiler代表了整個(gè)webpack從啟動(dòng)到關(guān)閉的生命周期,而compilation只是代表了一次新的編譯過程
  • compiler和compilation暴露出許多鉤子,我們可以根據(jù)實(shí)際需求的場(chǎng)景進(jìn)行自定義處理

compiler鉤子文檔
compilation鉤子文檔

下面我們手動(dòng)開發(fā)一個(gè)簡(jiǎn)單的需求,在生成打包文件之前自動(dòng)生成一個(gè)關(guān)于打包出文件的大小信息

新建一個(gè)webpack-firstPlugin.js

class firstPlugin {
    constructor(options) {
        this.options = options
    }
    apply(compiler) {
        compiler.plugin('emit', (compilation, callback) => {
            let str = ''
            for (let filename in compilation.assets) {
                str += `文件:${filename}  大小${compilation.assets[filename]['size']()}n`
            } // 通過compilation.assets可以獲取打包后靜態(tài)資源信息,同樣也可以寫入資源     
            compilation.assets['fileSize.md'] = {
                source: function() {
                    return str
                },
                size: function() {
                    return str.length
                }
            }
            callback()
        })
    }
}
module.exports = firstPlugin

如何使用

const path = require('path')const firstPlugin = require('webpack-firstPlugin.js')module.exports = {    // 省略其他代碼    plugins:[        new firstPlugin()    ]}

執(zhí)行 npm run build即可看到在dist文件夾中生成了一個(gè)包含打包文件信息的fileSize.md

上面兩個(gè) loaderplugin案例只是一個(gè)引導(dǎo),實(shí)際開發(fā)需求中的 loaderplugin要考慮的方面很多,建議大家自己多動(dòng)手嘗試一下。

附上官網(wǎng) 如何編寫一個(gè)plugin

3.3 手寫webpack

由于篇幅過長(zhǎng),且原理深入較多。鑒于本篇以快速上手應(yīng)用于實(shí)際開發(fā)的原則,這里決定另起一篇新的文章去詳細(xì)剖析 webpack原理以及實(shí)現(xiàn)一個(gè) demo版本。待格式校準(zhǔn)后,將會(huì)貼出文章鏈接在下方

4. webpack5.0的時(shí)代

無論是前端框架還是構(gòu)建工具的更新速度遠(yuǎn)遠(yuǎn)超乎了我們的想象,前幾年的jquery一把梭的時(shí)代一去不復(fù)返。我們要擁抱的是不斷更新迭代的vue、react、node、serverless、docker、k8s....

v2-916338e511a20085880148dbd1d9b060_b.gif

不甘落后的webpack也已經(jīng)在近日發(fā)布了 webpack 5.0.0 beta 10 版本。在之前作者也曾提過webpack5.0旨在減少配置的復(fù)雜度,使其更容易上手(webpack4的時(shí)候也說了這句話),以及一些性能上的提升

  • 使用持久化緩存提高構(gòu)建性能;
  • 使用更好的算法和默認(rèn)值改進(jìn)長(zhǎng)期緩存(long-term caching);
  • 清理內(nèi)部結(jié)構(gòu)而不引入任何破壞性的變化;
  • 引入一些breaking changes,以便盡可能長(zhǎng)的使用v5版本。

目前來看,維護(hù)者的更新很頻繁,相信用不了多久webpack5.0將會(huì)擁抱大眾。感興趣的同學(xué)可以先安裝beta版本嘗嘗鮮。不過在此之前建議大家先對(duì)webpack4進(jìn)行一番掌握,這樣后面的路才會(huì)越來越好走。

c95b888cee04722dffe05d5b8ea9ce19.png

原作者姓名:前端小布
原出處:segmentfault
(收藏)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容