webpack5的使用与原理

287 阅读4分钟

5大核心概念

熟悉webpack的5大核心概念,是我们使用webpack的前提,这为后面的使用起指导作用。

  1. entry(入口)

webpack会以一个文件或多个文件作为打包入口,entry就是指示从哪个文件开始打包

  1. output(输出)

指示webpack将打包后的文件输出在哪个目录

  1. loader(加载器)

webpack本身只能处理js、json,其他资源需要loader完成

  1. plugins(插件)

扩展webpack的功能

  1. mode(模式)
  • 开发模式:development
  • 生产模式: production

基本配置

我们在项目的根目录下新建一个webpack.config.js配置文件,webpack会根据这个文件进行打包

const path = require('path') // node.js核心模块,专门用来处理路径问题

module.exports = {
    //入口
    entry: './src/main.js', //相对路径
    //输出
    output: {
        //文件的输出路径
        // __dirname node.js的变量, 代表当前文件的文件夹目录
        path: path.resolve(__dirname,"dist"), //绝对路径
        filename: 'main.js'
    },
    //加载器
    module: {
        rules: [
            // loader的配置
        ],
    },
    //插件
    plugins: [
        // plugins的配置
    ],
    //模式
    mode: "development",
}

开发模式介绍

开发模式下我们主要做两件事:

  1. 编译代码,使得浏览器可以运行
  2. 代码质量检查,树立代码规范

处理样式资源

这里我们学习使用webpack处理Css、Less、Sass、Scss、Styl样式资源

css资源的处理

由于webpack本身并不能处理css等样式资源,所以我们要借助loader,webpack官网包含了loader的下载与配置。

安装

npm install --save-dev css-loader style-loader

在入口文件中引入css资源

import 'file.css';

在webpack.config.js中配置lodaer

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/, //检查.css结尾的文件
        use: [ //执行顺序:从右到左(从下到上)
             'style-loader', //将js中css通过创建style标签添加到html文件中生效
             'css-loader' //将css资源编译成commonjs模块到js
        ]
      }
    ]
  }
}

less资源的处理

安装

npm install --save-dev less-loader less

配置

module.exports = {
    ...
    module: {
        rules: [{
            test: /.less$/,
            use: [{
                loader: "style-loader" // creates style nodes from JS strings
            }, {
                loader: "css-loader" // translates CSS into CommonJS
            }, {
                loader: "less-loader" // compiles Less to CSS
            }]
        }]
    }
};

处理图片资源

webpack5集成了打包图片的能力,只需要如下配置即可

{
                test: /\.(png|jpe?g|gif|webp|svg)$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        // 小于10kb的图片转base64
                        //优点: 减少请求数量 缺点:体积会更大
                        maxSize: 10 * 1024 // 10kb
                    }
                }        
            }

修改输出文件目录

默认情况下,打包后的图片、入口文件等资源都在同一个文件夹下,非常混乱,我们可以单独为某一资源指定输出路径。以图片打包为例

            {
                test: /\.(png|jpe?g|gif|webp|svg)$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        // 小于10kb的图片转base64
                        //优点: 减少请求数量 缺点:体积会更大
                        maxSize: 10 * 1024 // 10kb
                    }
                },
                generator: {
                    //输出图片的路径与名称
                    //[hash:10] hash值取前10位
                    filename: "static/images/[hash:10]/[ext]/[query]/"
                }        
            }

自动清空上次打包内容

配置非常简单,只需要在output中添加clean: true

    output: {
        //文件的输出路径
        // __dirname node.js的变量, 代表当前文件的文件夹目录
        path: path.resolve(__dirname,"dist"), //绝对路径
        // 入口文件打包输出文件名
        filename: 'static/js/main.js',
        // 自动清空上次打包的内容
        // 原理: 在打包前,将path整个目录内容清空,再进行打包
        clean: true,
    },

ESLint代码规范检查

安装

npm install eslint eslint-webpack-plugin --save-dev

在webpack.config.js中添加eslint-webpack-plugin插件

const ESLintPlugin = require('eslint-webpack-plugin');

...

    plugins: [
        // plugins的配置
        new ESLintPlugin({
            //检查哪些文件
            context: path.resolve(__dirname,"src")
        }),
    ],

在根目录上新建.eslintrc.js文件

