上手webpack4并進階?來看這里~

webpack作為一個模塊打包器,主要用于前端工程中的依賴梳理和模塊打包,將我們開發(fā)的具有高可讀性和可維護性的代碼文件打包成瀏覽器可以識別并正常運行的壓縮代碼,主要包括樣式文件處理成css,各種新式的JavaScript轉換成瀏覽器認識的寫法等,也是前端工程師進階的不二法門,本文借鑒了部分vue-cliwebpack的配置思路,還有一些網上比較好的解決方案,在此對這些作者一并表示感謝。

webpack.config.js配置項簡介

  1. Entry:入口文件配置,Webpack 執(zhí)行構建的第一步將從 Entry 開始,完成整個工程的打包。
  2. Module:模塊,在Webpack里一切皆模塊,Webpack會從配置的Entry開始遞歸找出所有依賴的模塊,最常用的是rules配置項,功能是匹配對應的后綴,從而針對代碼文件完成格式轉換和壓縮合并等指定的操作。
  3. Loader:模塊轉換器,用于把模塊原內容按照需求轉換成新內容,這個是配合Module模塊中的rules中的配置項來使用。
  4. Plugins:擴展插件,在Webpack構建流程中的特定時機注入擴展邏輯來改變構建結果或做你想要的事情。(插件API)
  5. Output:輸出結果,在Webpack經過一系列處理并得出最終想要的代碼后輸出結果,配置項用于指定輸出文件夾,默認是./dist。
  6. DevServer:用于配置開發(fā)過程中使用的本機服務器配置,屬于webpack-dev-server這個插件的配置項。

webpack打包流程簡介

  • 根據(jù)傳入的參數(shù)模式(development | production)來加載對應的默認配置
  • entry里配置的module開始遞歸解析entry所依賴的所有module
  • 每一個module都會根據(jù)rules的配置項去尋找用到的loader,接受所配置的loader的處理
  • entry中的配置對象為分組,每一個配置入口和其對應的依賴文件最后組成一個代碼塊文件(chunk)并輸出
  • 整個流程中webpack會在恰當?shù)臅r機執(zhí)行plugin的邏輯,來完成自定義的插件邏輯

基本的webpack配置搭建

首先通過以下的腳本命令來建立初始化文件:

npm init -y
npm i webpack webpack-cli -D // 針對webpack4的安裝
mkdir src && cd src && touch index.html index.js
cd ../ && mkdir dist && mkdir static
touch webpack.config.js
npm i webpack-dev-server --save-dev

修改生成的package.json文件,來引入webpack打包命令:

"scripts": {
    "build": "webpack --mode production",
    "dev": "webpack-dev-server --open --mode development"
}

webpack.config.js文件加入一些基本配置loader,從而基本的webpack4.x的配置成型(以兩個頁面入口為例):

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin') // 復制靜態(tài)資源的插件
const CleanWebpackPlugin = require('clean-webpack-plugin') // 清空打包目錄的插件
const HtmlWebpackPlugin = require('html-webpack-plugin') // 生成html的插件
const ExtractTextWebapckPlugin = require('extract-text-webpack-plugin') //CSS文件單獨提取出來
const webpack = require('webpack')

module.exports = {
    entry: {
        index: path.resolve(__dirname, 'src', 'index.js'),
        page: path.resolve(__dirname, 'src', 'page.js'),
        vendor:'lodash' // 多個頁面所需的公共庫文件,防止重復打包帶入
    },
    output:{
        publicPath: '/',  //這里要放的是靜態(tài)資源CDN的地址
        path: path.resolve(__dirname,'dist'),
        filename:'[name].[hash].js'
    },
    resolve:{
        extensions: [".js",".css",".json"],
        alias: {} //配置別名可以加快webpack查找模塊的速度
    },
    module: {
        // 多個loader是有順序要求的,從右往左寫,因為轉換的時候是從右往左轉換的
        rules:[
            {
                test: /\.css$/,
                use: ExtractTextWebapckPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'postcss-loader'] // 不再需要style-loader放到html文件內
                }),
                include: path.join(__dirname, 'src'), //限制范圍,提高打包速度
                exclude: /node_modules/
            },
            {
                test:/\.less$/,
                use: ExtractTextWebapckPlugin.extract({
                    fallback: 'style-loader',
                    use: ['css-loader', 'postcss-loader', 'less-loader']
                }),
                include: path.join(__dirname, 'src'),
                exclude: /node_modules/
            },
            {
                test:/\.scss$/,
                use: ExtractTextWebapckPlugin.extract({
                    fallback: 'style-loader',
                    use:['css-loader', 'postcss-loader', 'sass-loader']
                }),
                include: path.join(__dirname, 'src'),
                exclude: /node_modules/
            },
            {
                test: /\.jsx?$/,
                use: {
                    loader: 'babel-loader',
                    query: { //同時可以把babel配置寫到根目錄下的.babelrc中
                      presets: ['env', 'stage-0'] // env轉換es6 stage-0轉es7
                    }
                }
            },
            { //file-loader 解決css等文件中引入圖片路徑的問題
            // url-loader 當圖片較小的時候會把圖片BASE64編碼,大于limit參數(shù)的時候還是使用file-loader 進行拷貝
                test: /\.(png|jpg|jpeg|gif|svg)/,
                use: {
                  loader: 'url-loader',
                  options: {
                    outputPath: 'images/', // 圖片輸出的路徑
                    limit: 1 * 1024
                  }
                }
            }
        ]
    },
    plugins: [
        // 多入口的html文件用chunks這個參數(shù)來區(qū)分
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname,'src','index.html'),
            filename:'index.html',
            chunks:['index', 'vendor'],
            hash:true,//防止緩存
            minify:{
                removeAttributeQuotes:true//壓縮 去掉引號
            }
        }),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname,'src','page.html'),
            filename:'page.html',
            chunks:['page', 'vendor'],
            hash:true,//防止緩存
            minify:{
                removeAttributeQuotes:true//壓縮 去掉引號
            }
        }),
        new webpack.ProvidePlugin({
            _:'lodash' //所有頁面都會引入 _ 這個變量,不用再import引入
        }),
        new ExtractTextWebapckPlugin('css/[name].[hash].css'), // 其實這個特性只用于打包生產環(huán)境,測試環(huán)境這樣設置會影響HMR
        new CopyWebpackPlugin([
            {
                from: path.resolve(__dirname, 'static'),
                to: path.resolve(__dirname, 'dist/static'),
                ignore: ['.*']
            }
        ]),
        new CleanWebpackPlugin([path.join(__dirname, 'dist')]),
    ],
    devtool: 'eval-source-map', // 指定加source-map的方式
    devServer: {
        contentBase: path.join(__dirname, "dist"), //靜態(tài)文件根目錄
        port: 3824, // 端口
        host: 'localhost',
        overlay: true,
        compress: false // 服務器返回瀏覽器的時候是否啟動gzip壓縮
    },
    watch: true, // 開啟監(jiān)聽文件更改,自動刷新
    watchOptions: {
        ignored: /node_modules/, //忽略不用監(jiān)聽變更的目錄
        aggregateTimeout: 500, //防止重復保存頻繁重新編譯,500毫米內重復保存不打包
        poll:1000 //每秒詢問的文件變更的次數(shù)
    },
}

在命令行下用以下命令安裝loader和依賴的插件,生成完全的package.json項目依賴樹。

npm install extract-text-webpack-plugin@next --save-dev
npm i style-loader css-loader postcss-loader --save-dev
npm i less less-loader --save-dev
npm i node-sass sass-loader --save-dev
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 --save-dev
npm i file-loader url-loader --save-dev

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

npm run dev

默認打開的頁面是index.html頁面,可以加上/page.html來打開page頁面看效果。
PS: 關于loader的詳細說明可以參考webpack3.x的學習介紹,上面配置中需要注意的是多頁面的公共庫的引入采用的是vendor+暴露全局變量的方式,其實這種方式有諸多弊端,而webpack4針對這種情況設置了新的API,有興趣的話,就繼續(xù)看下面的高級配置吧。

進階的webpack4配置搭建

包含以下幾個方面:

  1. 針對CSSJSTreeShaking來減少無用代碼,針對JS需要對已有的uglifyjs進行一些自定義的配置(生產環(huán)境配置)
  2. 新的公共代碼抽取工具(optimization.SplitChunksPlugin)提取重用代碼,減小打包文件。(代替commonchunkplugin,生產和開發(fā)環(huán)境都需要)
  3. 使用HappyPack進行javascript的多進程打包操作,提升打包速度,并增加打包時間顯示。(生產和開發(fā)環(huán)境都需要)
  4. 創(chuàng)建一個webpack.dll.config.js文件打包常用類庫到dll中,使得開發(fā)過程中基礎模塊不會重復打包,而是去動態(tài)連接庫里獲取,代替上一節(jié)使用的vendor。(注意這個是在開發(fā)環(huán)境使用,生產環(huán)境打包對時間要求并不高,后者往往是項目持續(xù)集成的一部分)
  5. 模塊熱替換,還需要在項目中增加一些配置,不過大型框架把這塊都封裝好了。(開發(fā)環(huán)境配置)
  6. webpack3新增的作用域提升會默認在production模式下啟用,不用特別配置,但只有在使用ES6模塊才能生效。

關于第四點,需要在package.json中的script中增加腳本:
"build:dll": "webpack --config webpack.dll.config.js --mode development",

補充安裝插件的命令行:

npm i purify-css purifycss-webpack -D // 用于css的tree-shaking
npm i webpack-parallel-uglify-plugin -D // 用于js的tree-shaking
npm i happypack@next -D //用于多進程打包js
npm i progress-bar-webpack-plugin -D //用于顯示打包時間和進程
npm i webpack-merge -D //優(yōu)化配置代碼的工具
npm i optimize-css-assets-webpack-plugin -D //壓縮CSS
npm i chalk -D
npm install css-hot-loader -D // css熱更新
npm i mini-css-extract-plugin -D

TreeShaking需要增加的配置代碼,這一塊參考webpack文檔,需要三方面因素,分別是:

  • 使用ES6模塊(import/export)
  • package.json文件中聲明sideEffects指定可以treeShaking的模塊
  • 啟用UglifyJSPlugin,多入口下用WebpackParallelUglifyPlugin(這是下面的配置代碼做的事情)
/*最上面要增加的聲明變量*/
const glob = require('glob')
const PurifyCSSPlugin = require('purifycss-webpack')
const WebpackParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

/*在`plugins`配置項中需要增加的兩個插件設置*/
new PurifyCSSPlugin({
    paths: glob.sync(path.join(__dirname, 'src/*.html'))
}),
new WebpackParallelUglifyPlugin({
    uglifyJS: {
        output: {
            beautify: false, //不需要格式化
            comments: false //不保留注釋
        },
        compress: {
            warnings: false, // 在UglifyJs刪除沒有用到的代碼時不輸出警告
            drop_console: true, // 刪除所有的 `console` 語句,可以兼容ie瀏覽器
            collapse_vars: true, // 內嵌定義了但是只用到一次的變量
            reduce_vars: true // 提取出出現(xiàn)多次但是沒有定義成變量去引用的靜態(tài)值
        }
    }
    // 有興趣可以探究一下使用uglifyES
}),

關于ES6模塊這個事情,上文的第六點也提到了只有ES6模塊寫法才能用上最新的作用域提升的特性,首先webpack4.x并不需要額外修改babelrc的配置來實現(xiàn)去除無用代碼,這是從webpack2.x升級后支持的,改用sideEffect聲明來實現(xiàn)。但作用域提升仍然需要把babel配置中的module轉換去掉,修改后的.babelrc代碼如下:

{
  "presets": [["env", {"loose": true, "modules": false}], "stage-0"]
}

但這個時候會發(fā)現(xiàn)import引入樣式文件就被去掉了……只能使用require來改寫了。

打包DLL第三方類庫的配置項,用于開發(fā)環(huán)境:

  1. webpack.dll.config.js配置文件具體內容:
const path = require('path')
const webpack = require('webpack')
/**
 * 盡量減小搜索范圍
 * target: '_dll_[name]' 指定導出變量名字
 */
module.exports = {
    entry: {
        vendor: ['jquery', 'lodash']
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].dll.js',
        library: '_dll_[name]' // 全局變量名,其他模塊會從此變量上獲取里面模塊
    },
    // manifest是描述文件
    plugins: [
        new webpack.DllPlugin({
            name: '_dll_[name]',
            path: path.join(__dirname, 'dist', 'manifest.json')
        })
    ]
}
  1. webpack.config.js中增加的配置項:
/*找到上一步生成的`manifest.json`文件配置到`plugins`里面*/
new webpack.DllReferencePlugin({
    manifest: require(path.join(__dirname, '..', 'dist', 'manifest.json')),
}),

多文件入口的公用代碼提取插件配置:

/*webpack4.x的最新優(yōu)化配置項,用于提取公共代碼,跟`entry`是同一層級*/
optimization: {
    splitChunks: {
        cacheGroups: {
            commons: {
                chunks: "initial",
                name: "common",
                minChunks: 2,
                maxInitialRequests: 5,
                minSize: 0
            }
        }
    }
}

/*針對生成HTML的插件,需增加common,也去掉上一節(jié)加的vendor*/
new HtmlWebpackPlugin({
    template: path.resolve(__dirname,'src','index.html'),
    filename:'index.html',
    chunks:['index', 'common'],
    vendor: './vendor.dll.js', //與dll配置文件中output.fileName對齊
    hash:true,//防止緩存
    minify:{
        removeAttributeQuotes:true//壓縮 去掉引號
    }
}),
new HtmlWebpackPlugin({
    template: path.resolve(__dirname,'src','page.html'),
    filename:'page.html',
    chunks:['page', 'common'],
    vendor: './vendor.dll.js', //與dll配置文件中output.fileName對齊
    hash:true,//防止緩存
    minify:{
        removeAttributeQuotes:true//壓縮 去掉引號
    }
}),

PS: 這一塊要多注意,對應入口的HTML文件也要處理,關鍵是自定義的vendor項,在開發(fā)環(huán)境中引入到html

HappyPack的多進程打包處理:

/*最上面要增加的聲明變量*/
const HappyPack = require('happypack')
const os = require('os') //獲取電腦的處理器有幾個核心,作為配置傳入
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const ProgressBarPlugin = require('progress-bar-webpack-plugin')

/*在`module.rules`配置項中需要更改的`loader`設置*/
{
    test: /\.jsx?$/,
    loader: 'happypack/loader?id=happy-babel-js',
    include: [path.resolve('src')],
    exclude: /node_modules/,
},

/*在`plugins`配置項中需要增加的插件設置*/
new HappyPack({ //開啟多線程打包
    id: 'happy-babel-js',
    loaders: ['babel-loader?cacheDirectory=true'],
    threadPool: happyThreadPool
}),
new ProgressBarPlugin({
    format: '  build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)'
})

PS:要記住這種使用方法下一定要在根目錄下加.babelrc文件來設置babel的打包配置。

開發(fā)環(huán)境的代碼熱更新:
其實針對熱刷新,還有兩個方面要提及,一個是html文件里面寫代碼的熱跟新(這個對于框架不需要,如果要實現(xiàn),建議使用glup,后面有代碼),一個是寫的樣式代碼的熱更新,這兩部分也要加進去。讓我們一起看看熱更新需要增加的配置代碼:

/*在`devServer`配置項中需增加的設置*/
hot:true

/*在`plugins`配置項中需要增加的插件設置*/
new webpack.HotModuleReplacementPlugin(), //模塊熱更新
new webpack.NamedModulesPlugin(), //模塊熱更新

在業(yè)務代碼中要做一些改動,一個比較low的例子為:

if(module.hot) { //設置消息監(jiān)聽,重新執(zhí)行函數(shù)
    module.hot.accept('./hello.js', function() {
        div.innerHTML = hello()
    })
}

但還是不能實現(xiàn)在html修改后自動刷新頁面,這里有個概念是熱更新不是針對頁面級別的修改,這個問題有一些解決方法,但目前都不是很完美,可以參考這里,現(xiàn)在針對CSS的熱重載有一套解決方案如下,需要放棄使用上文提到的ExtractTextWebapckPlugin,引入mini-css-extract-pluginhot-css-loader來實現(xiàn),前者在webpack4.x上與hot-css-loader有報錯,讓我們改造一番:

/*最上面要增加的聲明變量*/
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

/*在樣式的`loader`配置項中需增加的設置,實現(xiàn)css熱更新,以css為例,其他可以參照我的倉庫來寫*/
{
    test: /\.css$/,
    use: ['css-hot-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
    include: [resolve('src')], //限制范圍,提高打包速度
    exclude: /node_modules/
}

/*在`plugins`配置項中需要增加的插件設置,注意這里不能寫[hash],否則無法實現(xiàn)熱跟新,如果有hash需要,可以開發(fā)環(huán)境和生產環(huán)境分開配置*/
new MiniCssExtractPlugin({
    filename: "[name].css",
    chunkFilename: "[id].css"
})

用于生產環(huán)境壓縮css的插件,看官方文檔說明,樣式文件壓縮沒有內置的,所以暫時引用第三方插件來做,以下是配置示例。

/*要增加的聲明變量*/
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

/*在`plugins`配置項中需要增加的插件設置*/
new OptimizeCSSPlugin({
    cssProcessorOptions: {safe: true}
})

最終成果

在進階部分我們對webpack配置文件根據(jù)開發(fā)環(huán)境和生產環(huán)境的不同做了分別的配置,因此有必要分成兩個文件,然后發(fā)現(xiàn)重復的配置代碼很多,作為有代碼潔癖的人不能忍,果斷引入webpack-merge,來把相同的配置抽出來,放到build/webpack.base.js中,而后在build/webpack.dev.config.js(開發(fā)環(huán)境)和build/webpack.prod.config.js(生產環(huán)境)中分別引用,在這個過程中也要更改之前文件的路徑設置,以免打包或者找文件的路徑出錯,同時將package.json中的腳本命令修改為:

"scripts": {
    "build": "webpack --config build/webpack.prod.config.js --mode production",
    "dev": "webpack-dev-server --open --mode development --config build/webpack.dev.config.js",
    "dev:dll": "webpack --config build/webpack.dll.config.js --mode development",
    "start": "npm run dev:dll && npm run dev"
}

接下來就是代碼的重構過程,這個過程其實我建議大家自己動手做一做,就能對webpack配置文件結構更加清晰,下面的代碼過于冗長,有興趣請到我的github地址來看。

  • build文件夾下的webpack.base.js文件(太長不上)
  • build文件夾下的webpack.dev.config.js文件(太長不上)
  • build文件夾下的webpack.prod.config.js文件(太長不上)

多說一句,就是實現(xiàn)JS打包的treeShaking還有一種方法是編譯期分析依賴,利用uglifyjs來完成,這種情況需要保留ES6模塊才能實現(xiàn),因此在使用這一特性的倉庫中,.babelrc文件的配置為:"presets": [["env", { "modules": false }], "stage-0"],就是打包的時候不要轉換模塊引入方式的含義。

接下來就可以運行npm start,看一下進階配置后的成果啦,吼吼,之后只要不進行build打包操作,通過npm run dev啟動,不用重復打包vendor啦。生產環(huán)境打包使用的是npm run build

以上就是對webpack4.x配置的踩坑過程,期間參考了大量谷歌英文資料,希望能幫助大家更好地掌握wepback最新版本的配置,以上內容親測跑通,有問題的話,歡迎加我微信(kashao3824)討論,來github地址issue也可,歡迎fork/star。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容