你知道webpack是如何打包的吗?

158 阅读6分钟

一、Webpack 打包

  • 需要解决ESM 存在兼容性问题
  • 通过模块化划分的文件比较多,网络请求频繁
  • 所有的前端资源都需要模块化,模块化是必要的
  • 需要编译代码,将高版本转化为低版本

1.1 webpack 概述

  • 是一款前端整体的模块打包工具
  • 安装 yarn,webpack、webpack-cli
  • 通过yarn webpack运行程序,使文件压缩
yarn webpack

1.2 webpack 配置

1.2.1 webpack 配置文件

  • 默认使 src/index.js 作为入口,然后新的文件会存放在 dist/main.js
  • 对文件的操作可以配置在webpack.config.js文件中
const path = require("path")

module.exports = {
    entry:  "./src/index.js",  // 指定打包的路径
    output: {  // 输出文件设置
        filename: "bundle.js",  // 文件名
        path: path.join(__dirname, 'output') // 一定要是绝对路径
    }
}

1.2.2 webpack 工作模式

  • webpack --mode development:开发模式,会加快打包的速度
  • webpack --mode none:默认模式,会进行最原始的打包,不会对代码进行处理
  • webpack --mode production:生产模式
  • 可以在运行时使用yarn webpack --mode 模式 进行运行
  • 或者配置 webpack.config.js中的 mode 属性直接设置模式
module.exports = {
    entry:  "./src/index.js",  // 指定打包的路径
    output: {  // 输出文件设置
        filename: "bundle.js",  // 文件名
        path: path.join(__dirname, 'output') // 绝对路径
    },
    mode: "development"  // 设置相关的执行模式
}

1.2.3 webpack资源模块加载

  • css资源需要先安装css-loader,然后在webpack.config.js中设置module总的rules数组
  • 设置test获取的文件和use使用的方法
  • 运行后数据存放在指定的js文件中
  • 安装style-loader,use属性存在多个使用方法时用数组包裹,并且是从右向左依次加载
const path = require("path")

module.exports = {
    entry:  "./src/main.css",  // 指定打包的路径
    output: {  // 输出文件设置
        filename: "bundle.js",  // 文件名
        path: path.join(__dirname, 'output') // 绝对路径
    },
    mode: "none",  // 设置相关的执行模式
    module: {
        rules: [  // 针对其他资源的配置
            {
                test: /.css$/, // 匹配打包的文件路径
                use: ["style-loader","css-loader" ] // 指定匹配到的文件需要执行的loader
            }
        ]
    }
}

1.2.4 webpack导入资源模块

  • 打包入口 -》运行入口
  • 通过import引入.css资源
// main.js
import creating from './header.js'
import './main.css'

const heading = creating()
document.body.append(heading)

// heading.js
import "./heading.css"

export default () => {
    const ele = document.createElement('h2')

    ele.textContent = "hello world"
    ele.classList.add("heading")
    ele.addEventListener("click", () => {
        alert("hello wepack")
    })

    return ele
}

1.2.5 webpack 文件资源加载器

  • 安装 file-loader,到webpack.config.js中进行配置
  • 在加载的js文件夹中导入
  • 设置publicPath设置默认加载的目录 ’/‘不能省略
const path = require("path")

module.exports = {
    entry:  "./src/index.js",  // 指定打包的路径
    output: {  // 输出文件设置
        filename: "bundle.js",  // 文件名
        path: path.join(__dirname, 'output'), // 绝对路径
        publicPath: "output/"  // 加载的路径 / 不能省略
    },
    mode: "none",  // 设置相关的执行模式
    module: {
        rules: [  // 针对其他资源的配置
            {
                test: /.css$/, // 匹配打包的文件路径
                use: ["style-loader","css-loader" ] // 指定匹配到的文件需要执行的loader
            },
            {
                test: /.png$/,
                use: "file-loader"
            }
        ]
    }
}

1.2.6 data URL加载器

  • 是一种直接表示文件内容的方式,base64编码

  • 需要 url-loader 插件

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

  • 将use属性设置为对象,其中

    • loader属性依旧是加载器的名称
    • options对象为配置属性,limit为限制的大小,小于这个限制的图片使用data URLs大于的采用file-loader
  • 这种方式一定要安装file-loader和url-loader

{
                test: /.png$/,
                use: {
                    loader: "url-loader",
                    options: {
                        limit: 10 * 1024,
                        esModule: false   // 新版本file-loader的esModule属性默认是true
                    }
                }
            }

