大前端 - Webpack

冯皓
2023-12-01

概述


本质

JavaScript 应用程序的静态模块打包器

核心

加载器(Loader)机制

工作流程

  1. 配置初始化 webpack 会首先读取配置文件,执行默认配置
  2. 编译前准备 webpack 会实例化 compiler,注册 plugins、resolverFactory、hooks。
  3. reslove 前准备 webpack 实例化 compilation、NormalModuleFactory 和 ContextModuleFactory
  4. reslove 流程解析文件的路径信息以及 inline loader 和配置的 loader 合并、排序
  5. 构建 module runLoaders 处理源码,得到一个编译后的字符串或 buffer。将文件解析为 ast,分析 module 间的依赖关系,递归解析依赖文件
  6. 生成 chunk 实例化 chunk 并生成 chunk graph,设置 module id,chunk id,hash 等
  7. 资源构建 使用不同的 template 渲染 chunk 资源
  8. 文件生成 创建目标文件夹及文件并将资源写入,打印构建信息

基本使用


 安装

yarn add webpack webpack-cli --dev

配置

// package.json
"scripts": {
    "build": "webpack"
}

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 用于访问内置插件

module.exports = {
    mode: 'development', // 指定打包时的工作模式
    entry: './src/main.js', // 指定 webpack 打包入口文件
    output: { // 打包后,打包结果的输出路径
        filename: 'bundle.js', // 输出文件的名称
        path: path.join(__dirname, 'output'), // 输出文件所在的目录,必须是绝对路径
        publicPath: 'dist/', // 打包后文件的发布路径,默认为空,即项目的根目录
    },
    module: { // 指定打包所使用到的 loader
        rules: [ // 配置文件编译规则
            { test: '/\.txt$/', use: 'raw-loader' }
        ]
    },
    plugins: [ // 指定使用到的插件,需要配合 require 引入
        new HtmlWebpackPlugin({template: './src/index.html'})
    ]
}

打包

yarn webpack

工作模式


production(生产模式)

  • 默认模式
  • 会自动启动优化,对代码进行压缩、编译
  • 打包结果代码会极大的丢失可读性

development(开发模式)

  • 会自动优化打包速度
  • 打包时,会自动添加一些调试过程中需要的辅助到代码中

none 

  • 运行最原始状态的打包,不会对代码做任何额外的处理

资源加载


Webpack 默认只能编译 JS 文件,会将所有文件都当成 JS 来解析编译。要编译打包其他非 JS 类型的文件,需要安装引入对应的解析模块,否则便会报错

加载方式

  • 遵循 ES Modules 标准的 import 声明
import tools from './tools.js' 
import icon from './icon.png' 
import './main.css'
  • 遵循 CommonJS 标准的 require 函数
const tools = require('./tools.js').default 
const icon = require('./icon.png') 
require('./main.css')
  • 遵循 AMD 标准的 define 函数和 require 函数
define(['./tools.js', './icon.png', './main.css'], (tools, icon) => {})
require(['./tools.js', './icon.png', './main.css'], (tools, icon) => {})
  • CSS 样式代码中的 @import 指令和 url 函数
  • HTML 代码中图片标签的 src 属性

Loader(资源加载器)


作用

  • 负责资源文件从输入到输出的转换
  • 支持链式传递
    • 对于同一个资源可以一次使用多个 loader
    • 多个 loader 按有后往前的顺序执行,即先执行的 loader 应该排在后边
  • 用于对模块源码的转换,因为 webpack 本身只支持 js 处理,loader 描述了 webpack 如何处理非 javascript 模块,并且在 build 中引入这些依赖

常用加载器

  • 编译转换类
    • 将加载的资源模块转化成 JS 代码模块
  • 文件操作类
    • 会将导入的文件拷贝到输出的路径
  • 代码检查类
    • 统一代码风格
    • 一般不会自主修改代码

文件资源加载器