module.exports = {
    //继承 Eslint 规则
    extends: ["eslint:recommended"],
    env: {
        node: true, // 启用node中全局变量
        browser: true, // 启用浏览器中全局变量
    },
    parserOptions: {
        ecmaVersion: 6, // es6
        sourceType: "module", // es6 module
    },
    rules: {
        "no-var": 2, // 不能使用 var 定义变量
    }
}

在根目录上新建.eslintignore文件,指明哪些文件不需要eslint扫描

babel

安装

npm install -D babel-loader @babel/core @babel/preset-env

在webpack.config.js的loader中配置

            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/, //排除某些文件不处理
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }

在根目录下新建babel.config.js文件

module.exports = {
    // 智能预设:能够编译ES6语法
    presets: ["@babel/preset-env"],
}

处理HTML资源

我们在html中是手动引入入口文件的,但是如果有很多文件的话,那么就需要一个个的引入,并且文件在打包后文件名可能会变化,那有没有自动引入文件的方法呢?

HtmlWebpackPlugin

安装

npm install --save-dev html-webpack-plugin

配置

var HtmlWebpackPlugin = require('html-webpack-plugin');

...

plugins: [
    new HtmlWebpackPlugin({
            // 模板:以public/index.html文件创建新的htnl
            // 新的html文件特点:1.结构和原来一致 2. 自动引入打包输出后的资源
            template: path.resolve(__dirname,"public/index.html")
        })
]

搭建开发服务器&自动化

每次修改代码或其他内容都需要重新打包,如果能自动打包的话,那就太方便了,所以我们需要搭建一个开发服务器,实现热更新,配置很简单。

安装

npm i webpack-dev-server -D

在webpack.config.js中配置

    // 开发服务器:不会输出资源,打包后内容存放在内存中。
    devServer: {
        host: "localhost", //启动服务器域名
        port: "3000", //启动服务器端口号
        open: true, // 是否自动打开浏览器
    },

CSS处理

提取css为单独的文件

CSS文件会被打包到js文件中,当js文件加载时,会动态创建style标签来生成样式,这样的话对于网站来说,会出现闪屏现象,因为浏览器会先加载html元素,再执行js。我们应该是单独的css文件,通过link标签来引入。

MiniCssExtractPlugin

这个插件会为每一个包含css的js文件单独创建一个css文件

安装

npm install --save-dev mini-css-extract-plugin

在webpack.config.js中配置

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

pluginsL: [
    new MiniCssExtractPlugin({
            filename: 'static/css/main.css', //指定输出位置
        }),
]

style-loader改为MiniCssExtractPlugin.loader,因为style-loader是js创建style标签引入样式,MiniCssExtractPlugin.loader是用打包时创建link标签引入

CSS兼容性处理

安装

npm install --save-dev postcss-loader postcss

在loader中配置

{
                            loader: "postcss-loader",
                            options: {
                                postcssOptions: {
                                    plugins: [
                                        [
                                            "postcss-preset-env",
                                            {
                                                // Options
                                            },
                                        ],
                                    ],
                                },
                            },
                        }

CSS压缩

安装

npm install css-minimizer-webpack-plugin --save-dev

在plugins

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
plugins: [
    new CssMinimizerPlugin(),
]

HTML、JS压缩

在生产环境下,默认进行了压缩,无需配置

Webpack高级配置

所谓高级配置其实就是进行webpack优化,让我们代码在编译/运行时性能更好。

我们会从以下角度进行优化:

  1. 提升开发体验
  2. 提升代码打包速度
  3. 减少代码体积
  4. 优化代码运行性能

提升开发体验-SourceMap

为什么

编译后的代码与源代码没有映射关系,如果代码运行报错,浏览器会直接定位到编译后的代码,并且出错的位置是不准确的,不易阅读,所以我们需要在编译代码与源代码间建立一个映射文件,将错误直接定位到源代码。

是什么

SourceMap(源代码映射)是一种在编译代码与源代码之间建立映射的方案

怎么用

  1. 开发模式下:cheap-module-source-map
  • 优点:打包编译速度快,只包含行映射
  • 缺点:不包含列映射
    mode: "development",
    devtool: "cheap-module-source-map",
  1. 生产环境下:source-map
  • 优点:包含行、列映射
  • 缺点:打包编译速度更慢
     mode: "production",
     devtool: "source-map"

提升打包速度

HotModuleReplacement

为什么

开发时我们修改了其中一个模块代码,Webpack默认会将所有模块全部重新打包编译,速度很慢。

所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。

是什么

HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。

怎么用

  1. 基本配置
    devServer: {
        host: "localhost", //启动服务器域名
        port: "3000", //启动服务器端口号
        open: true, // 是否自动打开浏览器,
        hot: true, //开启HMR (只能用于开发环境,生产环境不需要了)
    },

此时css样式经过style-loader处理,已经具备HMR功能了。但是js还不行

所以我们可以在入口文件手动操作

import count from './js/count'
import sum from './js/sum'
import './css/index.css'
import './less/index.less'


console.log(count(2,2))
console.log(sum(1,2,3,4))

// 热模块替换
if(module.hot){ 
    module.hot.accept('./js/count.js')
    module.hot.accept('./js/sum.js')
}

上面这样写会很麻烦,所以实际开发我们会使用其他loader来解决。

比如:vue-loaderreact-hot-loader

OneOf

为什么

打包时每个文件都会经过所有loader处理,虽然因为test正则原因实际上没有处理,但是都要经过一遍,比较慢。

是什么

顾名思义就是只能匹配上一个loader,剩下的就不匹配了。

怎么用

将loader包裹在oneOf对象中即可。

    module: {
        rules: [
            // loader的配置
            {
                // 每个文件只能被其中一个loader配置处理
                oneOf: [
                    {
                        test: /\.css$/, //检查.css结尾的文件
                        use: [ //执行顺序:从右到左(从下到上)
                            'style-loader', //将js中css通过创建style标签添加到html文件中生效
                            'css-loader' //将css资源编译成commonjs模块到js
                        ]
                    }, {
                        test: /\.less$/,
                        use: [{
                            loader: "style-loader" // creates style nodes from JS strings
                        }, {
                            loader: "css-loader" // translates CSS into CommonJS
                        }, {
                            loader: "less-loader" // compiles Less to CSS
                        }]
                    }, {
                        test: /\.(png|jpe?g|gif|webp|svg)$/,
                        type: 'asset',
                        parser: {
                            dataUrlCondition: {
                                // 小于10kb的图片转base64
                                //优点: 减少请求数量 缺点:体积会更大
                                maxSize: 10 * 1024 // 10kb
                            }
                        },
                        generator: {
                            //输出图片的路径与名称
                            //[hash:10] hash值取前10位
                            filename: "static/images/[hash:10]/[ext]/[query]/"
                        }
                    }, {
                        test: /\.js$/,
                        exclude: /(node_modules|bower_components)/, //排除某些文件不处理
                        use: {
                            loader: 'babel-loader',
                            options: {
                                presets: ['@babel/preset-env']
                            }
                        }
                    }
                ]
            }
        ],
    },

Include/Exclude

为什么

开发时我们需要使用第三方的库或插件,所有文件都下载到node_modules中了。而这些文件是不需要编译可以直接使用的。

所以我们在对js文件处理时,要排除node_modules下面的文件

是什么

  • include

包含,只处理xxx文件

  • exclude

排除,除了xxx文件以外其他文件都处理

怎么用