1.2.7 加载器类型

  • 编译转换类型,例如css-loader,转化为以js形式工作的css模块
  • 文件操作类型的加载器,将文件拷贝到输出的目录,同时导出文件路径,例如file-loader
  • 代码检查类,为了统一代码的风格,提高代码质量,例如eslint-loader

1.2.8 Webpack 与 ES 2015

  • webpack仅仅是完成打包工作,因为模块打包需要,所以需要处理import 和 export\

  • 需要配置babel-loader,并且需要安装babel-core核心 和 babel-preset-env规范\

  • 因为babel只是一个平台,所以需要在use中进行配置

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

1.2.9 webpack加载资源的方式

  • 遵循ESM的 import/export
  • 遵循commonjs的require(),require接收ESM默认参数时需要加.default属性获取
  • 遵顼AMD标准的define函数和require函数
  • loader加载的非js也会触发资源加载,样式代码中的@import指令和url函数

css-laoder加载资源的两种方式

@import url(./heading.css);  /* 加载资源样式 */

body {
    color: greenyellow;
    background-image: url(./icon.png);  /* 先进行css-loader 发现其他格式的就交给其他格式的loader转化 */
    background-size: cover;
}

html加载资源,比如image标签的src,不过需要先配置html-loader

{
    test: /.html$/,
    use: {
        loader: 'html-loader',  // 默认只会处理image标签的src属性
        options: {
            atts: ['img:src', 'a:href']  // 手动设置加载的标签和标签属性,需要html-loader是0.5.5版本
        }
    }
}

1.2.10 webpack核心工作原理

  • 选择一个打包入口文件,解析每一个模块的依赖
  • 再根据配置的属性rules,选择加载器,解析相关的模块,输出到打包结果中
  • loader机制是webpack 的核心

1.2.11 开发一个 loader

  • 制作一个markdown文件

两种方法

  • 一个函数中直接输出,不过return的一定是一个js格式的数据
  • 通过多个loader进行输出,完成转换

其实loader就是管道

const marked = require("marked")

module.exports = sourse => {
    const html = marked(sourse)
    // return `export default (${JSON.stringify(html)})`  // 直接返回输出的字符串,

    // 还可以 返回 html 字符串,交给下一个loader操作
    return html
}

1.3 webpack常用插件

1.3.1 自动清除插入目录插件

  • clean-webpack-plugin 插件,清除遗留的文件在创建文件夹
const { CleanWebpackPlugin } = require("clean-webpack-plugin")  // 导入插件

module.exports = {
    plugins: [  // 配置插件的位置
        new CleanWebpackPlugin()  // 创建实例并且放到plugins数组中
    ]
}

1.3.2 自动生成使用打包结果的html

  • html-webopack-plugin
  • 根目录下自建的html存在一些问题,比如引用的路径需要手动修改,
  • template属性为模板,要想模板生效,不要配置html-loader
  • 可以在模板字符串中 通过 htmlWebpackPlugin 对象获取内部相关的属性
// plugins 数组中的设置

new HtmlWebpackPlugin({
            title: "webpack plugin sample",  // 设置html文件的title主体
            meta: {
                viewport: "width=device-width"  // 视口宽度
            },
            template: "./src/index.html"  // 参照的模板
        })

./src/index.html

<h1><%= htmlWebpackPlugin.options.title %></h1>
  • 生成多个html文件,创建多个new HtmlWebpackPlugin()实例对象,设置filename属性来设置文件名
new HtmlWebpackPlugin({
            filename: "about.html"
        })

1.3.3 静态资源打包器

  • copy-webpack-plugin
  • 实例对象需要传入数组,数组内是要复制的地址,可以是通配符、文件路径、相对地址
new copyWebpackPlugin({  // 实例 copy插件  设置对象
            patterns: [  // 设置地址
                "public"   // 指定的文件目录
            ]
        })

1.3.4 开发一个插件

  • 相对于 loader,plugin拥有更宽的能力范围

  • plugin通过钩子机制实现

  • 插件必须是一个函数或者是apply方法的对象

  • compilation获取的是操作过程中的信息结果

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

class myPlugin {
    apply (compliter) {
        console.log("启动");

        compliter.hooks.emit.tap("mypl", compilation => {  // 注册函数,emit是输出文件前的事件挂载点
            // compilitation 可以理解为此次打包的上下文,打包产生的过程中产生的结果都在这个数据中

            for (const name in compilation.assets) {
                // console.log(name);  // 获取文件名
                // console.log(compilation.assets[name].source());  // 获取文件内容

                if (name.endsWith(".js")) {
                    const contents = compilation.assets[name].source()
                    const withComment = contents.replace(//**+//g, "")
                    compilation.assets[name] = {
                        source: () => withComment,  // source 是将来要操作的数据
                        size: () => withComment.length  // size 是必需的
                    }
                }
            }
        })
    }
}

1.4 webpack 提升开发体验