// 安装
yarn add file-loader --dev

// 导入文件
import icon from './icon.png'
const img = new Image()
img.src = icon
document.body.append(img)

// 配置 webpack.config.js
module: {
    rules: [
        {
            test: /.png$/,
            use: 'file-loader'
        }
    ]
}

URL 资源加载器

  • Data URLs
    • 特殊的 URL 协议,可以用来直接表示一个文件的内容
    • 引用时不会发起 http 请求
  • url-loader
    • 可以将(图片)文件转化成 Data URL,从而直接在 JS 中导入
    • 可针对小文件(小于10K)使用,减少请求的次数
    • 大文件(大于10K)则应单独提取存放,提高加载速度
data:text/html;charset=UTF-8,<h1>html content</h1>
data:image/png;base64,iVBodfasfjl...jfoasfe

// 安装 url-loader
yarn add url-loader --dev

// 配置 webpack.config.js
module: {
    rules: [
        {
            test: /.png$/,
            use: {
                loader: 'url-loader',
                options: {
                    limit: 10 * 1024 // 10KB,即只将小于 10KB 的文件进行转化
                }
            }
        }
    ]
}

Plugin(插件)


本质

一个函数或者一个包含 apply 方法的对象。

工作机制

Plugin 通过在生命周期的钩子中挂载函数实现扩展

作用

增强 Webpack 自动化能力,解决其他自动化工作,如:清除 dist 目录、拷贝静态文件至输出目录、压缩输出代码等。

常用插件

  • 自动清除输出目录

// 安装
yarn add clean-webpack-plugin --dev

// 配置 webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
    plugins: [
        new CleanWebpackPlugin()
    ]
}
  • 自动生成使用 bundle.js 的 HTML

// 安装
yarn add html-webpack-plugin

// 配置 webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    plugins: [
        // 用于生成 index.html
        new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample', // 设置导出的 html 的 title
            meta: {
                viewport: 'width=device-width'
            },
            template: './src/index.html' // 设置导出的模板文件
        }),
        // 用于生成 about.html
        new HtmlWebpackPlugin({
            filename: 'about.html' // 设置生成的文件的文件名
        }) 
    ]
}
  • 复制静态文件

// 安装
yarn add copy-webpack-plugin --dev

// 配置 webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
    plugins: [
        new CopyWebpackPlugin([
            'pulic' // 需要复制的文件所在的目录
        ])
    ]
}

自定义插件案例

class MyPlugin {
    apply (compiler) {
        console.log('MyPlugin 启动')
        // 将插件挂载到钩子上
        compiler.hooks.emit.tap('MyPlugin', compilation => {
            // compilation => 可以理解为此次打包的上下文
            for (const name in compilation.assets) {
                if (name.endsWith('.js')) {  // 对 js 文件进行操作
                    // 获取文件的内容
                    const contents = compilation.assets[name].source()
                    const withoutComments = contents.replace(/\/\*\*+\*//g, '') // 匹配注释
                    compilation.assets[name] = {
                        source: () => withoutComments, // 返回处理结果的内容
                        size: () => withoutComments.length // 返回处理结果的大小,必须
                    }
                }
            }
        })
    }
}

// 配置 webpack.config.js
module.exports = {
    plugins: [
        new MyPlugin()
    ]
}

Dev Server


Webpack Dev Server 是由 Webpack 官方开发的工具,将自动编译和自动刷新的功能集成在一起

安装

yarn add webpack-dev-server --dev

使用

yarn webpack-dev-server
// 自动唤醒浏览器 ===>
yarn webpack-dev-server --open

原理