{
                        test: /\.js$/,
                        //exclude: /(node_modules|bower_components)/, //排除某些文件不处理
                        include: path.resolve(__dirname,"../src"),
                        use: {
                            loader: 'babel-loader',
                            options: {
                                presets: ['@babel/preset-env']
                            }
}

Cache

为什么

每次打包时js文件都要经过Eslint检查和Babel编译,速度比较慢

我们可以缓存之前的Eslint检查和Babel编译结果,这样第二次打包时速度就会更快了

是什么

对Eslint检查和Babel编译结果进行缓存

怎么用

开启babel缓存

{
                        test: /\.js$/,
                        //exclude: /(node_modules|bower_components)/, //排除某些文件不处理
                        include: path.resolve(__dirname,"../src"),
                        use: {
                            loader: 'babel-loader',
                            options: {
                                //presets: ['@babel/preset-env'],
                                cacheDirectory: true, //开启babel缓存
                                cacheCompression: false,// 关闭缓存文件压缩
                            }
}

开启Eslint缓存

new ESLintPlugin({
            //检查哪些文件
            context: path.resolve(__dirname, "../src"),
            exclude: "node_modules", //默认值
            cache: true, //开启缓存
            cacheLocation: path.resolve(__dirname,'../node_modules/.cache/eslintcache')
        }),

多进程打包-Thread

为什么

当项目越来越大时,打包速度越来越慢,甚至于需要一个下午才能打包出来的代码。

我们想要继续提升打包速度,其实就是要提升js的打包速度,因为其他文件都比较少。

而对js文件处理主要就是eslint、babel、Terser三个工具,所以我们要提升他们的速度

我们可以开启多进程同时处理js文件,这样速度就比之前的单进程打包更快了。

是什么

多进程打包:开启电脑的多个进程同时干一件事,速度更快 需要注意:请尽在特别耗时的操作中使用,因为每个进程启动就有大约600ms左右开销

怎么用

我们启动进程的数量就是CPU的核数

  1. 如何获取CPU的核数,每个电脑都不一样
const os = require('os')
const threads = os.cpus().length // cpu核数
  1. 在loader中配置
{
      loader: 'thread-loader', //开启多进程
      options: {
           works: threads // 进程数量
      }
},
  1. 在eslint中配置
        new ESLintPlugin({
            //检查哪些文件
            context: path.resolve(__dirname, "../src"),
            exclude: "node_modules", //默认值
            cache: true, //开启缓存
            cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),
            threads, //开启多进程和设置进程数量
        }),
  1. 在terser中配置
const TerserWebpackPlugin = require('terser-webpack-plugin')

new TerserWebpackPlugin({
    parallel: threads,
})

减少代码体积

Tree Shaking

为什么

开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。

如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们只用上极小部分的功能。

这样整个库都打包进来,体积就太大了。

是什么

Tree Shaking 是一个术语,通常用于描述移出javascript中没有使用上的代码。

注意:它依赖 ES Module

怎么用

webpack已经默认开启这个功能,无需其他配置

Babel

为什么

Babel为编译的每个文件都插入辅助代码,使代码体积过大!

Babel对一些公共方法使用了非常小的辅助代码,比如_extend。默认情况下会被添加到每一个需要它的文件中。

你可以将这些辅助代码作为一个独立模块,来避免重复引入。

是什么

@babel/plugin-transform-runtime: 禁用了Babel自动对每个文件的runtime注入,而是引入@babel/plugin-transform-runtime并且使所有辅助代码从这里引用。

怎么用

npm i  @babel/plugin-transform-runtime -D
                        {
                            loader: 'babel-loader',
                            options: {
                                //presets: ['@babel/preset-env'],
                                cacheDirectory: true, //开启babel缓存
                                cacheCompression: false, // 关闭缓存文件压缩
                                plugins: ['@babel/plugin-transform-runtime'], //减少代码体积
                            }
                        }

Image Minimizer

为什么

开发项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。

我们可以对图片进行压缩,减少图片体积。

注意:如果项目中图片都是在线连接,那么就不需要了。本地图片才需要压缩。

是什么

image-minimizer-webpack-plugin 用来压缩图片的插件

怎么用

  1. 下载包
npm install image-minimizer-webpack-plugin imagemin --save-dev
  • 无损压缩
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev
  • 有损压缩
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev

优化代码运行性能

代码分割

为什么

打包代码时会将所有js文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的js文件,其他文件不应该加载。

所以我们需要将打包生成的文件进行代码分割,生成多个js文件,渲染哪个页面就只加载某个js,这样加载的资源就少,速度就更快。

是什么

Preload/Prefetch

为什么

我们前面已经做了代码分割,同时会使用import动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。

但是加载速度还不够好,比如:用户点击按钮才加载这个资源的,如果资源体积很大,那么用户会感觉到明显的卡顿效果。

我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上PreloadPrefetch技术。

是什么

  • Preload: 告诉浏览器立即加载资源
  • Prefetch: 告诉浏览器在空闲时才开始加载资源

它们共同点:

  • 都只会加载资源,并不执行
  • 都有缓存。

它们区别:

  • Preload加载优先级高,Prefetch加载优先级低。
  • Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面的资源,也可以加载下一个页面需要使用的资源。

总结:

  • 当前页面优先级高的资源用 Preload 加载
  • 下一个页面需要使用的资源用 Prefetch 加载

它们的问题:兼容性较差

PWA

为什么

开发 Web App项目,项目一旦处于网络离线情况,就没法访问了。

我们希望给项目提高离线体验。

是什么

渐进式网络应用程序(PWA): 是一种可以提供类似于native app(原生应用程序)体验的web app技术。

其中最重要的是,在离线时应用程序能继续运行功能。

内部通过Service Workers技术实现的,

怎么用

  1. 下载
npm install workbox-webpack-plugin --save-dev
  1. 在webpack.config.js中配置
const WorkboxPlugin = require('workbox-webpack-plugin');
     new WorkboxPlugin.GenerateSW({
       // these options encourage the ServiceWorkers to get in there fast
       // and not allow any straggling "old" SWs to hang around
       clientsClaim: true,
       skipWaiting: true,
     }),
  1. 在入口文件中添加
 if ('serviceWorker' in navigator) {
   window.addEventListener('load', () => {
     navigator.serviceWorker.register('/service-worker.js').then(registration => {
       console.log('SW registered: ', registration);
     }).catch(registrationError => {
       console.log('SW registration failed: ', registrationError);
     });
   });
 }

Loader原理

loader概念

帮助webpack将不同类型的文件转换为可识别的模块

loader执行顺序

  1. 分类
  • pre: 前置loader
  • normal: 普通loader
  • inline: 内置loader
  • post: 后置loader
  1. 执行顺序
  • 4类loader的执行优先级:pre > normal > inline > post
  • 相同优先级的loader执行顺序为: 从右到左,从下到上

开发一个loader

/* 
 loader就是一个函数
 当webpack解析资源时,会调用相应的loader去处理
 loader就会接收文件内容作为参数,返回内容出去
    - content: 文件内容
    - map: SourceMap
    - meta: 别的loader传递的参数
*/

module.exports = function(content, map, meta){
    console.log(content)
    return content
}

loader 分类

1. 同步loader

同步loader有两种方式

module.exports = function (content){
    return content
} 
module.exports = function(content, map, meta){
    console.log("同步loader")
    /* 
    第一个参数:err 代表是否有错误
    第二个参数:content 处理后的内容
    第三个参数:source-map 继续传递source-map
    第四个参数:meta 给下一个loader传递参数
    */
    this.callback(null, content, map, meta)
}

2. 异步loader

异步loader会异步处理content,这里我们用一个延迟函数做演示。只有callback被调用后,content才会传递给下一个loader。

module.exports = function (content, map, meta) {
    const callback = this.async()

    setTimeout(() => {
        console.log("异步loader")
        callback(null, content, map, meta)
    }, 1000);
}

3. raw loader

raw:未加工的,raw loader接收到的content是Buffer数据,这对处理图片等资源时很友好

// raw loader接收到content是Buffer数据
module.exports = function(content){
    console.log(content)
    return content
}

module.exports.raw = true;

4. pitch loader

开发loader

1.自定义clean-log-loader

clean-log-loader能自动清除js中的console.log语句

module.exports = function(content){
    //清除文件中的console.log语句
    return content.replace(/console\.log\(.*\);?/g,'')
}

2. 自定义banner.loader

const schema = require('./schema.json')

module.exports = function (content) {
    //schema对options的验证规则
    //schema符合JSON Schema的规则
    const options = this.getOptions(schema)
    const prefix = `
        /* 
        * Author: ${options.author}
        */
    `
    return prefix + content
}

3. babel-loader

const schema = require("./schema.json")
const babel = require("@babel/core")

module.exports = function(content){
    const options = this.getOptions(schema)
    const callback = this.async()
    //使用babel对代码进行编译
    babel.transform(content, options, function (err, result) {
        if(err) callback(err)
        else callback(null,result.code)
    })
}

Plugin

第一个plugin

  1. webpack加载webpack.config.js中所有配置, 此时就会new TestPlugin(), 执行插件的constructor
  2. webpack创建compiler对象
  3. 遍历所有plugins中的插件,调用插件的apply方法
  4. 执行剩下编译流程(触发各个hooks事件)
class TestPlugin {
    constructor(){
        console.log('TestPlugin constructor')
    }

    apply(compiler){
        console.log('TestPlugin apply')
    }
}

module.exports = TestPlugin

注册hooks