  • 原始的需要编译=》打包=》运行应用=》刷新浏览器
  • 理想的方法
  • 1.以http server运行
  • 2.自动编译 + 自动刷新
  1. 提供source Map 支持

1.4.1 实现自动编译

  • watch 工作模式,监视文件变化,自动重新打包
yarn webpack --watch

1.4.2 实现自动刷新

  • webpack Dev Server 集成 自动编译和自动刷新浏览器
  • 将打包结果存放在内存中
  • --open运行后会自动打开浏览器
yarn webpack-dev-server --open
  • 自动刷新的问题
  • 页面刷新会丢失状态

1.4.3 webpack Dev Server 静态资源访问

  • 在配置文件中设置 devServer配置对象
module.exports = {
        devServer: {
            contentBase: ["public"]  // 配置静态路径,可以是数组、字符串
        }
    }

1.4.4 webpack Dev Server 代理API服务

  • webpack dev server 支持配置代理,避免了跨域请求的问题
  • 实验目标,将github API代理到开发服务器
devServer: {
        contentBase: ["public"],  // 配置静态路径,可以是数组、字符串
        proxy: {  // 用于配置代理的配置

            "/api": {  // 代理的请求路径前缀
                // http://localhost:8080/api/user 相对于 https://api.github.com/api/user, 所有需要重写
                target: "https://api.github.com",  // 代理的目标

                pathRewrite: {  // 代理路径的重写
                    // http://localhost:8080/api/user 相对于 https://api.github.com/user
                    "^/api": ""  // 将代理到的路径中以 /api 开头的路由替换为空
                },

                // 默认为 false  不能使用 localhost:8080 作为请求 gitHub 主机名
                changeOrigin: true  // true 会以实际代理的主机名进行请求
            }
        }
    }

1.5 Source Map

  • 调试和报错的基于运行代码
  • 运行代码和源代码之间完全不同,无法定位错误信息

1.5.1 源代码地图

  • 映射转换过后的代码和源代码之间的关系

  • .map的文件,内容是json格式

  • version:文件使用的source map的版本

  • sources:转换前原文件的名称

  • names:代码中使用的成员名称

  • mappings:转译之后的字符与转移之前的字符的映射关系\

  • Source Map 解决了源代码与运行代码不一致所产生的调试问题\

1.5.2 webpack 配置 Source Map

  • 在devtool属性中设置”source-map“,不过现在方法很多
devtool: "source-map"  // 开发中配置的辅助工具

1.5.3 eval 模式