  • 监听文件变化
  • 当文件变化时,重新执行打包
  • 将打包后的文件暂存再内存中,而不写入磁盘
  • 内部的 http server 直接从内存中读取文件并发送给浏览器,从而减少不必要的磁盘读写操作,大大提高构建效率

配置

// webpack.config.js
module.exports = {
    devServer: {
        // 静态资源访问
        contentBase: './public' // 指定静态资源目录
        // 代理 API
        proxy: {
            '/api': {
                // http://localhost:8080/api/users => https://api.github.com/
                target: 'https://api.github.com'
                pathRewrite: { // 配置代理路径重写规则
                    '^/api': '' // 将代理路径中的指定字符串替换掉
                },
                // 不能使用当前主机名(如:localhost:8080)作为请求的主机名
                changeOrigin: true
            }
        }
    }
}

Source Map


作用

定位代码中错误信息的位置。

因为打包后运行的代码与开发的源代码之间存在较大差异,而调试和报错都是基于运行代码执行的,如此一来就很难定位到错误信息的位置。所以,就需要 Source Map 来描述结果代码和开发源码之间的对应关系。

配置

// webpack.config.js
module.exports = {
    devtool: 'source-map'
}

模式

  • eval
    • 是否使用 eval 执行模块代码
  • cheap
    • Source Map 是否包含行信息
  • module
    • 是否能够得到 Loader 处理之前的源代码

Source Map 模式对比

devtool

build

rebuild

production

quality (lo: lines only)

(none)

fastest

fastest

yes

bundled code

eval

fastes

fastest

no

generated code

cheap-eval-source-map

fast

faster

no

transformed code (lo)

cheap-module-eval-source-map

slow

faster

no

original source (lo)

eval-source-map

slowest

fast

no

original source

cheap-source-map

fast

slow

yes

transformed code (lo)

cheap-module-source-map

slow

slower

yes

original source (lo)

inline-cheap-source-map

fast

slow

no

transformed code (lo)

inline-cheap-module-source-map

slow

slower

no

original source (lo)

source-map

slowest

slowest

yes

original source

inline-source-map

slowest

slowest

no

original source

hidden-source-map

slowest

slowest

yes

original source

nosources-source-map

slowest

slowest

yes

without source content

HMR(模块热更新)


Dev Server 在自动编译后 ,会自动刷新页面。

但是,在页面自动刷新后,会导致原有的页面状态丢失。如,自动刷新前测试输入的内容的丢失。

配置

// 方案一
webpack-dev-server --hot

// 方案二 webpack.config.js
const webpack = require('webpack')
module.exports = {
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
}

示例

// main.js
const editor = createEditor()
document.body.appendChild(editor)

const img = new Image()
img.src = background
document.body.appendChild(img)

// 使用手动热更新的 API,会替换原有的处理方案,不会再自动刷新
if(module.hot) { // 判断是否已开启 HMR,避免 API 报错 
    // js 模块热替换
    let lastEditor = editor
    module.hot.accept('./editor', () => { // 手动处理人更新后的处理方式
        console.log('editor 模块更新了')
        const value = lastEditor.innerHTML
        document.body.removeChild(lastEditor)
        const newEditor = createEditor()
        newEditor.innerHTML = value
        document.body.appendChild(newEditor)
        lastEditor = newEditor
    })
    // 图片热替换
    module.hot.accept('./better.png', () => {
        img.src = background
        console.log(background)
    })
}

注意事项

  • 手动处理的代码中出现错误时,会自动回退使用自动刷新,从而导致无法看到错误信息
  • 没启用 HMR 的情况下,HMR API 报错
  • webpack 打包时会自动去除没有意义的代码

关于样式文件热更新问题

因为样式文件是通过 loader 处理的,在 style loader 中就会自动执行热更新。样式文件更新后只需要把对应的更新替换到原本的文件中。所以,样式文件的热更新是开箱即用的,不需手动处理。

生产环境优化


配置方案

