webpack知识梳理

143 阅读5分钟

1、快速上手

安装依赖

npm i webpack webpack-cli --dev

文件目录结构

image.png

src/index.js文件

(function func() {
    for (let i = 0; i < 2; i++) {
        console.log(i);
    }
})()

package.json文件

{
  "name": "webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start":"webpack" //添加的脚本指令
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^5.76.2",
    "webpack-cli": "^5.0.1"
  }
}

webpack.config.js

const path = require('path')
module.exports = {
    entry: './src/index.js', //入口文件(相对路径)
    output: {
        filename: 'bundle.js', //打包后生成的文件
        path: path.join(__dirname, 'output') //生成路径(绝对路径)
    }
}




打包后生成的文件

bundle.js

!function(){for(let o=0;o<2;o++)console.log(o)}();





2、资源模块加载

css模块

依赖下载

npm i css-loader style-loader --dev

目录结构

image.png

heading.js

import './index.css'
export default () => {
    const element = document.createElement('h2')
    element.textContent = 'hello world'
    element.classList.add('heading')
    element.addEventListener('click', () => {
        alert('hello webpack')
    })
    return element
}

index.css
.heading {
    margin: 0 auto;
    padding: 0 20px;
    max-width: 800px;
    background: #d31d1d;
    color: blue;
}

index.js

import createHeading from './heading.js'
const heading = createHeading()
document.body.append(heading)

webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output')
    },
    module: {
        rules: [
            {
                test: /\.css$/,  // 正则匹配式,只检测css文件
                use: [
                    // 从下往上依次解析,所以css在下,css解析完之后再解析style
                    'style-loader',  
                    'css-loader'
                ]
            }
        ]
    }
}

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <script type="module" src="./output/bundle.js"></script>
    </body>
</html>

输出结果

image.png




静态资源

安装依赖

npm i file-loader --dev

目录结构

image.png

index.js

import createHeading from './heading.js'
import icon from '../public/th.png'
const heading = createHeading()
document.body.append(heading)
const img = new Image()
img.src = icon
document.body.append(img)

webpack.config.js
const path = require('path')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        publicPath: 'output/' //配置静态自愿的根目录 但目前这行代码好像不加也没关系
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /\.png$/,
                use: 'file-loader'
            }
        ]
    }
}



输出结果

image.png






url加载器(静态资源优化)

安装依赖

npm i url-loader --dev

目录结构

image.png

webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        publicPath: 'output/'
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /\.png$/,
                // 将静态资源(图片)转换成十六进制的编码 ,此时output目录里没有生成单独的文件
                use: 'url-loader' 
            },
        ]
    }
}




总结

  • 小文件使用url,减少请求次数
  • 大文件单独提取存放,提高加载速度


优化写法

use:{
   loader:'url-loader',
   options:{
     limit: 10 * 1024  //当文件大小大于10kb时单独存放,小于10kb时使用url
   }
}

es模块

将es6代码解析成最初始的js代码(const -> var()=>{} -> function(){})

webpack.config.js

module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
     }






3、开发一个loader(md文件)

安装依赖

npm i marked --dev
npm i html-loader --dev

目录结构

image.png


about.md

我是成龙

index.js

import about from './about.md'

console.log(about);

markdown-loader.js

// 解析md文档
const { marked } = require('marked')
module.exports = source => {
    const html = marked(source)
    // 返回的类型必须为js语句的字符串
    return `module.exports = ${JSON.stringify(html)}`
}

webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        publicPath: 'output/'
    },
    module: {
        rules: [
            {
                test: /\.md$/,
                use: [
                    './markdown-loader.js'
                ]
            },
        ]
    }
}




引进html-loader

markdown-loader.js

// 解析md文档
const { marked } = require('marked')
module.exports = source => {
    const html = marked(source)
    // 返回的类型必须为js语句的字符串
    return html
}

webpack.config.js

const path = require('path')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        publicPath: 'output/'
    },
    module: {
        rules: [
            {
                test: /\.md$/,
                use: [
                    'html-loader'
                    './markdown-loader.js'
                ]
            },
        ]
    }
}

这里因为引入了html-loader,所以流程为: md ->marked(依赖)-> html格式 -> html-loader -> js语句的字符串