  • 生成的速度最快,但是只能定位模块的名称,而不知道具体的行列信息
  • 不会生成source map文件,会在模块文件中最后一个eval末尾书写//# sourceURL=xxxxx地址
devtool: "eval"
const HtmlWebpackPlugin = require("html-webpack-plugin")
const allModes = [
    'eval',
    'cheap-eval-source-map',
    'cheap-module-eval-source-map',
    'eval-source-map',
    'cheap-source-map',
    'cheap-module-source-map',
    'inline-cheap-source-map',
    'inline-cheap-module-source-map',
    'source-map',
    'inline-source-map',
    'hidden-source-map',
    'nosources-source-map'
]
module.exports = allModes.map(item => {
    return {
        mode: "none",
        devtool: item,
        entry: "./src/index.js",
        output: {
            filename: `js/${item}.js`
        },
        module: {
            rules: [
                {
                    test: /.js$/,
                    use: {
                        loader: "babel-loader",
                        options: {
                            presets: ["@babel/preset-env"]
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                filename: `${item}.html`
            })
        ]
    }
})

1.6 webpack HMR

  • HMR 全称 hot module replacement,模块热更新\

  • 运行的过程中实时替换,不会影响内容\

1.6.1 HMR开启

  • 集成在webpack-dev-server中
  • 开启只需要运行webpack-dev-server加上 --hot就会开启
  • 或者配置文件
  • 现在devServer中配置hot:true
  • 然后引入webpack中的属性.HotModuleReplacementPlugin()
  • HMR不能开箱即用,需要手动处理模块热替换逻辑
  • css样式文件经过loader处理
const webpack = require("webpack")

module.exports = {
    ……
    devServer: {
        hot: true  // 开启热加载
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()  // 模块热加载插件
    ]
}

1.6.2 HMR API

  • 在入口模块中添加module.hot.accept()注册模块更新的处理函数
  • 参数1,依赖模块的路径
  • 参数2,处理的函数
  • 这只是针对js的处理方案
let lastHeader = heading   // 获取旧元素
module.hot.accept("./header.js", () => {
    const value = lastHeader.innerHTML  // 获取状态
    document.body.remove(lastHeader)
    const newHeading = creating()
    newHeading.innerHTML = value  // 将状态赋值给新的元素
    document.body.append(newHeading)
    lastHeader = newHeading
})
  • 针对图片
  • 因为import获取的是对象的地址,所以会是实时变化的,因此icon一直是最新地址
import icon from "./icon.png"

module.hot.accept("./icon.png", () => {
    img.src = icon
    console.log(icon);
})

1.6.3 HMR 注意事项

  • 处理热替换的代码不易被发现
  • hot属性改为hotOnly,不会刷新,便于查看错误
// hot: true,
hotOnly: true,

1.7 生产环境优化

  • 模式(mode)
  • 为不同的环境创建不同的配置

1.7.1 不同环境下的配置

  • 根据不同环境导出不同配置

  • 一个环境一个人配置

  • webpack.config.js 可以导出一个函数

  • 参数1为环境名参数

  • 参数2运行过程中的所有参数

  • 适用于小型项目

module.exports = (env, argv) => {  // 环境名,运行过程中的所有参数
    const config = {
        entry: "./src/index.js",  // 指定打包的路径
        output: {  // 输出文件设置
            filename: "js/bundle.js",  // 文件名
            path: path.join(__dirname, 'dist'), // 绝对路径
            // publicPath: "dist/"  // 加载的路径 / 不能省略
        },
        mode: "development",  // 设置相关的执行模式
        devtool: "eval",  // 开发中配置的辅助工具
        devServer: {
            hot: true,
            // hotOnly: true,
            contentBase: ["public"],  // 配置静态路径,可以是数组、字符串
        },
        module: {
            rules: [  // 针对其他资源的配置

                {
                    test: /.css$/,
                    use: ["style-loader", "css-loader"]
                },
                {
                    test: /.png/,
                    use: {
                        loader: "url-loader",
                        options: {
                            limit: 10 * 1024,
                            esModule: false
                        }
                    }
                },
                {
                    test: /.js$/,
                    use: {
                        loader: "babel-loader",
                        options: {
                            presets: ["@babel/preset-env"]
                        }
                    }
                }
            ]
        },
        plugins: [  // 配置插件的位置
            new HtmlWebpackPlugin({
                title: "webpack plugin sample",  // 设置html文件的title主体
                meta: {
                    viewport: "width=device-width"  // 视口宽度
                },
                template: "./src/index.html"  // 参照的模板
            }),
            new webpack.HotModuleReplacementPlugin()
        ]
    }  // 开发模式

    if (env === "production") {
        config.mode = "production"
        config.devtool = false
        config.plugins = [
            new CleanWebpackPlugin(),
            new CopyWebpackPlugin(['public']),
            ...config.plugins
        ]
    }

    return config
}
  • 多文件配置,适合大型项目
  • 需要webpack-merge
  • 因为没有了默认配置文件,所以需要 --config 配置文件
// webpack.prod.js

const common = require("./webpack.common")
const { merge } = require("webpack-merge")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = merge(common, {   // merge 类似于 assign(),但是merge可以用于重新改变值,并且是新的空间
    mode: 'production',
    plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin({  // 实例 copy插件  设置对象
            patterns: [  // 设置地址
                "public"   // 指定的文件目录
            ]
        })
    ]
})

1.7.2 definePlugin

  • 为代码注入全局成员
  • 它是webpack内置插件,自定义数据
const webpack = require("webpack")

module.exports = {
    entry: "./src/main.js",
    output: {
        filename: "main.js",
    },
    mode: "none",
    plugins: [
        new webpack.DefinePlugin({
            API : JSON.stringify("123456")
        })
    ]
}

1.8 代码优化配置—optimization

1.8.1 tree shaking (摇树)

  • 摇掉代码中未引用的部分
  • 生产模式下自动开启
  • tree-shaking不是配置选项,它是功能搭配使用的效果
  • minimize:相当于清除枯树叶
  • usedexports:相当于标记枯树叶
const webpack = require("webpack")

module.exports = {
    ……,
    optimization : {  // 集中配置webpack内部优化功能
        usedExports: true,  // 只输出外部使用的成员
        minimize: true  // 开启压缩
    }
}

1.8.2 合并模块

  • 通过concatenateModules属性设置
  • 尽可能将多的模块合并到一个模块
module.exports = {
    ……,
    optimization : {  // 集中配置webpack内部优化功能
        usedExports: true,  // 只输出外部使用的成员
        concatenatemodules: true  // 合并尽可能多的模块
    }
}

1.8.3 tree-shaking 与 babel

  • tree-shaking必须是使用 ES module 的模块化
  • babel则是将所有的模块转换为commonjs格式,所以tree-shaking不会生效
  • 新版本中针对这个问题进行了优化,但是最保险的方法还是设置一下{modules: false},关闭 ESM转化
module.exports = {
    ……,
    rules: [
            {
                test: /.js$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            ["@babel/preset-env", { modules: false }]  // 默认属性是modules属性默认值是auto,自动转换 ESM 插件是否开启
                        ]
                    }
                }
            }
        ],
    ……
}

1.8.4 sideEffects —副作用

  • 模块除了导出成员外是否做了其他事情
  • 一般用于 npm 包标记是都有副作用,
  • webpack配置文件设置 sideEffects: true,开启这个标识,production模式下自动开启;在package.json中设置sideEffects:true/false,注明这个代码是否有副作用
  • sideEffects是把未使用但无副作用的模块一并删除,tree-shaking只是删除未使用的,但是不知道是否有其他作用
  • 两者配合可以极大的优化代码
// webpack.config.js
module.exports = {
    ……,

    optimization: {  // 集中配置webpack内部优化功能
        usedExports: true,  // 只输出外部使用的成员
        sideEffects: true,  // 开启副作用插件
        // minimize: true  // 开启压缩
    }
}

// package.json
{
    ……,
    "sideEffects": false  // 是否有副作用
}

必须确保没有副作用才能用false

否则用数组标注有副作用的文件

  • css文件会被当作副作用文件
// package.json
{
    ……,
    "sideEffects": ["./src/a.js", "./src/*.css"]  // 有副作用的文件
}

1.9 code splitting—代码分包

  • 总是合并代码,合并的文件可能非常大
  • 分包和按需加载

1.9.1 多入口打包

  • 各个部分需要单独提取
  • 在entry中配置对象,属性名为随意,最好是文件名,属性只是文件路径
  • output.filename改为“[name].bundle.js”,[name]会动态匹配entry的属性名
  • 在htmlwebpackplugin插件中设置chunks:["名字"]
module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js'  // [name]会动态命名
  },
  ……,
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index']  // 允许插入到模板中的chunks
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

1.9.2 提取公共模块

  • 在optimization中设置splitChunks对象,chunks:"all"
optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: 'all'
    }
  }

1.9.3 动态导入

  • 按需加载,提高应用响应效率
  • 通过import()方法引入,参数为地址和函数,函数的参数为模块输出的成员
const render = () => {
  const hash = window.location.hash || '#posts'

  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ''

  if (hash === "#post") {
    import("./posts/posts.js").then(({ default: posts }) => {  // 获取成员
      document.body.appendChild(posts)
    })
  } else if (hash === "#album") {
    import("./album/album.js").then(({ default: album }) => {
      document.body.appendChild(album)
    })
  }
}

1.9.4 魔法注释

  • 目的是将打包的文件进行重命名
  • /webpackChunkName:'名字'/,固定格式
  • 如果名称相同则会合并
if (hash === "#post") {
    import(/*webpackChunkName: "auble" */"./posts/posts.js").then(({ default: posts }) => {
      document.body.appendChild(posts)
    })
  } else if (hash === "#album") {
    import("./album/album.js").then(({ default: album }) => {
      document.body.appendChild(album)
    })
  }

1.9.5 MiniCssExtractPlugin

  • 目的是提取打包后的css,先安装mini-css-extract-plugin
  • 为了实现css样式的按需加载
  • 在插件中新建,在rules中用 minicssextractplugin.loader 替换style-loader
  • 超过150kb使用效果更好
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      linkType: 'text/css',
    })
  ]
}

1.9.6 optimize-css-assets-webpack-plugin

  • 压缩提取的css文件
  • 官方建议插件在optimization.minimizer中设置,但是需要重新定义js的压缩插件

1.10 输出文件名hash

  • hash:整个项目级别的,有变换就会全部变化
  • chunkhash:同一路的打包都是一样的,同一个文件夹中的位置
  • contenthash:文件级别的hash值,不同的文件就有不同的hash值
new MiniCssExtractPlugin({
      filename: "[name]-[hash:8].bundle.css"  // :num 限制位数
    }