  • 配置文件根据环境不同导出不同的配置
// webpack.config.js
module.exports = (env, argv) => {
    const config = {
        // 开发环境配置
    }
    
    if (env === 'production') {
        config.mode = 'production'
        config.devtool = false
        config.plugins = [
            ...config.plugins,
            new CleanWebpackPlugin()
            new CopyWebpackPlugin(['public'])
        ]
    }
    
    return config
}
  • 一个环境对应一个配置文件
    • webpack.common.js
    • webpack.dev.js
    • webpack.prod.js

DefinePlugin

通过伪代码注入全局成员

// webpack.config.js
const webpack = require('webpack')
module.exports = {
    mode: 'none',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js'
    },
    plugins: [
        new webpack.DefinePlugin({
            API_BASE_URL: '"https://api.example.com"'
        })
    ]
}
// main.js
console.log(API_BASE_URL)

Tree Shaking

// 配置 webpack.config.js
module.exports = {
    optimization: { // 集中配置 webpack 中的优化功能
        useExports: true, // 是否只导出被外部使用的成员
        minimize: true, // 是否移除、压缩掉未被使用的成员代码
    }
}

Tree Shaking 的作用在于去除代码中未被引用的部分,从而减轻代码的重量。

Tree Shaking 并不特指某一个配置选项,而是在 production 模式下会自动启动的一组具有优化效果的功能的搭配只用。

Tree Shaking 的实现是存在限制性的。实现 tree shaking 的前提是 ESM,即由 Webpack 打包的代码必须使用 ESM 模式。而且,在同时使用 babel-loader 时,由于低版本的 babel 可能会会先将模块转化成 CommonJS,从而导致 Tree Shaking 无法实现

合并模块

// webpack.config.js
module.exports = {
    optimization: {
        concatenateModules: true, // 是否将所有模块合并到同一个函数中
    }
}

又称为作用域提升(Scope Hoisting)。即将所有的模块尽可能地合并输出到一个函数中,提升运行效率的同时,减小代码的体积。

代码分割

  • 多入口打包

// webpack.config.js
optimization: {
    splitChunks: {
        chunks: 'all' // 提取公共模块,组成一个单独的文件
    }  
},
plugins: [
    new HTMLWebpackPlugin({
        title: 'Multi Entry',
        template: './src/index.html',
        filename: 'index.html',
        chunks: ['index']
    }),
    new HTMLWebpackPlugin({
        title: 'Multi Entry',
        template: './src/album.html',
        filename: 'album.html',
        chunks: ['album']
    })
]
  • 动态导入

// 在引用时才执行导入
const render = () => {
    const hash = window.location.hash || '#posts'
    const mainElement = document.querySelector('.main')
    mainElement.innerHTML = ''
    if (hash === '#posts') {
        // 满足条件则导入、使用
        /* 魔法注释,通过 webpackChunkName 给分包进行命名,若输出注释同名,则会被打包到一个文件中 */
        import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
            mainElement.appendChild(posts())
        })
    }
}

MiniCssExtractPlugin

将 CSS 提取打包到单独的文件当中,在通过 link 标签的方式注入到输出结果的 html 中

// 安装
yarn add mini-css-extract-plugin

// 配置 webpack.config.js
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
    optimization: {
        // 一旦配置,则会覆盖默认的压缩模式,需要重新手动配置
        minimizer: [
            new OptimizeCssAssetsWebpackPlugin(), // 压缩输出的 css 文件
            new TerserWebpackPlugin()
        ]  
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin()
    ]
}

输出文件名 Hash

添加 hash 名,可以在每次模块内容更改时得到一个新的包含 hash 值的文件名。

生产环境下,当系统识别到新的文件名时,就会重新发送文件请求,能有效避免缓存的问题。

// 项目级
// 项目中任意位置有改动,都会触发整个项目的重置、更新
module.exports = {
    output: {
        filename: '[name]-[hash].bundle.js'
    }  
}

// 目录级
// 目录下的文件有改动,则触发整个文件夹中的文件重置、更新
filename: '[name]-[chunkhash].bundle.js'

// 文件级
// 只更新有改动过的文件
filename: '[name]-[contenthash].bundle.js'

 类似资料: