webpack4.0從入門到放棄

一蜡饵、入坑初探

1. 設(shè)置項(xiàng)目為私有

我們只需要在package.json文件中配置,因?yàn)槭撬接许?xiàng)目不需要向外部暴露的甲献,所以我們可以去掉main: index.js

"private": true

2. 運(yùn)行webpack

一般我們安裝webpack時(shí)會(huì)同時(shí)安裝webpack-cli端壳,它的作用是使我們可以在命令行使用webpack命令,在命令行中執(zhí)行

npx webpack --config webpack.config.js

--config指定webpack執(zhí)行的文件韩玩,如果沒(méi)有垒玲,默認(rèn)是webpack.config.js,因?yàn)槲覀兪窃诿钚兄袌?zhí)行找颓,所以需要npx合愈,如果我們寫在package.json文件中,則只需要"bundle": "webpack"就可以了击狮。

3. webpack簡(jiǎn)單配置

webpack只能識(shí)別后綴是.js的文件佛析,如果是其它類型的文件,就需要引入loader來(lái)幫助我們編譯彪蓬。
下面我們來(lái)做一個(gè)簡(jiǎn)單的對(duì)圖片和css的打包配置:

rules: [
    {
        test: /\.(jpg|png|gif)$/,
        use: {
        loader: 'url-loader',
        options: { // loader額外參數(shù)配置
            name: '[name]_[hash].[ext]', // name: 原來(lái)的名字 ext:原來(lái)的后綴
            outputPath: 'images/', // 輸出路徑
            limit: 10240 // 限制说莫,大于10240kb時(shí)才進(jìn)行此操作,否則直接打到j(luò)s文件中
         }
      } 
      },{
          test: /\.scss$/,
          use: [
                'style-loader',  // 將css掛載到header中
                // options: {
                    // insertAt: 'top' // 插到頂部
                // },
            'css-loader',  // 分析當(dāng)前有幾個(gè)css文件寞焙,將css文件整合,分析@import這種語(yǔ)法
            'sass-loader',
            'postcss-loader'
          ]
     }
]

file-loader和url-loader的區(qū)別是url-loader會(huì)把圖片等(任何文件)文件直接打包到j(luò)s中,如果圖片很小捣郊,我們可以使用這種方式辽狈,如果圖片較大,我們就需要將圖片打包到統(tǒng)一的images目錄中呛牲,在上面代碼中我們做了一個(gè)限制刮萌,當(dāng)圖片大于10kb時(shí),就打包到images目錄中娘扩,否則直接打包到j(luò)s中

注意loader執(zhí)行順序是從下到上執(zhí)行的着茸,如css這里,執(zhí)行順序?yàn)?
postcss-loader->sass-loader->css-loader->style-loader

最后我們?cè)賮?lái)看打包完命令行中的展示琐旁,如下圖所示:


image.png

Chunks: 打包的js的id涮阔,
Chunk Names: 打包的js名字

二、loader篇

1. css相關(guān)loader

{
    test: /\.scss$/,
    use: [
        'style-loader', 
        {
            loader: 'css-loader',
            options: {
                importLoaders: 2, // 如果當(dāng)前引入的scss文件又引入了其它scss文件灰殴,讓引入的scss文件也需要通過(guò)postcss-loader,sass-loader編譯敬特,如果不加,就會(huì)直接走css-loader牺陶,2代表前兩個(gè)伟阔,幾就代表前幾個(gè)
                modules: true // 開啟css模塊化,開啟后css需要用模塊化引入的寫法
            }
        },
        'sass-loader',
        'postcss-loader'
    ]
}

關(guān)于配置css-next的方法查看postcss-loader的文檔:https://webpack.js.org/loaders/postcss-loader

2. 打包字體文件

{
    test: /\.(eot|ttf|svg)$/,
    use: {
        loader: 'file-loader'
    } 
}

打包字體文件用file-loader把字體文件打包到dist目錄中就可以了

三掰伸、webpack基礎(chǔ)

plugins相當(dāng)于vue皱炉,react中的鉤子,可以在webpack運(yùn)行到某個(gè)時(shí)刻的時(shí)候狮鸭,幫助我們做一些事情

1. html-webpack-plugin

我們需要自動(dòng)生成一個(gè)html文件合搅,把打包生成的js自動(dòng)引入到這個(gè)html文件中

plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html' // 指定模版文件
    })

2. CleanWebpackPlugin

我們需要在每次打包后刪掉上一次的打包文件

new CleanWebpackPlugin(['dist'])]

關(guān)于dist目錄和webpack配置文件不在同一個(gè)根目錄下,我們需要如下解決方法

new CleanWebpackPlugin(['dist'], {
    root: path.resolve(__dirname, '../')
})

2. copyWebpackPlugin

有些時(shí)候我們需要拷貝一些靜態(tài)資源文件到dist目錄

new CopyWebpackPlugin([
   {from: 'doc', to: './'}
])

2. bannerPlugin

版權(quán)聲明插件怕篷,可以在我們打包生成的文件前生成一些版權(quán)信息等

new webpack.BannerPlugin('zxhnext@qq.com')

3. 打包多份js历筝,指定cdn引用路徑

首先我們需要配置多入口

entry: {
    main: './src/index.js',
    sub: './src/index.js'
}

出口處我們不能寫死一個(gè)名字,否則會(huì)因打包處兩份相同的文件而報(bào)錯(cuò)

output: {
    publicPath: 'http://cdn.com.cn', // 設(shè)置前綴(cdn地址)
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
}

4. sourceMap

devtool: 'cheap-module-eval-source-map'  // development
devtool: 'cheap-module-source-map'  // production

一般在開發(fā)環(huán)境中我們使用cheap-module-eval-source-map廊谓,在線上環(huán)境使用cheap-module-source-map梳猪,如果要關(guān)閉sourceMap我們需要把devtool置為none
cheap:1. 只指出哪一行出錯(cuò),不指出哪一頁(yè)蒸痹。2. 只報(bào)我們的業(yè)務(wù)代碼春弥,不處理loader等中的代碼錯(cuò)誤。
module:指出loader等中的錯(cuò)誤
source-map: 生成一個(gè).map文件
inline: 將映射文件放到main.js中
eval: 將業(yè)務(wù)代碼與 以及source-map通過(guò)eval方式執(zhí)行叠荠,速度最快
具體用法參考官方文檔:https://webpack.js.org/configuration/devtool/#devtool

5. 熱啟動(dòng)

5.1 通過(guò)shell腳本

"watch": "webpack --watch",

我們只需要在package.json文件中設(shè)置watch即可匿沛,但是這種方法存在很多缺陷,如果我們需要開啟一個(gè)本地服務(wù)榛鼎,那么我們需要使用webpack-dev-server

5.2. webpack-dev-server

devServer: {
    contentBase: './dist',
    open: true, // 是否打開瀏覽器
    port: 8080
}

我們需要注意的是逃呼,使用webpack-dev-server時(shí)我們并未發(fā)現(xiàn)有dist目錄鳖孤,這時(shí)因?yàn)閣ebpack-dev-server將打包好的文件隱藏到計(jì)算機(jī)的內(nèi)存中了,這樣執(zhí)行更快抡笼。
關(guān)于webpack-dev-server的更多配置參考官網(wǎng):https://webpack.js.org/configuration/dev-server

下面我們來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的webpack-dev-server

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');

// 編譯
const complier = webpack(config);
const app = express();
// 在應(yīng)用里使用webpack
app.use(webpackDevMiddleware(complier, {
  // config.output.publicPath
}));
app.listen(3000, () => {
    console.log('server is running');
});

在命令行中使用webpack語(yǔ)法:https://www.webpackjs.com/api/cli/

在node中使用webpack: https://www.webpackjs.com/api/node/

6. Hot Module Replacement 熱模塊更新

當(dāng)我們每次修改代碼時(shí)苏揣,頁(yè)面都會(huì)整個(gè)刷新,這樣豈不是很麻煩推姻,有沒(méi)有辦法只更新被修改的部分平匈,而不刷新整個(gè)頁(yè)面,這時(shí)我們需要用到HotModuleReplacementPlugin

const webpack = require('webpack');
module.exports = {
    ...
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true,
        hotOnly: true
    },
    ...
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
}

這里我們要注意的是藏古,必須在devServer加上hot: true,hotOnly: true增炭,Hot Module Replacement才會(huì)生效
hotOnly: HotModuleReplacementPlugin失效時(shí),重新刷新一次頁(yè)面

修改了某個(gè)文件后拧晕,我們就需要手動(dòng)去更新了

if(module.hot) {
    module.hot.accept('./number', () => {
        document.body.removeChild(document.getElementById('number'));
        number();
    })
}

然而我們平常用css隙姿,vue和react等的時(shí)候并沒(méi)有這么去做,這是因?yàn)槭且驗(yàn)閏ss-loader防症,vue-loader孟辑,react-loader中自動(dòng)幫我們實(shí)現(xiàn)了

7. Babel 處理 ES6 語(yǔ)法

這里我們要參考babel官方文檔:https://babeljs.io/setup#installation,下面我們先來(lái)做一個(gè)簡(jiǎn)單的配置:

{ 
    test: /\.js$/, 
    exclude: /node_modules/, 
    // include: path.resolve(__dirname, '../src'), // 只檢測(cè)某個(gè)目錄蔫敲,exclude除掉某個(gè)目錄
    loader: 'babel-loader',
    options: {
        presets: [['@babel/preset-env', { // @babel/preset-env將es6轉(zhuǎn)為es5
            useBuiltIns: 'usage'
        }]]
    }
}

這里我們需要注意饲嗽,我們需要配置exclude: /node_modules/, 否則這里也會(huì)去匹配node_modules中的js文件,同時(shí)我們可以看到奈嘿,如果所有配置都寫在webpack.config.js中貌虾,那將會(huì)變得非常復(fù)雜,所以這里建議新建一個(gè).babelrc文件裙犹,將babel-loader中的配置放在.babelrc中尽狠,如下所示:

{
    presets: [
        [
            "@babel/preset-env", {
                targets: {
                    chrome: "67", // 支持哪個(gè)版本以上的瀏覽器
                },
                useBuiltIns: 'usage' // 實(shí)現(xiàn)按需加載
            }
        ]
    ]
}

在有些低版本瀏覽器中是不支持es5的一些語(yǔ)法的,這時(shí)我們需要@babel/polyfill幫我們解決叶圃,我們直接在入口文件中main.js引入@babel/polyfill即可

import "@babel/polyfill";

但是我們?cè)趺磳?shí)現(xiàn)按需加載呢袄膏,我們?cè)?babelrc中添加useBuiltIns: 'usage'
如果配置了useBuiltIns: 'usage',會(huì)默認(rèn)引入@babel/polyfill掺冠,不需要手動(dòng)調(diào)用
參見官網(wǎng):https://babeljs.io/docs/en/babel-polyfill

8. 類庫(kù)的配置

當(dāng)我們寫一個(gè)類庫(kù)時(shí)沉馆,我們可以用@babel/plugin-transform-runtime,相比@babel/polyfill德崭,它是通過(guò)閉包實(shí)現(xiàn)依賴注入斥黑,這樣做不會(huì)污染全局環(huán)境

{
    "plugins": [["@babel/plugin-transform-runtime", {
        "corejs": 2, // 設(shè)為2可以實(shí)現(xiàn)按需引入而不是全局引入,設(shè)為2后需要安裝@babel/runtime-corejs2
        "helpers": true,
        "regenerator": true,
        "useESModules": false
    }]]
}

9. watch用法

watch: true,
watchOptions: { // 監(jiān)控的選項(xiàng)
    poll: 1000, // 每秒監(jiān)控多少次
    aggregateTimeout: 500, // 防抖眉厨,停止輸入500ms后再打包
    ignored: /node_modules/ // 不需要監(jiān)控的文件夾
}

四锌奴、Webpack進(jìn)階

1. Tree-shaking

Tree-shaking大意就是只打包我們有使用的代碼,將無(wú)用的部分去掉憾股,舉例如下:
我們有一個(gè)math.js的方法庫(kù)鹿蜀,內(nèi)容如下

export const add = (a, b) => {
    console.log( a + b );
}

export const minus = (a, b) => {
    console.log( a - b );
}

然后我們?cè)趇ndex.js中使用math.js的add方法

import { add } from './math.js';

add(1, 7);

這里有一點(diǎn)我們需要注意箕慧,Tree-shaking只支持import這種ES Module,不支持require這種形式的茴恰。
雖然我們只引入了add方法,但是webpck默認(rèn)把math.js中所有的文件都幫我們打包了销钝,如何做到只打包我們使用的部分代碼呢?這時(shí)我們需要在webpack中作如下配置

plugins: [],
...
optimization: {
    usedExports: true
},

然后我們需要在package.json文件中這樣配置:

"sideEffects": [ // 不對(duì)下面的文件進(jìn)行tree shaking
    "@babel/polly-fill",
    "*.css"
]

首先來(lái)解釋下它是什么意思琐簇,即忽略掉哪些模塊不做Tree-shaking,首先我們要忽略所有的css文件座享,其次如果像import @babel/polyfill這種形式的婉商,我們沒(méi)有引入任何東西,webpack會(huì)自動(dòng)幫我們忽略掉渣叛,這樣打包文件就出錯(cuò)了

在生產(chǎn)環(huán)境tree shaking 是自動(dòng)生效的丈秩,不用再webpack中做配置,但是我們依然需要在package.json中需要配置

"sideEffects": false // false代表沒(méi)有需要忽略的文件

2. Develoment 和 Production

我們仿照create-react-app淳衙,創(chuàng)建build目錄存放我們的weback配置文件蘑秽,首先我們將公用文件提到webpack.common.js,然后我們用webpack-merge合并箫攀,如下所示:

const commonConfig = require('./webpack.common.js');
const devConfig = {
  ...
}
module.exports = merge(commonConfig, devConfig);

因?yàn)槲覀儗ebpack配置文件放在了build目錄中肠牲,此時(shí)dist與webpack配置文件不在同一根目錄下,這是我們需要解決dist和webpack不在同一個(gè)根目錄下而產(chǎn)生的clean插件無(wú)法刪除dist目錄問(wèn)題靴跛,解決方法如下:

new CleanWebpackPlugin(['dist'], {
    root: path.resolve(__dirname, '../')
})

3. Code Splitting代碼分割

3.1 多入口打包方法

如果我們想把引入的模塊單獨(dú)打包缀雳,我們需要單獨(dú)創(chuàng)建一個(gè)文件引入這個(gè)包,然后掛載到window上梢睛,再在入口處引入這個(gè)文件
這里我們以lodash為例:
新建lodash.js文件肥印,內(nèi)容如下:

import _ from 'lodash';
window._ = _;

然后我們?cè)趀ntry引入這個(gè)包,

entry: {
    lodash: './src/lodash.js',
    main: './src/index.js'
}

3.2 配置optimization

在webpack中我們可以配置chunks來(lái)自動(dòng)幫我們做(同步)代碼分割

optimization: {
    splitChunks: {
        chunks: 'all'
    }
}

3.3 異步模塊打包

異步模塊不需要我們做任何配置绝葡,webpack會(huì)自動(dòng)幫我們將異步代碼打包到另一個(gè)文件中深碱。
在使用異步加載的寫法時(shí)(vue中懶加載模塊),我們需要安裝@babel/plugin-syntax-dynamic-import藏畅,然后在.babelrc中配置

{
    presets: [
        [
            "@babel/preset-env", {
                targets: {
                    chrome: "67",
                },
                useBuiltIns: 'usage'
            }
        ]
    ],
    plugins: ["@babel/plugin-syntax-dynamic-import"]
}

異步代碼寫法

function getComponent() {
    return import('lodash').then(({ default: _ }) => {
        var element = document.createElement('div');
        element.innerHTML = _.join(['Dell', 'Lee'], '-');
        return element;
    })
}

getComponent().then(element => {
    document.body.appendChild(element);
});
// es7寫法
async function getComponent() {
    const { default: _ } = await import(/* webpackChunkName:"lodash" */ 'lodash');
    const element = document.createElement('div');
    element.innerHTML = _.join(['Dell', 'Lee'], '-');
    return element;
}

document.addEventListener('click', () =>{
    getComponent().then(element => {
        document.body.appendChild(element);
    });
})

import(/* webpackChunkName:"lodash" */ 'lodash');

這是魔法注釋敷硅,加上后,打包出來(lái)的js會(huì)是你注釋的值墓赴,否則為一個(gè)id(如0)值

4. SplitChunksPlugin 配置參數(shù)

splitChunks默認(rèn)配置,當(dāng)我們寫一個(gè)splitChunks: {}竞膳,默認(rèn)等于如下

splitChunks: {
    chunks: "async", // async 只對(duì)異步代碼生效, all同步異步都生效诫硕, initial同步生效
    minSize: 30000, // 文件大于多少時(shí)才會(huì)打包
    //maxSize: 0, // 會(huì)嘗試對(duì)大于多少的文件再次分割為兩個(gè)小文件
    minChunks: 1, // 當(dāng)一個(gè)模塊至少被用了幾次后才做代碼分割
    maxAsyncRequests: 5, // 最多分割幾個(gè)包
    maxInitialRequests: 3, // 入口文件引入的庫(kù)最多能分割成幾個(gè)包
    automaticNameDelimiter: '~', // 生成文件名字中間的連接符
    name: true, // 使cacheGroups中設(shè)置的文件名有效
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10 // 優(yōu)先級(jí)的意思坦辟,如果同時(shí)滿足vendors和default,這個(gè)值誰(shuí)大就打包到哪個(gè)組章办,-10大于-20
        },
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true // 如果引入的某個(gè)文件之前已經(jīng)被打包過(guò)锉走,就不會(huì)被打包了滨彻,會(huì)直接去復(fù)用之前的
        }
    }
}

chunks: "all"時(shí)我們需要注意,這時(shí)webpack會(huì)繼續(xù)找到cacheGroups挪蹭,vendors中的test表示被打包的文件是否在node_modules這個(gè)文件夾中亭饵,如果是的話,就會(huì)打包到vendors這個(gè)組中梁厉,這時(shí)打包出來(lái)的文件名字應(yīng)該是vendors~main.js,main是定義的入口文件名字辜羊,如果我們想指定一個(gè)名字,可以在vendors中設(shè)置filename指定一個(gè)名字

default是指如果不符合vendors中的要求的文件词顾,比如我們自己寫的一個(gè)包八秃,這個(gè)包并不在node_modules中,這時(shí)會(huì)分到default組中
cacheGroups作用是做一個(gè)緩存組肉盹,如果我們引入了多個(gè)包昔驱,就會(huì)分割成很多模塊,而cacheGroups作用就是先將需要打包的文件緩存起來(lái)上忍,然后統(tǒng)一打包到一個(gè)組中

vendors, default也可以設(shè)置為false

5. 打包分析骤肛,Preloading, Prefetching

5.1 打包分析

在package.json中設(shè)置一個(gè)下面的命令,然后運(yùn)行

"dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js"

會(huì)生成一個(gè)stats.json文件窍蓝,這是一個(gè)對(duì)打包過(guò)程的描述文件腋颠,借助一些工具我們可以進(jìn)行分析。
參考analyse:https://github.com/webpack/analyse
參考官網(wǎng):https://webpack.js.org/guides/code-splitting/#bundle-analysis

5.2 代碼使用率

在瀏覽器調(diào)試工具中按command+shift+p它抱,然后我們選擇show coverage選項(xiàng)秕豫,可以查看代碼的使用率,代碼使用率越高說(shuō)明優(yōu)化的越好观蓄,所以我們開發(fā)時(shí)盡量多寫異步的代碼混移,這樣代碼使用的時(shí)候才會(huì)去加載
如下所示:

// click.js
function handleClick() {
    const element = document.createElement('div');
    element.innerHTML = 'Dell Lee';
    document.body.appendChild(element);
}

export default handleClick;

// index.js
document.addEventListener('click', () =>{
    import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => {
        func();
    })
});

Prefetching是等主代碼加載完才會(huì)加載,Preloading是與主代碼同時(shí)加載

6. CSS代碼分割

這里我們需要使用mini-css-extract-plugin
參考官網(wǎng):https://webpack.js.org/plugins/mini-css-extract-plugin

6.1 我們先來(lái)看一下output內(nèi)容:

output: {
    filename: '[name].js',
    chunkFilename: '[name].chunk.js',
    path: path.resolve(__dirname, '../dist')
}

這里說(shuō)一下filename與chunkFilename的區(qū)別:
入口文件的打包用filename侮穿,chunk文件打包用chunkFilename

6.2 分割css

如果我們不分割css歌径,webpack會(huì)默認(rèn)把css打包到j(luò)s文件中,這是我們不希望看到的亲茅,下面來(lái)看下mini-css-extract-plugin的使用方法回铛。注意,如果打包失敗克锣,需要看一下是不是package.json文件中這里配置有誤茵肃,可能是tree shaking影響了

"sideEffects": "false"
// 改為
"sideEffects": ["*.css"]
{
    test: /\.css$/,
    use: [
        MiniCssExtractPlugin.loader,
        'css-loader',
        'postcss-loader'
    ]
}

plugins: [
    new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].chunk.css'
    })
]

6.3 css壓縮

我們還可以對(duì)css進(jìn)行壓縮,這時(shí)我們需要用到optimize-css-assets-webpack-plugin
然后配置如下:

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})]
}

6.4 多入口的css打包到一個(gè)css中

這個(gè)配置意思是只要是css文件就打包到這個(gè)組中

optimization: {
    splitChunks: {
        cacheGroups: {
            styles: {
                name: 'styles',
                test: /\.css$/,
                chunks: 'all',
                enforce: true
            }
        }
    }
}

enforce為true表示忽略其它的默認(rèn)參數(shù)

6.5 不同入口打包到不同組

參考官網(wǎng):https://webpack.js.org/plugins/mini-css-extract-plugin

6.6 去掉性能上的警告

performance: false, // 去掉性能上的警告
output: {
    path: path.resolve(__dirname, '../dist')
}

7. runtimeChunk

配置runtimeChunk是因?yàn)樵谝恍├习姹镜膚ebpack中袭祟,manifest(包與包之間的關(guān)系)文件是加在main與vendors文件中的验残,這樣會(huì)導(dǎo)致即使我們沒(méi)有更改文件,但是包與包之間的關(guān)系變了而引起的contenthash發(fā)生變化巾乳,這時(shí)我們就需要這樣配置將這部份代碼抽離出來(lái)您没,在新版webpack中不會(huì)出現(xiàn)這個(gè)問(wèn)題

optimization: {
    runtimeChunk: {
        name: 'runtime'
    },
    usedExports: true,
    splitChunks: {
        chunks: 'all',
        cacheGroups: {
            vendors: {
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                name: 'vendors',
            }
        }
    }
},

8. Shimming

一些第三方的庫(kù)(library)可能會(huì)引用一些全局依賴(例如 jQuery 中的 $)鸟召。這些庫(kù)也可能創(chuàng)建一些需要被導(dǎo)出的全局變量。這些“不符合規(guī)范的模塊”就是 shimming 發(fā)揮作用的地方氨鹏。

8.1 全局引入

new webpack.ProvidePlugin({
    $: 'jquery',
    _join: ['lodash', 'join']
})

當(dāng)發(fā)現(xiàn)一個(gè)模塊中用了$時(shí)欧募,會(huì)在模塊中默認(rèn)引入jquery
如果需要使用模塊中的某個(gè)方法,我們可以用一個(gè)數(shù)組的方式定義

8.2 修改this指向

每個(gè)模塊的this指向的都是模塊自身仆抵,如果想讓this指向window,需要imports-loader插件跟继,然后我們?cè)僮鋈缦屡渲茫?/p>

{ 
    test: /\.js$/, 
    exclude: /node_modules/,
    use: [{
        loader: 'babel-loader'
    }, {
        loader: 'imports-loader?this=>window'
}

9. 環(huán)境變量的使用

module.exports = (env) => {
    if(env && env.production) {
        return merge(commonConfig, prodConfig);
    }else {
        return merge(commonConfig, devConfig);
    }
}

然后在package.json中設(shè)置環(huán)境變量

"build": "webpack --env.production --config ./build/webpack.common.js"

五、webpack高級(jí)使用技巧

1. 類庫(kù)代碼打包

我們對(duì)package.json進(jìn)行設(shè)置

"license": "MIT", // 開源

然后在output中做如下設(shè)置:

output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'library.js',
    library: 'library', // 可以script標(biāo)簽引入镣丑,在全局掛載了一個(gè)library變量
    libraryTarget: 'umd' // 使支持amd,cmd,require等語(yǔ)法
}

還可以用如下寫法

library: 'library', // 可以script標(biāo)簽引入还栓,在全局掛載了一個(gè)library變量
libraryTarget: this' // 這兩個(gè)配合就不支持amd等寫法了,只會(huì)掛載一個(gè)全局變量
lodash : {
      commonjs: 'lodash', // 通過(guò)require(common.js)引入時(shí)传轰,名字必須叫l(wèi)odash
      amd: 'lodash',
      root: '_' // 通過(guò)script標(biāo)簽引入時(shí)必須在全局掛載一個(gè)_變量
}

const lodash = require('lodash')  // commonjs設(shè)置的意思是const后的名字必須叫l(wèi)odash

如果我們編寫的庫(kù)中引入了其它包,我們不希望引入的包被打包谷婆,這時(shí)我們可以設(shè)置

module.exports = {
    ...
    externals: 'lodash',
    output: {
        ...
    }
}

這里寫成一個(gè)數(shù)組慨蛙,對(duì)象,字符串形式都可以纪挎,對(duì)象形式:

module.exports = {
    ...
    externals: {
        lodash: {
            commonjs: 'lodash'
        }
    },
    output: {
        ...
    }
}

參考官網(wǎng):https://webpack.js.org/configuration/externals/#externals

最后我們需要把package.json的入口文件改為

"main": "./dist/library.js",

然后在npm注冊(cè)一個(gè)賬號(hào)期贫,
然后npm adduser添加用戶名和密碼
再npm publish

2. PWA 的打包配置

安裝workbox-webpack-plugin
在plugins中配置:

new WorkboxPlugin.GenerateSW({
    clientsClaim: true,
    skipWaiting: true
})

js文件為

if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
        .then(registration => {
            console.log('service-worker registed');
        }).catch(error => {
            console.log('service-worker register error');
        })
    })
}

3. TypeScript 的打包配置

我們需要安裝ts-loader typescript
在rules中配置:

{
    test: /\.tsx?$/, // ?代表可有可無(wú)
    use: 'ts-loader',
    exclude: /node_modules/
}

同時(shí)創(chuàng)建tsconfig.json文件,做如下配置

{
    "compilerOpitons": {
        "outDir": "./dist",
        "module": "es6", // 使用es6的模塊引入方法
        "target": "es5", // 轉(zhuǎn)換為es5形式
        "allowJs": true // 允許ts中引入js文件
    }
}

一般庫(kù)的typescript版本都是@types/名字异袄,可以參考:https://github.com/DefinitelyTyped/DefinitelyTyped

4. WebpackDevServer 實(shí)現(xiàn)請(qǐng)求轉(zhuǎn)發(fā)

注意本章只在開發(fā)環(huán)境生效通砍,對(duì)生產(chǎn)環(huán)境沒(méi)有影響

4. 1. 代理接口

devServer: {
  proxy: {
    // index: '', // 如果要代理根路徑,需要把index設(shè)置為false或者''
    '/react/api': {
      target: 'https://www.dell-lee.com', // 代理請(qǐng)求接口
      secure: false, // 如果是https網(wǎng)址烤蜕,這里需要設(shè)置為false
      pathRewrite: { // 代理接口封孙,訪問(wèn)header.json時(shí)會(huì)幫你請(qǐng)求demo.json
        'header.json': 'demo.json'
      },
      changeOrigin: true, // 后端可能設(shè)置了changeOrigin防止爬蟲,這里我們?cè)O(shè)置true以后就可以避開這個(gè)限制了
      headers: { // 設(shè)置請(qǐng)求頭
        host: 'www.dell-lee.com',
        cookie: ....
      },
      bypass: function(req, res, proxyOptions) { // 攔截讽营,如果請(qǐng)求的是一個(gè)html內(nèi)容虎忌,則返回index.html
        if (req.headers.accept.indexOf('html') !== -1) {
          console.log('Skipping proxy for browser request.');
          return '/index.html';
        }
      }
    }
  }
}

webpackdevserver proxy底層用了 http-proxy-middleware這個(gè)插件

如何使用mock數(shù)據(jù)

devServer: {
    before(app) {
        app.get('/user', (req, res) => {
            res.json(....)
        })
    }
}

4. 2. WebpackDevServer 解決單頁(yè)面應(yīng)用路由問(wèn)題

當(dāng)不使用hash路由時(shí),我們可以設(shè)置以下內(nèi)容

historyApiFallback: true, // 把對(duì)服務(wù)器的請(qǐng)求都轉(zhuǎn)換為對(duì)跟路徑的請(qǐng)求
historyApiFallback: {
      rewrites: [ // 訪問(wèn)abc.html時(shí)代理到index.html
        { from: /abc.html/, to: '/views/index.html' }
      ]
}

historyApiFallback: true相當(dāng)于

historyApiFallback: {
      rewrites: [
        { from: /\.*\/, to: '/index.html' }
      ]
}

底層用了connect-history-api-fallback這個(gè)插件

5. EsLint 在 Webpack 中的配置

安裝eslint
npx eslint --init

module.exports = {
    "extends": "airbnb", // 使用那個(gè)規(guī)則
    "parser": "babel-eslint", // 解析器
    "rules": {
        "react/prefer-stateless-function": 0,
        "react/jsx-filename-extension": 0
    },
    globals: {
        document: false // 不允許覆蓋全局變量document
    }
};

在webpack中使用eslint
安裝eslint-loader:https://webpack.js.org/loaders/eslint-loader

{ 
    test: /\.js$/, 
    exclude: /node_modules/, 
    use: ['babel-loader', 'eslint-loader']
}

同時(shí)配置overlay: true,eslint有錯(cuò)會(huì)在瀏覽器中提示

devServer: {
    overlay: true
}

設(shè)置force為pre代表強(qiáng)制先執(zhí)行橱鹏,fix會(huì)自動(dòng)修復(fù)一些項(xiàng)目中eslint簡(jiǎn)單的錯(cuò)誤

{ 
    test: /\.js$/, 
    exclude: /node_modules/, 
    use: [
        {
            loader: 'eslint-loader',
            options: {
                fix: true
            },
            force: 'pre'
         },
        'babel-loader'
    ]
}

六膜蠢、webpack 性能優(yōu)化

1. 經(jīng)常更新版本

2. 使用loader時(shí)指定檢測(cè)目錄,圖片沒(méi)有必要

{ 
    test: /\.js$/, 
    include: path.resolve(__dirname, '../src'), // 只檢測(cè)某個(gè)目錄莉兰,exclude除掉某個(gè)目錄
    use: [{
        loader: 'babel-loader'
    }]
}

3. 盡少使用plugin挑围,盡可能精簡(jiǎn)并確保可靠

4. 合理配置resolve

resolve: {
    extensions: ['.js', '.jsx'],
    mainFIles: ['index', 'child']
},

當(dāng)一個(gè)引入的文件沒(méi)有后綴時(shí)糖荒,會(huì)識(shí)別它是不是.js杉辙,.jsx文件
引入一個(gè)目錄,回去查找目錄下是否有index寂嘉,child文件
給文件或路徑設(shè)置別名

resolve: {
    extensions: ['.js', '.jsx'],
    alias: {
        child: path.resolve(__dirname, '../src/child')
    }
},

5. 第三方模塊只打包一次

新建一個(gè)webpack.dll.js, 運(yùn)行它對(duì)第三方模塊單獨(dú)打包奏瞬,并生成vendors.manifest.json映射文件

const path = require('path');
const webpack = require('webpack');

module.exports = {
    mode: 'production',
     entry: {
        vendors: ['lodash'],
        react: ['react', 'react-dom'],
        jquery: ['jquery']
    },
    output: {
        filename: '[name].dll.js',
        path: path.resolve(__dirname, '../dll'),
        library: '[name]' // 將它暴露出去
    },
    plugins: [
        new webpack.DllPlugin({
            name: '[name]',
            path: path.resolve(__dirname, '../dll/[name].manifest.json'),
        })
    ]
}

然后再配置webpack.common.js

const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

new webpack.DllReferencePlugin({ // 查找vendors.manifest.json枫绅,如果發(fā)現(xiàn)這里有,就不會(huì)再重復(fù)打這個(gè)包
    manifest: path.resolve(__dirname, '../dll', '../dll/vendors.manifest.json')
})
new AddAssetHtmlWebpackPlugin({ // 向html中添加引入某個(gè)文件
    filepath: path.resolve(__dirname, '../dll',  '../dll/vendors.dll.js')
})

自動(dòng)化引入

const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
    if(/.*\.dll.js/.test(file)) {
        plugins.push(new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../dll', file)
        }))
    }
    if(/.*\.manifest.json/.test(file)) {
        plugins.push(new webpack.DllReferencePlugin({
             manifest: path.resolve(__dirname, '../dll', file)
        }))
    }
})

6. 控制包文件大小

7. thread-loader硼端,parallel-webpack并淋,happypack多線程打包

let happypack = require('happypack');

rules: [
  {
      test: /\.js$/,
      exclude: /node_modules/,
      include: path.resolve('src'),
      use: 'happypack/loader?id=js'
  }
]

plugins: [
    new happypack({
        id: 'js',
        use: [{
            loader: 'babel-loader',
            options: {
                ...
            }
        }]
    })
]

8. 合理使用sourceMap

9. 開發(fā)環(huán)境內(nèi)存編譯

webpackdevserver用的就是內(nèi)存編譯

10. 開發(fā)環(huán)境無(wú)用插件剔除

11. noParse

module: {
    noParse: /jquery/, 不去解析jquery中的依賴庫(kù)
}

12. ignoreplugin

忽略掉我們不需要引入的包文件中的部分內(nèi)容

// 我們不需要引入moment這個(gè)包里的/locale文件夾,就把它忽略掉
new webpack.IgnorePlugin(/\.\/locale/,/moment/)

七珍昨、多頁(yè)面打包配置

多入口

entry: {
    index: './src/index.js',
    list: './src/list.js',
    detail: './src/detail.js',
}

生成多個(gè)html

new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'index.html',
    chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}), 
new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'list.html',
    chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}), 
new HtmlWebpackPlugin({
    template: 'src/index.html',
    filename: 'detail.html',
    chunks: ['runtime', 'vendors', 'list'] // 指定引入文件
}), 

自動(dòng)化方式

const makePlugins = (configs) => {
    const plugins = [
        new CleanWebpackPlugin(['dist'], {
            root: path.resolve(__dirname, '../')
        })
    ];
    Object.keys(configs.entry).forEach(item => {
        plugins.push(
            new HtmlWebpackPlugin({
                template: 'src/index.html',
                filename: `${item}.html`,
                chunks: ['runtime', 'vendors', item]
            })
        )
    });
    return plugins;
}

八县耽、webpack原理篇

1. 編寫一個(gè) Loader

1.1 同步操作

新建loader文件夾,在文件夾中新建replaceLoader.js文件

const loaderUtils = require('loader-utils');

module.exports = function(source) { // 注意這里不能使用箭頭函數(shù)镣典,我們需要變更this指向來(lái)調(diào)用this中的一些方法
    return source.replace('lee', 'world');
}

然后我們?cè)趙ebpack.config.js中引入

rules: [{
    test: /\.js/,
    use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
}]

同時(shí)我們還可以傳入一些參數(shù)

rules: [{
    test: /\.js/,
    use: [
        {
            loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
            options: {
                name: 'zxh'
            }
        }
    ]
}]

這時(shí)我們就可以在replaceLoader.js兔毙,通過(guò)this.query可以接收到options中的內(nèi)容

module.exports = function(source) { 
    return source.replace('hello',  this.query.name);
}

或者我們可以通過(guò)webpack官方提供的loader-utils模塊,使用方法如下

const loaderUtils = require('loader-utils');
module.exports = function(source) {
    const options = loaderUtils.getOptions(this);
    const result = source.replace('dell', options.name);
    return source.replace('hello',  options.name);
}

想要返回多個(gè)值時(shí)可以用this.callback


image.png
const loaderUtils = require('loader-utils');

module.exports = function(source) {
    const options = loaderUtils.getOptions(this)
    const result = source.replace('dell', options.name);
    this.callback(null, result, source, mata)
}

1.2 使用異步操作 this.async

const loaderUtils = require('loader-utils');

module.exports = function(source) {
   const options = loaderUtils.getOptions(this);
   const callback = this.async(); // 聲明是異步操作

   setTimeout(() => {
       const result = source.replace('dell', options.name);
       callback(null, result);
   }, 1000);
}

引入模塊時(shí)兄春,會(huì)來(lái)node_modules中找澎剥,找不到了再來(lái)loaders文件夾中找,這時(shí)我們就可以像引入node_modules中的loader那樣寫了

entry: {
    main: './src/index.js'
},
resolveLoader: {
    modules: ['node_modules', './loaders']
},
module: {
    rules: [{
        test: /\.js/,
        use: [
            {
                loader: 'replaceLoader',
            }
        ]
    }]
}

2. 編寫一個(gè) Plugin

發(fā)布赶舆,訂閱設(shè)計(jì)模式
https://webpack.js.org/api/compiler-hooks

class CopyrightWebpackPlugin {

    apply(compiler) {

        compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => { // 同步哑姚,不用傳callback
            console.log('compiler');
        })

        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { // emit是異步的,我們需要在后面寫tapAsync芜茵,打包完放到文件夾時(shí)叙量,compiler是所有打包文件,compilation是本次打包文件
            debugger;
            compilation.assets['copyright.txt']= {
                source: function() { // 內(nèi)容
                    return 'copyright by dell lee'
                },
                size: function() { // 文件長(zhǎng)度
                    return 21;
                }
            };
            cb(); // 最后必須調(diào)一下cb()
        })
    }

}

module.exports = CopyrightWebpackPlugin;

開啟node調(diào)試工具

"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"

3. Bundler源碼編寫

安裝cli-highlight:命令行高亮顯示工具

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser'); // 幫助分析源代碼
const traverse = require('@babel/traverse').default; // 幫助遍歷module
const babel = require('@babel/core');

const moduleAnalyser = (filename) => {
    const content = fs.readFileSync(filename, 'utf-8'); // 讀取文件內(nèi)容
    const ast = parser.parse(content, { // 抽象語(yǔ)法樹九串,ast
        sourceType: 'module' // 如果是es6模塊方法绞佩,這里需要設(shè)置
    });
    const dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) { // 如果有引入語(yǔ)句,就執(zhí)行
            const dirname = path.dirname(filename);
            const newFile = './' + path.join(dirname, node.source.value); // 改為相對(duì)根目錄的路徑
            dependencies[node.source.value] = newFile;
        }
    });
    const { code } = babel.transformFromAst(ast, null, { // 將ast抽象語(yǔ)法樹轉(zhuǎn)換為瀏覽器可以識(shí)別的代碼
        presets: ["@babel/preset-env"]
    });
    return {
        filename,
        dependencies,
        code
    }
}

const moduleInfo = moduleAnalyser('./src/index.js'); // 入口文件
console.log(moduleInfo);

vue-cli3多頁(yè)面配置


image.png

參考:https://cli.vuejs.org/zh/config/

webpack loader與plugins編寫:http://www.reibang.com/p/21cbc228d7f5

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猪钮,一起剝皮案震驚了整個(gè)濱河市品山,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烤低,老刑警劉巖谆奥,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異拂玻,居然都是意外死亡酸些,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門檐蚜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)魄懂,“玉大人,你說(shuō)我怎么就攤上這事闯第∈欣酰” “怎么了?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)填帽。 經(jīng)常有香客問(wèn)我蛛淋,道長(zhǎng),這世上最難降的妖魔是什么篡腌? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任褐荷,我火速辦了婚禮,結(jié)果婚禮上嘹悼,老公的妹妹穿的比我還像新娘叛甫。我一直安慰自己,他們只是感情好杨伙,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布其监。 她就那樣靜靜地躺著,像睡著了一般限匣。 火紅的嫁衣襯著肌膚如雪抖苦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天米死,我揣著相機(jī)與錄音睛约,去河邊找鬼。 笑死哲身,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贸伐。 我是一名探鬼主播勘天,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼捉邢!你這毒婦竟也來(lái)了脯丝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤伏伐,失蹤者是張志新(化名)和其女友劉穎宠进,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藐翎,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡材蹬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吝镣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堤器。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖末贾,靈堂內(nèi)的尸體忽然破棺而出闸溃,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布辉川,位于F島的核電站表蝙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏乓旗。R本人自食惡果不足惜府蛇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寸齐。 院中可真熱鬧欲诺,春花似錦、人聲如沸渺鹦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)毅厚。三九已至塞颁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吸耿,已是汗流浹背祠锣。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咽安,地道東北人伴网。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像妆棒,于是被迫代替她去往敵國(guó)和親澡腾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容