webpack4.0從入門到放棄

一、入坑初探

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

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

"private": true

2. 運(yùn)行webpack

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

npx webpack --config webpack.config.js

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

3. webpack簡單配置

webpack只能識別后綴是.js的文件,如果是其它類型的文件,就需要引入loader來幫助我們編譯。
下面我們來做一個(gè)簡單的對圖片和css的打包配置:

rules: [
    {
        test: /\.(jpg|png|gif)$/,
        use: {
        loader: 'url-loader',
        options: { // loader額外參數(shù)配置
            name: '[name]_[hash].[ext]', // name: 原來的名字 ext:原來的后綴
            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這種語法
            'sass-loader',
            'postcss-loader'
          ]
     }
]

file-loader和url-loader的區(qū)別是url-loader會把圖片等(任何文件)文件直接打包到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

最后我們再來看打包完命令行中的展示,如下圖所示:


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文件也需要通過postcss-loader,sass-loader編譯,如果不加,就會直接走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

我們需要自動生成一個(gè)html文件,把打包生成的js自動引入到這個(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è)名字,否則會因打包處兩份相同的文件而報(bào)錯

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. 只指出哪一行出錯,不指出哪一頁。2. 只報(bào)我們的業(yè)務(wù)代碼,不處理loader等中的代碼錯誤。
module:指出loader等中的錯誤
source-map: 生成一個(gè).map文件
inline: 將映射文件放到main.js中
eval: 將業(yè)務(wù)代碼與 以及source-map通過eval方式執(zhí)行,速度最快
具體用法參考官方文檔:https://webpack.js.org/configuration/devtool/#devtool

5. 熱啟動

5.1 通過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

下面我們來實(shí)現(xiàn)一個(gè)簡單的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語法:https://www.webpackjs.com/api/cli/

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

6. Hot Module Replacement 熱模塊更新

當(dāng)我們每次修改代碼時(shí),頁面都會整個(gè)刷新,這樣豈不是很麻煩,有沒有辦法只更新被修改的部分,而不刷新整個(gè)頁面,這時(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才會生效
hotOnly: HotModuleReplacementPlugin失效時(shí),重新刷新一次頁面

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

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

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

7. Babel 處理 ES6 語法

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

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

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

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

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

import "@babel/polyfill";

但是我們怎么實(shí)現(xiàn)按需加載呢,我們再.babelrc中添加useBuiltIns: 'usage'
如果配置了useBuiltIns: 'usage',會默認(rèn)引入@babel/polyfill,不需要手動調(diào)用
參見官網(wǎng):https://babeljs.io/docs/en/babel-polyfill

8. 類庫的配置

當(dāng)我們寫一個(gè)類庫時(shí),我們可以用@babel/plugin-transform-runtime,相比@babel/polyfill,它是通過閉包實(shí)現(xiàn)依賴注入,這樣做不會污染全局環(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大意就是只打包我們有使用的代碼,將無用的部分去掉,舉例如下:
我們有一個(gè)math.js的方法庫,內(nèi)容如下

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

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

然后我們在index.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": [ // 不對下面的文件進(jìn)行tree shaking
    "@babel/polly-fill",
    "*.css"
]

首先來解釋下它是什么意思,即忽略掉哪些模塊不做Tree-shaking,首先我們要忽略所有的css文件,其次如果像import @babel/polyfill這種形式的,我們沒有引入任何東西,webpack會自動幫我們忽略掉,這樣打包文件就出錯了

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

"sideEffects": false // false代表沒有需要忽略的文件

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插件無法刪除dist目錄問題,解決方法如下:

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._ = _;

然后我們在entry引入這個(gè)包,

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

3.2 配置optimization

在webpack中我們可以配置chunks來自動幫我們做(同步)代碼分割

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

3.3 異步模塊打包

異步模塊不需要我們做任何配置,webpack會自動幫我們將異步代碼打包到另一個(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');

這是魔法注釋,加上后,打包出來的js會是你注釋的值,否則為一個(gè)id(如0)值

4. SplitChunksPlugin 配置參數(shù)

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

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

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

default是指如果不符合vendors中的要求的文件,比如我們自己寫的一個(gè)包,這個(gè)包并不在node_modules中,這時(shí)會分到default組中
cacheGroups作用是做一個(gè)緩存組,如果我們引入了多個(gè)包,就會分割成很多模塊,而cacheGroups作用就是先將需要打包的文件緩存起來,然后統(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"

會生成一個(gè)stats.json文件,這是一個(gè)對打包過程的描述文件,借助一些工具我們可以進(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),可以查看代碼的使用率,代碼使用率越高說明優(yōu)化的越好,所以我們開發(fā)時(shí)盡量多寫異步的代碼,這樣代碼使用的時(shí)候才會去加載
如下所示:

// 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是等主代碼加載完才會加載,Preloading是與主代碼同時(shí)加載

6. CSS代碼分割

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

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

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

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

6.2 分割css

如果我們不分割css,webpack會默認(rèn)把css打包到j(luò)s文件中,這是我們不希望看到的,下面來看下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壓縮

我們還可以對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文件中的,這樣會導(dǎo)致即使我們沒有更改文件,但是包與包之間的關(guān)系變了而引起的contenthash發(fā)生變化,這時(shí)我們就需要這樣配置將這部份代碼抽離出來,在新版webpack中不會出現(xiàn)這個(gè)問題

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

8. Shimming

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

8.1 全局引入

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

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

8.2 修改this指向

每個(gè)模塊的this指向的都是模塊自身,如果想讓this指向window,需要imports-loader插件,然后我們再做如下配置:

{ 
    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高級使用技巧

1. 類庫代碼打包

我們對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等語法
}

還可以用如下寫法

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

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

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

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

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

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

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

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

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

然后在npm注冊一個(gè)賬號,
然后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?$/, // ?代表可有可無
    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文件
    }
}

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

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

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

4. 1. 代理接口

devServer: {
  proxy: {
    // index: '', // 如果要代理根路徑,需要把index設(shè)置為false或者''
    '/react/api': {
      target: 'https://www.dell-lee.com', // 代理請求接口
      secure: false, // 如果是https網(wǎng)址,這里需要設(shè)置為false
      pathRewrite: { // 代理接口,訪問header.json時(shí)會幫你請求demo.json
        'header.json': 'demo.json'
      },
      changeOrigin: true, // 后端可能設(shè)置了changeOrigin防止爬蟲,這里我們設(shè)置true以后就可以避開這個(gè)限制了
      headers: { // 設(shè)置請求頭
        host: 'www.dell-lee.com',
        cookie: ....
      },
      bypass: function(req, res, proxyOptions) { // 攔截,如果請求的是一個(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īng)用路由問題

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

historyApiFallback: true, // 把對服務(wù)器的請求都轉(zhuǎn)換為對跟路徑的請求
historyApiFallback: {
      rewrites: [ // 訪問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有錯會在瀏覽器中提示

devServer: {
    overlay: true
}

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

{ 
    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í)指定檢測目錄,圖片沒有必要

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

3. 盡少使用plugin,盡可能精簡并確保可靠

4. 合理配置resolve

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

當(dāng)一個(gè)引入的文件沒有后綴時(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)行它對第三方模塊單獨(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)這里有,就不會再重復(fù)打這個(gè)包
    manifest: path.resolve(__dirname, '../dll', '../dll/vendors.manifest.json')
})
new AddAssetHtmlWebpackPlugin({ // 向html中添加引入某個(gè)文件
    filepath: path.resolve(__dirname, '../dll',  '../dll/vendors.dll.js')
})

自動化引入

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)境無用插件剔除

11. noParse

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

12. ignoreplugin

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

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

七、多頁面打包配置

多入口

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'] // 指定引入文件
}), 

自動化方式

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指向來調(diào)用this中的一些方法
    return source.replace('lee', 'world');
}

然后我們在webpack.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,通過this.query可以接收到options中的內(nèi)容

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

或者我們可以通過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í),會來node_modules中找,找不到了再來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() { // 文件長度
                    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, { // 抽象語法樹,ast
        sourceType: 'module' // 如果是es6模塊方法,這里需要設(shè)置
    });
    const dependencies = {};
    traverse(ast, {
        ImportDeclaration({ node }) { // 如果有引入語句,就執(zhí)行
            const dirname = path.dirname(filename);
            const newFile = './' + path.join(dirname, node.source.value); // 改為相對根目錄的路徑
            dependencies[node.source.value] = newFile;
        }
    });
    const { code } = babel.transformFromAst(ast, null, { // 將ast抽象語法樹轉(zhuǎn)換為瀏覽器可以識別的代碼
        presets: ["@babel/preset-env"]
    });
    return {
        filename,
        dependencies,
        code
    }
}

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

vue-cli3多頁面配置


image.png

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

webpack loader與plugins編寫:http://www.itdecent.cn/p/21cbc228d7f5

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

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

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