4、插件机制

自动清理输出目录插件

安装依赖

clean-webpack-plugin



目录结构

image.png



webpack.config.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        publicPath: 'output/'
    },
    module: {
        rules: [
            {
                test: /\.md$/,
                use: [
                    './markdown-loader.js'
                ]
            },
        ]
    },
    plugins: [
        new CleanWebpackPlugin
    ]
}



打包后会发现1.txt这个多余的文件被删除了

image.png






生成html插件

安装依赖

html-webpack-plugin



webpack.config.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        // 注释掉公共路径
        // publicPath: 'output/'
    },
    module: {
        rules: [
            {
                test: /\.md$/,
                use: [
                    './markdown-loader.js'
                ]
            },
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            // 修改里面的配置
            title: 'webpack测试',
            meta: {
                viewport: 'width-device-width'
            }
        })
    ]
}



配置完成打包后会在output文件夹里面生成一个index.html文件

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    <meta name="viewport" content="width-device-width"><script defer src="bundle.js"></script></head>
    <body>
        <div class="container">
            <h1>webpack测试</h1>
        </div>
    </body>
</html>

template模板

当想添加额外的标签时

plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'webpack测试',
            meta: {
                viewport: 'width-device-width'
            },
            // 引入模板
            template: './src/index.html'
        })
    ]

src目录下的template模板->index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div class="container">
            <h1><%= htmlWebpackPlugin.options.title %></h1>
        </div>
    </body>
</html>



打包生成后的文件

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    <meta name="viewport" content="width-device-width"><script defer src="bundle.js"></script></head>
    <body>
        <div class="container">
            <h1>webpack测试</h1>
        </div>
    </body>
</html>





指定生成多个html文件

plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'webpack测试',
            meta: {
                viewport: 'width-device-width'
            },
            template: './src/index.html'
        }),
        new HtmlWebpackPlugin({
            filename: 'about.html',
        })
    ]

image.png






自己开发一个插件

插件功能: 清除掉打包后文件里的注释文字(带双引号的)

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

class Myplugin {
    apply(compiler) {
        console.log('MyPlugin启动');
        compiler.hooks.emit.tap('Myplugin', compilation => {
            // compilation =>可以理解为此次打包的上下文
            for (const name in compilation.assets) {
                if (name.endsWith('.js')) {
                    const contents = compilation.assets[name].source()
                    // 正则替换
                    const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
                    compilation.assets[name] = {
                        // 内容替换
                        source: () => withoutComments,
                        size: () => withoutComments.length
                    }
                }
            }
        })
    }
}
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        // 注释掉公共路径
        // publicPath: 'output/'
    },
    module: {
        rules: [
            {
                test: /\.md$/,
                use: [
                    './markdown-loader.js'
                ]
            },
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: 'webpack测试',
            meta: {
                viewport: 'width-device-width'
            },
            template: './src/index.html'
        }),
        new HtmlWebpackPlugin({
            filename: 'about.html',
        }),
        new Myplugin()
    ]
}

5、Dev Server

启动Dev Server

安装依赖

npm i webpack-dev-server

package.json里面添一行脚本代码

"scripts": {
    "server":"webpack-dev-server"
}

启动server命令:

npm run server

运行结果

image.png

静态资源访问

webpack.config.js文件中添加代码,以访问public目录中的静态资源

module.exports = {
    ...
    devServer: {
        static: {
            directory: './public',
        }
    },
}

运行结果

image.png

Dev Server代理

src/index.js

fetch('./api/users').then(res => res.json())

webpack.config.js

devServer: {
    static: {
        directory: './public',
    },
    proxy: {
        '/api': {
            // http://localhost:8080/api/users -> https://api.github.com/api/users
            target: 'https://api.github.com',
            // https://api.github.com/api/users -> https://api.github.com/users
            pathRewrite: {
                '^/api': ''
            },
            // 不能使用localhost:8080作为请求Github的主机名
            changeOrigin: true
        }
    }
}

运行结果

image.png




6、Source Map

为什么要用Source Map以及简单的使用

打包后的代码如果出现了bug,会出现无法定位的问题,因为打包后的代码太乱了,所以此时可以用Source Map

webpack.config.js

module.exports = {
    devtool: 'source-map'
}

src/index.js里故意写一行错误代码,之后打包,打包后的output目录里会生成一个bundle.js.map文件

image.png

控制台 image.png


image.png

Source Map的选择

开发环境下用cheap-module-eval-source-map,因为cheap-module-eval-source-map能暴露出错误代码的文件文件位置以及行数

module.exports = {
    devtool: 'cheap-module-eval-source-map'
}

上产环境下尽量不配置Source Map

7、HMR初体验

介绍和简单配置

简单来讲就是那种特么的热更新,每保存一下就更新一下,保证服务器上运行的都是你最新保存的代码

webpack.config.js

const webpack = require('webpack')
devServer: {
    hot: true
}

局部热更新

index.js文件里面引入test.js文件,当test.js文件里的内容有变动时,index.js文件里的内容也会进行重新渲染,此时可以用局部热更新的方法

文件目录

image.png

test.js

export function test() {
    alert(22234)
}

index.js

import { test } from './test'

fetch('./api/users').then(res => res.json())
alert('index')
test()
module.hot.accept('./test', () => {
    test()
})

8、生产环境优化

不同环境下的配置

生产环境运行指令: npm run start:production

package.json

"scripts": {
    "start:production": "webpack --env production"
}

webpack.config.js

const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
class Myplugin {
    apply(compiler) {
        console.log('MyPlugin启动');
        compiler.hooks.emit.tap('Myplugin', compilation => {
            for (const name in compilation.assets) {
                if (name.endsWith('.js')) {
                    const contents = compilation.assets[name].source()
                    const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
                    compilation.assets[name] = {
                        source: () => withoutComments,
                        size: () => withoutComments.length
                    }
                }
            }
        })
    }
}
// env为一个对象:{ WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, production: true }
module.exports = (env, argv) => {
    const config = {
        ...
    }
    // 当运行生产环境命令时,会执行以下代码
    if (env.production) {
        config.mode = 'production'
        config.devtool = false
        config.plugins = [
            ...config.plugins,
            new CleanWebpackPlugin(),
            new CopyWebpackPlugin({
                patterns: [
                    {
                        from: path.join(__dirname, 'public'),
                        to: 'output'
                    }
                ]
            }),
        ]
    }
    return config
}



普通打包指令运行后

image.png

生产指令运行后的目录

image.png

不同环境的配置文件

安装依赖

npm i webpack-merge

文件目录

image.png

公共文件 webpack.common.js

const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
class Myplugin {
    apply(compiler) {
        console.log('MyPlugin启动');
        compiler.hooks.emit.tap('Myplugin', compilation => {
            // compilation =>可以理解为此次打包的上下文
            for (const name in compilation.assets) {
                if (name.endsWith('.js')) {
                    const contents = compilation.assets[name].source()
                    // 正则替换
                    const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
                    compilation.assets[name] = {
                        // 内容替换
                        source: () => withoutComments,
                        size: () => withoutComments.length
                    }
                }
            }
        })
    }
}
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output'),
        // 注释掉公共路径
        // publicPath: 'output/'
    },
    devtool: 'source-map',
    devServer: {
        hot: true,
        static: {
            directory: './public',
        },
        proxy: {
            '/api': {
                // http://localhost:8080/api/users -> https://api.github.com/api/users
                target: 'https://api.github.com',
                // https://api.github.com/api/users -> https://api.github.com/users
                pathRewrite: {
                    '^/api': ''
                },
                // 不能使用localhost:8080作为请求Github的主机名
                changeOrigin: true
            }
        }
    },
    module: {
        rules: [
            {
                test: /\.md$/,
                use: [
                    './markdown-loader.js'
                ]
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'webpack测试',
            meta: {
                viewport: 'width-device-width'
            },
            template: './src/index.html'
        }),
        new Myplugin(),
        new webpack.HotModuleReplacementPlugin()
    ]
}

生产文件 webpack.prod.js,利用merge方法引入公共配置,在此基础上添加属于自己的配置

const common = require('./webpack.common')
const { merge } = require('webpack-merge')
const path = require('path')

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = merge(common, {
    mode: 'production',
    plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: path.join(__dirname, 'public'),
                    to: 'output'
                }
            ]
        })
    ]
})

DefinePlugin

作用:全局变量替换

index.js

consoel.log(TEST)

webpack.prod.js

module.exports = merge(common, {
    plugins: [
        new webpack.DefinePlugin({
            // 要用双引号包起来
            TEST: '"hello111"'
        })
    ]
}

测试结果

image.png

多入口打包

目录

image.png



webpack.config.js

const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode: 'none',
    entry: {
        test1: './src/test1.js',
        test2: './src/test2.js'
    },
    output: {
        // 输出打包后的文件名[name] :test1.bundle.js 和 test2.bundle.js
        filename: '[name].bundle.js',
        path: path.join(__dirname, 'output')
    }
    plugins: [
        new HtmlWebpackPlugin({
            title: 'test1',
            meta: {
                viewport: 'width-device-width'
            },
            template: './src/test1.html',
            filename: 'test1.html',
            chunks: ['test1'],
        }),
        new HtmlWebpackPlugin({
            title: 'test2',
            meta: {
                viewport: 'width-device-width'
            },
            // 引用模板
            template: './src/test2.html',
            // 输出后的文件名
            filename: 'test2.html',
            chunks: ['test2'],
        }),
        new Myplugin(),
        new webpack.HotModuleReplacementPlugin()
    ]
}

提取公共模块

目录结构

image.png

test1.jstest2.js引入公共模块global.cssglobal.js

此时打包后的文件里面都会生成公共模块的代码,所以可以进行一个公共模块的提取

webpack.config.js

module.exports = {
    ...
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    },
}

打包后会发现多了一个公共文件

image.png

动态导入(类似于懒加载)

目录结构

image.png

index.js

非动态导入写法

import test1 from './test1.js'
import test2 from './test2.js'

const flag = true
flag ? test1() : test2()

打包生成文件

image.png

动态导入写法

const flag = true

flag ? import('./test1.js').then(({ default: test1 }) => {
    test1()
}) : import('./test2.js').then(({ default: test2 }) => {
    test2()
})

test1.jstest2.js情况分类




  • 情况一

image.png

生成两个额外的文件:1.index.js2.index.js

image.png




  • 情况二

image.png

生成三个额外的文件:1.index.js2.index.js3.index.js

image.png

多出的那个文件3.index.js为公共资源的打包,这样原本要打包两遍的文件就只会打包一遍,再被依次引入就行了

mini-css-extract-plugin

作用:将打包中引入的css样式抽取成单独的文件

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
             }
         ]
     },
   plugins: [
       new MiniCssExtractPlugin()
   ]
}

效果图

image.png image.png

压缩打包后的css文件

下载依赖

npm i css-minimizer-webpack-plugin

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
    ...
    optimization: {
        minimizer: [
            new CssMinimizerPlugin(),
        ],
        minimize: true,
   },
   module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
   },
   plugins: [
       new MiniCssExtractPlugin()
   ]
}

打包后的代码结构

image.png

输出文件名hash

简单来讲hash就是在打包时会生成打包文件,打包的文件名中添加hash值,作用:省略打包资源,只有在特定的情况下才会进行重新打包

三种哈希

  • hash
  • chunkhash
  • contenthash




一、hash

 output: {
        filename: '[name].[chunkhash].js',
     },
plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename: '[name]-[hash].bundle.css'
        })
    ]

hash是只要任意文件中的一个文件内容发生变动,所有的文件就得进行重新打包



二 、chunkhash

 output: {
        filename: '[name].[chunkhash].js',
     },
plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            filename: '[name]-[chunkhash].bundle.css'
        })
    ]

chunkhash是这一整个依赖链中的一个文件内容发生变动,这一整个依赖链中的文件都会进行重新打包



二 、contenthash

 output: {
        filename: '[name].[chunkhash].js',
     }
plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new MiniCssExtractPlugin({
            // 在这里将chunkhash改为contenthash
            filename: '[name]-[contenthash].bundle.css'
        })
    ]

contenthash在chunkhash的基础上做了优化,比如index.js引入index.css,如果index.js发生变动,index.css也得重新打包,这显然不是我们想要的,所以contenthash很好的解决了这个问题,只有index.css本身内容发生变动,index.css才会重新进行打包