使用webpack構建一個專案 (更新ing)

xuyede發表於2019-03-28

webpack安裝

  • 安裝本地webpack
  • yarn add webpack webpack-cli -D

webpack可以零配置

  • 打包工具 -> 輸出打包後的結果
  • 打包 (支援模組化,多個模組(js/img/style)打包成一個模組)

webpack的執行流程

  • 因為瀏覽器環境不支援node語法
  • webpack封裝自己的 _webpack_require_方法提取模組中的module.exports內容
  • 使用遞迴原理,遍歷提取每個依賴的模組

webpack基礎配置

  • webpack.config.js
  • 最簡單的配置
  • mode -> 檔案打包的模式
    mode : 'development',   // production
複製程式碼
  • entry -> 打包檔案入口
    entry : './src/index.js'
複製程式碼
  • output -> 打包檔案出口(必須為絕對路徑)
    output : {
        filename : 'bundle.[hash:8].js',        //打包後的檔案加上8位雜湊,罷免打包檔案快取問題
        path : path.resolve(__dirname, 'build')
    }
複製程式碼

webpack-dev-server

  • yarn add webpack-dev-server -D
  • npx webpack-dev-server
  • 啟動一個靜態伺服器(記憶體中,沒有實體檔案)
  • devServer -> 伺服器配置
    devServer : {
        port : 3000,
        progress : true,            // 載入時顯示進度條
        contentBase : './build',    // 以build目錄為靜態服務入口
        compress : true,            // 所有服務都使用gzip壓縮
    }
複製程式碼

webpack的外掛(都是類)

    plugins : [...]     // 外掛
複製程式碼

打包html

  • yarn add html-webpack-plugin -D
    const HtmlWebpackPlugin = require('html-webpack-plugin')

    const config = {
        plugins : [     // 陣列,放著所有webpack的外掛
            new HtmlWebpackPlugin({
                template : './src/index.html',
                filename : 'index.html',
                minify : {      //html處理配置
                    removeAttributeQuotes : true,   //去除雙引號
                    collapseWhitespace : true       //縮寫成一行
                },
                hash : true     //引入js是否使用hash
            })
        ],
        ...
    }
複製程式碼

抽離樣式

  • yarn add mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const config = {
    plugins : [
        new MiniCssExtractPlugin({
            filename : 'css/main.[hash:10].css'
        })
    ],
    module : {
        rules : [
            {
                test : /\.css$/,
                use : [
                    MiniCssExtractPlugin.loader,    // 替換掉style-loader,把css打包抽離到main.css中        
                    'css-loader'
                ]
            },
            {
                test : /\.styl$/,
                use : [
                    MiniCssExtractPlugin.loader,    // 替換掉style-loader,把css打包抽離到main.css中        
                    'css-loader',
                    'stylus-loader'
                ]
            }
        ]
    }
    ...
}
複製程式碼

給css3加上字首

  • yarn add postcss-loader autoprefixer -D
  • 需要新增一個 postcss.config.js,使用postcss-loader會自動呼叫該檔案
    postcss.config.js
    ->
        module.exports = {
            plugins : [
                require('autoprefixer)
            ]
        }
____________________________________________________________________________________________________________
    const config = {
        module : {
            rules : [
                {
                    test : /\.css$/,
                    use : [
                        MiniCssExtractPlugin.loader,      
                        'css-loader',
                        'postcss-loader'
                    ]
                },
            ]
        },
        ...
    }
複製程式碼

壓縮樣式

  • webpack預設使用uglifyjs給js壓縮,如果想給css壓縮,需要手動修改optimization.minimizer配置,並手動給js壓縮
  • yarn add optimize-css-assets-webpack-plugin -D
  • yarn add uglifyjs-webpack-plugin -D
    const UglifyJsPlugin = require("uglifyjs-webpack-plugin")
    const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin")

    const config = {
        optimization : {
            minimizer : [
                new UglifyJsPlugin({
                    cache: true,    //快取
                    parallel: true, //併發壓縮
                    sourceMap: true //生成map對映
                }),
                new OptimizeCSSAssetsPlugin({})
            ]
        }
    }
複製程式碼

webpack處理模組

    module : {
        rules : [ {test : //, use : [...]}, {}, {}]    // 模組處理規則
    }
複製程式碼

處理樣式模組,插到style裡

  • css less sass stylus
  • css -> yarn add css-loader style-loader -D
  • less -> yarn add less less-loader -D
  • sass -> yarn add node-sass sass-loader -D
  • stylus -> yarn add stylus stylus-loader -D
    const config = {
        module : {
            rules : [
                { 
                    test : /\.css$/,
                    use : [
                        {
                            loader : 'style-loader',
                            options : {
                                insertAt : 'top'    //把解析的style插入頂部,避免覆蓋原有style
                            }
                        },
                        'css-loader',   // 處理@import 解析路徑
                    ] 
                },
                {
                    test : /\.styl$/,
                    use : [
                        {
                            loader : 'style-loader',
                            options : {
                                insertAt : 'top'    //把解析的style插入頂部,避免覆蓋原有style
                            }
                        },
                        'css-loader',   // 處理@import 解析路徑
                        'stylus-lader'  // stylus -> css
                    ]
                }
            ]
        }
    }
複製程式碼

處理js模組

  • yarn add babel-loader @babel/core @babel/preset-env -D
  • babel-loader
  • @babel/core -> babel核心模組,包含很多transform方法去轉換程式碼
  • @babel/preset-env -> 程式碼轉化的外掛
  • @babel/plugin-transform-runtime
  • @babel/runtime -> 把公用的方法提取到一個單獨的庫中
    const config = {
        module : {
            rules : [
                {
                    test : /\.js$/,
                    use : {
                        loader : 'babel-loader',
                        options : {
                            presets : [
                                '@babel/preset-env'     //包含大部分es6 -> es5的外掛
                            ],
                            plugins : [
                                ...           // 部分高階的js語法轉換外掛
                                '@babel/plugin-transform-runtime',      // async, promise, generator

                            ]
                        }
                    },
                    include : path.resolve(__dirname, 'src'),
                    exclude : /node_modules/
                }
            ]
        }
    }
複製程式碼

js語法檢測

  • yarn add eslint eslint-loader -D
  • 去eslint官網下載一個適合的 .eslintrc.json檔案放在根目錄
    const config = {
        module : {
            rules : [
                {
                    test : /\.js$/,
                    use : {
                        loader : 'eslint-loader',
                        options : {
                            enforce : 'pre'   // 把該loader變為前置loader,優先普通loader(一般的都是)執行
                        }
                    },
                    
                }
            ]
        }
    }
複製程式碼

全域性變數引入

  • 以jquery為例

  • expose-loader -> 使用內聯loader expose-loader把jquery暴露到window上

import $ from 'expose-loader?$!jquery'

console.log($)
console.log(window.$)

//或者在webpack.config.js中配置
{
    test : require.resolve('jquery'),
    use : 'expose-loader?$'
}
複製程式碼
  • webpack.ProvidePlugin(webpack的外掛) -> 每個模組中都注入$
const webpack = require('webpack')

plugins : [
    new webpack.ProvidePlugin({
        $ : 'jquery'
    })
]

console.log($)
複製程式碼
  • 使用cdn引入
const config = {
    externals : {
        "jquery": {
            commonjs: "$",      //如果我們的庫執行在Node.js環境中,import $ from 'jquery'等價於const $ = require('jquery')
            commonjs2: "$",     //同上
            amd: "$",           //如果我們的庫使用require.js等載入,等價於 define(["jquery"], factory);
            root: "$"           //如果我們的庫在瀏覽器中使用,需要提供一個全域性的變數‘$’,等價於 var $ = (window.$) or ($);
        } 
    },
    ...
}
複製程式碼

處理圖片模組

  • url-loader / file-loader(在output目錄裡生成一個檔案,使用import, 並把路徑返回)
// 在js中

import logo from 'logo.jpg'

let oImg = new Image()
oImg.src = logo
document.body.appendChild(oImg)

// 在webpack.config.js
module : {
    rules : [
        {
            test : /\.(jpg|png|git)$/,
            use : {
                loader : 'url-loader',
                options : {
                    fallback : 'file-loader',   // 備用loader,當圖片大小超過limit值時使用
                    limit : 1024 * 200
                }
            }
        }
    ]
}
複製程式碼
  • html-withimg-loader
    {
        test : /\.html$/,
        use : 'html-withimg-loader'     // 把html上的img標籤使用url-loader處理圖片
    }
複製程式碼

打包檔案分類(一般修改對應的filename屬性,特殊除外)

  • clean-webpack-plugin (每次打包都清空輸出目錄)
  • 圖片目錄
{
    test : /\.(jpg|png|git)$/,
    use : {
        loader : 'url-loader',
        options : {
            limit : 1024 * 2,
            fallback : 'file-loader',
            outputPath : '/static/img/'     // 新增outputPath屬性,規定輸出路徑
        }
    }
}
複製程式碼
  • js目錄
output : {
    filename : 'static/js/bundle.[hash:10].js',     // 修改filename
    path : path.resolve(__dirname, 'build')
},
複製程式碼
  • 樣式目錄
new CssPlugin({
    filename : 'static/css/main.[hash:10].css'      // 修改filename
})
複製程式碼

打包多頁應用

  • entry為一個物件
    entry : {
        home : './src/home.js',
        shop : './src/shop.js'
    }
複製程式碼
  • output.filename使用[name]屬性,[name]為entry的屬性
    output : {
        filename : '[name].[hash:8].js',
        path : path.resolve(__dirname, 'build')
    }
複製程式碼
  • 每個HtmlPlugin只能打包一個頁面,如果多頁面,需要使用多個HtmlPlugin
    plugins : [
        new HtmlPlugin({
            template : './src/index.html',
            filename : 'home.html',
            chunks : ['home']       // 程式碼塊,引入指定的js模組,entry的屬性
        }),
        new HtmlPlugin({
            template : './src/index.html',
            filename : 'shop.html',
            chunks : ['shop']       // 程式碼塊,引入指定的js模組,entry的屬性
        })
    ]
複製程式碼

source-map配置

  • 配置devtool屬性 (4種格式)
  • eval系列會整合到打包後的檔案,使檔案變大, 最好不要用
  • devtool : source-map -> 產生單獨的對映檔案,報錯時標識出錯的行和列
  • × devtool : eval-source-map -> 整合到打包後的檔案中,報錯時標識出錯的行和列
  • devtool : cheap-module-source-map -> 產生單獨的檔案對映檔案,報錯時只標識出錯的行
  • × devtool : cheap-module-eval-source-map -> 整合到打包後的檔案中,報錯時只標識出錯的行

檔案實時打包

  • 新增watch屬性和配置watchOptions
    const config = {
        watch : true,   // 開啟實時打包檔案
        watchOptions : {
            poll : 1000,    // 每秒監聽多少次
            ignored : /node_modules/,   // 打包忽略的目錄
            aggregateTimeout : 600,     // 600毫秒防抖
        }
    }
複製程式碼

webpack解決跨域

  • webpack-dev-server預設使用http-proxy-middleware中介軟體實現代理
    // 請求路徑: '/api/user' -> http://127.0.0.1:12306/api/user

    devServer : {
        proxy : {
            // 處理所有/api開頭的請求
            '/api' : {
                target : 'http://127.0.0.1:820',    // 把請求打到 http://127.0.0.1:820
                pathRewrite : {
                    '/api' : ''     // 把請求中的'/api'換成'': 127.0.0.1:12306/api/user -> 127.0.0.1:820/user
                }
            }
        }
    }
複製程式碼
  • devServer的before鉤子函式, 所有請求都會通過這裡
    // 請求路徑: '/user' -> http://127.0.0.1:12306/user

    devServer : {
        before (app) {  // app為express生成的
            app.get('/user', (req, res => {
                res.json({name : 'xyd-before'})
            }))
        }
    }
複製程式碼
  • 在服務端執行webpack,因為都在同一伺服器,則不存在跨域
  • 需要 webpack-dev-middleware中介軟體
    // server.js
    const express = require('express')
    const webpack = require('webpack')
    const webpackMiddle = require('webpack-dev-middleware')
    const webpackConfig = require('./webpack.config.js')

    const app = express()
    const compiler = webpack(webpackConfig)

    app.use( webpackMiddle(compiler) )

    app.listen(820, () => console.log('server start, listening port 820'))

複製程式碼

resolve(模組解析配置)

  • resolve.alias -> 引入模組的別名
    resolve : {
        alias : {
            js : path.resolve(__dirname, 'src/js')      // import 'js/a.js' = import '/src/js/a.js'
        }
    }
複製程式碼
  • resolve.extensions -> 自動解析模組字尾,記得加 .
    resolve : {
        extensions : ['.js', '.css', '.styl', '.json']      // 帶有這些檔案字尾的可以省略
    }
複製程式碼

環境變數

定義環境變數

  • 在開發中判斷環境變數
  • new webpack.DefinePlugin
    // webpack.config.js

        plugins : [
            nwe webpack.DefinePlugin({
                DEV : JSON.stringify('dev')
            })
        ]

    // index.js
    
        let host = ''
        if (DEV === 'dev') {
            host = 'http:127.0.0.1:8080/'
        } else {
            host = 'http:www.xuyede.com/'
        }

複製程式碼

判斷不同環境

  • 使用三個配置檔案, 使用 webpack-merge合併
  • webpack.base.js -> 公用基礎配置
  • webpack.dev.js -> 開發環境配置
  • webpack.prod.js -> 生產環境配置
    //webpack.dev.js

        const merge = require('webpack-merge')
        const base = require('./webpack.base.js')

        const devConfig = merge(base, {
            mode : 'development',
            ...
        })
        module.exports = devConfig
複製程式碼

webpack的優化

externals -> 將依賴的庫指向全域性變數,從而不用打包 (注意需要手動引入CDN min.js檔案)

    externals : {
        'react' : 'window.React',   //對於react這個模組,不打包,直接用cdn檔案
        'jquery' : 'window.jQuery'
    }
複製程式碼

module.noParse -> 匹配的項不解析依賴庫

    module : {
        noParse : /jquery|lodash/,      // 引入jquery|lodash時,不會去解析jquery|lodash相關的依賴庫
    }
複製程式碼

webpack.IgnorePlugin -> 在使用import|require引入模組時,忽略匹配到的模組

    // moment模組預設將所有本地化的內容一起打包,使用IgnorePlugin

        plugins : [
            new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)     // 在moment中, 如果引入 .locale, 就忽略掉
        ]
複製程式碼

動態連線庫 (dll)

建立一個依賴庫,用來伺服業務程式碼 webpack.DllPlugin(dll庫配置檔案) + webpack.DllReferencePlugin(業務程式碼配置檔案) 步驟:

  • 打包dll
  • 引用dll,打包業務程式碼
    //打包dll包,配置dll.config.js

        const path = require('path')
        const webpack = require('webpack')
        const vendors = [
            'react',
            'react-dom',
            // ... 其他庫
        ]
        const dllConfig = {
            mode : 'development',
            entry : {
                '_dll' : vendors
            },
            output : {
                filename : '[name].js',
                path : path.resolve(__dirname, 'build'),
                library : '[name]'      //把 _dll.js的結果匯出到 var _dll變數中
            },
            plugins : [
                new webpack.DllPlugin({
                    path : path.resolve(__dirname, 'build', 'manifest.json'),
                    name : '[name]',    //dll暴露的物件名,必須與output.library一致
                })
            ]
        }   
複製程式碼
    // 引入_dll.js到html中
複製程式碼
    // 打包業務程式碼,第三方庫會先去_dll.js中找,沒有再打包第三方庫

        const webpack = require('webpack')

        const config = {
            plugins : [
                new webpack.DllReferencePlugin({
                    manifest : path.resolve(__dirname, 'build', 'manifest.json')
                })
            ]
        }
複製程式碼
    // 修改 clean-webpack-plugin,避免沒事打包都刪除全部

        plugins : [
            new CleanWebpackPlugin({
                cleanOnceBeforeBuildPatterns : [
                    path.join(process.cwd(), 'build/static/*'),     //只清理static
                ]
            }) 
        ]
複製程式碼

多執行緒打包

  • happypack
  • 將原有的 webpack 對 loader 的執行過程,從單一程式的形式擴充套件多程式模式
  • 不改變原來loader的配置格式
    const HappyPack = require('happypack')
    const os = require('os')
    // 生成一個共享程式池,有os.cpus().length個子程式
    const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })    

    const config = {
        module : {
            rules : [
                {
                    test : /\.js$/,
                                                            |           use : {
                    use : 'happypack/loader?id=happyBabel'  | 使用前->      loader : 'babel-loader'  
                                                            |           }
                    include : path.resolve(__dirname, 'src'),
                    exclude : /node_modules/
                }
            ]
        },
        plugins : [
            new HappyPack({
                id : 'happyBabel',      // 對應上面的id
                loaders : [
                    'babel-loader'     //用法和loader一樣
                ],
                threadPool: happyThreadPool,    //如果有多個HappyPack例項,則啟用該屬性,共享happyThreadPool程式池
            }),
            ...
        ]
    }
複製程式碼
  • happypack 注意點
  • happypack對 file-loader和 url-loader不友好,避免在happypack中使用
  • happypack使用mini-css-extract-plugin需要把 MiniCssPlugin放到主執行緒中處理
    // happypack打包樣式(mini-css-extract-plugin)

        const HappyPack = require('happypack')
        const MiniCssPlugin = require('mini-css-extract-plugin')

        const config = {
            module : {
                rules : [
                    {
                        test : /\.css$/,
                        use : [
                            MiniCssPlugin.loader,        // MiniCssPlugin留在主執行緒中執行
                            'happypack/loader?id=happyCss'
                        ]
                    }
                ]
            },
            plugins : [
                new MinicssPlugin({
                    filename : 'static/css/main.css'
                }),
                new HappyPack({
                    id : 'happyCss',
                    loaders : [
                        'css-loader',
                        'post-loader'
                    ]
                })
            ]
        }

複製程式碼

webpack自帶優化

  • tree-shaking -> 只有import語法才生效,把業務中沒有用到的程式碼自動剔除
  • scope hosting -> 作用域提升

抽離公共程式碼 (把公共模組提取成一個庫)

  • optimization.shpitChunks.cacheGroups
    // 公共模組抽離

        optimization : {
            splitChunks : {     // 分割程式碼塊
                cacheGroups : {     // 快取組
                    common : {
                        chunks : 'initial',     // 初始化時抽離
                        minSize : 0,            // 最小抽離程式碼的大小
                        minChunks : 2,          // 最小抽離程式碼的呼叫次數
                        name : 'common'         // 抽離檔案的名字
                    }
                }
            }
        }
複製程式碼
    // 第三方庫抽離

        optimization : {
            splitChunks : {
                cacheGroups : { 
                    common : { ... },
                    vendor : {
                        priority : 1,           // 快取組的優先順序,數字越大越先處理
                        test : /node_modules/,  // 匹配
                        chunks : 'initial',     // 初始化時抽離
                        minSize : 0,            // 最小抽離程式碼的大小
                        minChunks : 2,          // 最小抽離程式碼的呼叫次數
                        name : 'vendor'         // 抽離檔案的名字
                    }
                }
            }
        }
複製程式碼

懶載入

  • import() -> 原理: 使用jsonp實現動態載入
  • 直接使用 import('./xyd.js') 引入
  • import('./xyd.js) 返回一個Promise物件
    // 假設有一個資原始檔 source.json, 點選按鈕時才請求

    let oBtn = document.createElement('button')
    oBtn.innerHTML = 'request source'

    oBtn.addEventListener('click', () => {
        // 使用懶載入
        import('./source.json')
            .then(data => {
                // ...
            })

    }, null)

    document.body.appendChild(oBtn)

複製程式碼

熱更新

  • 程式碼更改時不會重新整理整個頁面, 只更新更改的模組
  • devServer.hot : true
  • new webpack.NamedModulesPlugin() -> 開啟HMR時可以顯示更新模組的相對路徑
  • new webpack.HotModuleReplacementPlugin() -> 啟用HMR, 介面暴露到 module.hot
    import str from './source'

    if (module.hot) {
        module.hot.accept('./source', () => {   // 當'./source'模組更新的時候,做對應的處理
            console.log('source模組更新了')
            let str = require('./source')   // 模組更新,重新引入
        })
    }

複製程式碼

tapable

相關文章