前端模块打包工具 Webpack

前端模块打包工具 Webpack

打包工具解决的是前端整体的模块化,并不是单指JavaScript模块化

核心特性:

  1. 模块打包器(Module bundler)
  2. 模块加载器(Loader)
  3. 代码拆分(Code Splitting)
  4. 资源模块(Asset Module)
  • 在webpack4.0以后支持0配置的方式直接启动打包
  • 默认会将src/index.js 打包到 dist/main.js
  • webpack.config.js是运行在node环境中的js文件


下面是一个简单的webpack.config.js文件

const path = require("path");
// 自定义入口和导出文件
module.exports = {
    mode: "development", // 打包模式
    entry: "./src/main.js",  // 入口文件
    output: {
        filename: "bundle.js", // 输出文件
        path: path.join(__dirname, "output"), // 输出文件所在目录
    },
    module: {
        rules: [
            {
                test: /.css$/, // 匹配打包过程中遇到的文件路径
                // 匹配到文件所使用的loader 并且是从后往前执行,先执行的要放在数组的最后面
                use: [
                    "style-loader", // 把css-loader转换的结果通过style标签的形式追加到页面上
                    "css-loader" // 将css文件转换为js模块
                ]
            },
            {
                test: /.png$/, // 打包png后缀的文件
                use: {
                    // 使用该方式的话url-loader和file-loader都要安装
                    loader: "url-loader", // 使用url-loader, 超出limit的部分该加载器会调用file-loader
                    options: {
                        limit: 10 * 1024, // 限制大小不能超过10kb, 小文件使用url-loader
                    },
                },
            },
        ]
    }
};
  • 上面的代码中我们就使用到了很多加载器,我们可以对loader做一个分类
  1. 编译转换类
    例如: css-loader,
  2. 文件操作类
    例如: file-loader
  3. 代码检查类


注意:

  • Webpack只是打包工具
  • 加载器用来编译转换代码

核心工作原理

webpack会根据配置找到其中一个文件作为打包入口,一般都是js文件, 然后根据入口文件中的import,require语句解析推断出所依赖的资源模块,分别解析每个资源模块对应的依赖,就会形成一个所有文件的关系依赖树,最后webpack会递归这个依赖树,找到每个节点所对应的配置文件,根据配置文件中的rules属性找到模块对应的加载器,让加载器去加载这个模块,将结果放到打包结果文件当中

  • Loader是Webpack的核心特性,通过不同的Loader可以加载任何类型的资源

开发一个Loader

  • 综上我们自己开一个md的loader,以至于对Webpack的核心特性Loader的理解
  1. 先创建一个md文件
## 关于我

张轩赫, 一名手艺人~~~
  1. 接下来我们写Loader.js文件
// npm安装一个解析md文件的模块
const marked = require("marked");
// 每个Loader必须导出一个函数, 用source接收参数,然后return将处理结果输出
module.exports = (source) => {
    // 解析markdown文件,解析为了html代码
    const html = marked(source);
    // 返回的必须是一个标准的js代码,返回的结果直接拼接到模块当中
    return `module.exports = ${JSON.stringify(html)}`;
};
  1. 接下来我们配置一个webpack.config.js文件
const path = require("path");
module.exports = {
    mode: "none", // 打包模式
    entry: "./src/main.js", // 入口文件
    output: {
        filename: "bundle.js", // 打包后文件
        path: path.join(__dirname, "dist"), // 打包后文件目录
    },
    module: {
        rules: [
            {
                test: /.md$/, // md文件
                use: "./markdown-loader.js", // 这里支持相对路径,使用自己开发的md-loader
            },
        ],
    },
};
  1. 最后我们执行打包指令
npx webpack
  • 会多出一个dist文件夹且会打包出一个bundle.js文件,打包的第100行代码结果如下
/* 1 */
/***/ (function(module, exports) {

module.exports = "<h2 id=\"关于我\">关于我</h2>\n<p>张轩赫, 一名手艺人<del>~</del></p>\n"

/***/ })
/******/ ]);


至此一个简单的Loader功能就实现了,我们可以发现Loader的工作原理实际上就是文件从输入到输出的转换,对同一个文件可以依次使用多个Loader

插件机制


插件机制是Webpack中另外一个核心特性,目的是增强Webpack自动化能力,解决项目中除了资源加载以外的其他自动化工作

// 导入清除输出目录插件模块(大部分插件导出的都是一个类型)
// 该插件需要解构成员
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
// 默认导出的就是插件的类型
const HtmlWebpackPlugin = require("html-webpack-plugin");
    plugins: [
       // 清除输出目录
        new CleanWebpackPlugin(),
        // 用于生成 index.html
        new HtmlWebpackPlugin({
            title: "webpack", // 配置属性title,可以自定义配置
            filename: "index.html", // 输出的文件名,默认输出index.html
            template: "./src/index.html", // 模板文件 相对路径
        }),
        new CopyWebpackPlugin({
            patterns: [
                { from: "public", to: "./" }, // 将public目录下的所有文件拷贝到输出目录
            ],
        }),
    ],


以上是我们会常用的三个插件,插件特性相比于Loader(加载器)拥有更宽的能力范围.

  • 那么webpack的插件机制是怎么实现的呢???


其实,Plugin是通过钩子机制实现的,类似于web中的事件.

  • plugin(插件)必须是一个函数或者一个包含apply方法的对象

开发一个插件

  • 接下来我们自己开发一个简单的插件,该插件的作用是清除打包后js文件中多于的/ /代码
  1. 直接在webpack.config.js文件中定义
// 定义一个插件类,包含apply方法
class MyPlugin {
    // 根据插件特性,要么是一个函数,要么是一个具有apply方法的对象. 参数complier是所有的配置信息,同时也需要这个对象去注册钩子函数
    apply(complier) {
        console.log("MyPlugin 启动");
        // 访问到emit钩子,通过tap方法注册一个钩子函数
        // 接收两个参数, 第一个参数是插件名称, 第二个参数是此次打包的上下文;
        complier.hooks.emit.tap("MyPlugin", (compilation) => {
            // 获取资源文件信息
            for (let i in compilation.assets) {
                console.log("打包后每个文件的名称" + i);
                // 该插件只处理JS文件
                if (i.endsWith(".js")) {
                    // 文件资源内容 通过source方法可以拿到文件内容
                    const contents = compilation.assets[i].source();
                    const withoutComments = contents.replace(
                        /\/\*\*+\*\//g,
                        ""
                    );
                    // 定义的source,size这些方法必须是一个函数,否则打包失败
                    compilation.assets[i] = {
                        source: () => withoutComments,
                        // webpack要求必须有size这个方法,没有就报错
                        size: () => withoutComments.length,
                    };
                    // 其实就是重新定义了source,size方法
                    // compilation.assets[i].source = () => withoutComments;
                    // compilation.assets[i].size = () =>  withoutComments.length;
                }
            }
        });
    }
}
  1. 在webpack.config.js文件中直接使用
plugins: [ 
        new MyPlugin(),
    ]


以上,一个webpack简单的插件就开发完成了!!!

  • 那么现在我们就清楚的知道了,插件(plugin)是通过webpack生命周期的钩子函数中挂载函数实现的

Webpack-dev-server


我们现在编译了代码也打包了代码,现在当然是要模拟服务器来跑代码了


常用的插件有browser-sync, serve, http-server ... 很多很多.但是这些插件都有一个问题是,需要我们有两次的操作,先执行webpack打包,然后再执行服务器(就算是监听了webpack的行为配合browser-sync,也要开启两个控制台来跑)


哈哈,其实说了这么多,表达的意思就是,既然已经在用webpack,那么就用webpack的插件吧~~~

  • webpack有一个插件集成了自动编译 和自动刷新浏览器等功能
  • 安装了这个插件之后,这个插件有一个指令 webpack-dev-server
  • 这个插件运行的结果并不会将结果打包到磁盘当中(没有dist目录), 他是将打包结果暂时存放在内存当中的,减少了磁盘独写操作


只要是webpack输出的文件都可以直接被访问

devServer: {
    // 额外为开发服务器指定一个静态资源目录
    contentBase: "./public",
},


可能有些人会问了, CopyWebpackPlugin 那个插件类不就是做了移动静态资源文件的工作吗?, 事实确实如此,但是在实际开发中,避免多次copy, 尽量不使用copy-webpack-plugin, 而是在devServer这个对象下配置一个contentBase. 只有在项目要测试上线前,再使用copy-webpack-plugin

  • 在实际开发中应该也会经常遇到跨域的问题
  • devServer通过配置可以在开发阶段解决跨域问题


配置如下:

devServer: {
    proxy: {
        // 请求路径前缀
        "/api": {
            // http://localhost:8080/api  => https://api.github.com/api
            target: "https://api.github.com", // 代理目标
            pathRewrite: {
                "^/api": "", // pathRewrite 代理路径的重写(会以正则的方式替换) 根据实际项目路径进行配置,通常为空
            },
            // 目前我的配置重写了路径之后相当于是, http://localhost:8080  => https://api.github.com
            changeOrigin: true, // 不能使用localhost:8080 作为请求代理目标的 主机名
        },
    },
},


接下来我们在开发阶段通过/api 访问的 https://api.github.com 地址下的接口当将会是同源接口,不存在跨域问题

  • 但是现在存在一个自动刷新的问题:
    • 每次的代码修改都是通过页面的刷新更新到页面中, 但是我们希望页面不刷新的情况下,模块也可以及时刷新(要求真多),这样开发起来才更方便

热更新


HMR(Hot Module Replacement)


模块热替换(模块热更新): 应用运行过程中实时替换某个模块,应用状态不受影响


这么强大的功能,如何启动呢?


很好的是HMR已经集成在了webpack-dev-server中,我们不需要再安装其他模块了

  1. 使用指令 webpack-dev-server --hot 可以开启
  2. 在配置文件通过配置开启
const webpack = require("webpack");
module.exports = {
    // 省略其他配置
    devServer: {
        hot: true,
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
    ],
}
  • 通过简单的配置即可实现样式的热更新, 但是修改JS代码还是刷新了网页,这是怎么回事呢?


因为style-loader内部已经实现了样式的热更新. 有些人可能会说,项目中啥也没配置也一样可以热更新啊?


因为用了框架,然后框架内部做了处理,所以可以达到js热更新

  • 如果我们要配置则需要在入口文件里设置
  • 也就是main.js文件内配置 HMR API
// 注册一个模块更新后的处理事件
module.hot.accept("依赖模块的路径", () => {
    // 处理函数 这里是修改后重新赋值的一些逻辑处理
    console.log("模块更新了");
});
  • HMR注意事项
  1. 处理 HRM 的代码报错会导致自动刷新


修改开启热更新关键字即可

devServer: {
        // hot: true, // 开启热更新报错刷新页面
        hotOnly: true, // 报错后不刷新页面
    },
  1. 没有启用 HMR 的情况下, HMR API报错,没有accept


在webpack.config.js配置文件中没有开启热更新导致的(上面代码的参数值没有设置为true)


// 先判断是否存在是否开启插件 存在这个module.hot对象
if(module.hot) {
    // 注册一个模块更新后的处理事件
    module.hot.accept("依赖模块的路径", () => {
        // 处理函数 这里是修改后重新赋值的一些逻辑处理
        console.log("模块更新了");
    });
}
  1. 代码中多了一些与业务无关的代码


生产环境注释掉该插件即可,正如官网文档所说,千万不要在生产模式中使用该插件

plugins: [
    // 注释掉这个插件 处理热更新逻辑不会被打包
    // new webpack.HotModuleReplacementPlugin(), 
],

不同环境下的配置


一般有两种方式:

  1. 配置文件根据环境不同导出不同配置
  2. 一个环境对应一个配置文件(多文件配置)
  • 在配置文件中配置不同配置:
const webpack = require("webpack")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin")
// 传入的, 环境名参数,argv运行环境的所有参数
module.exports = (env,argv) => {
    const config =  {
        mode: "development",
        entry: "./src/main.js",
        output: {
            filename: "js/index.js",
        },
        devtool: "source-map",
        module: {
            rules: [
                {
                    test: /\.css$/,
                    use: ["style-loader", "css-loader"],
                },
                {
                    test: /\.(png|jpe?g|gif)$/,
                    use: {
                        loader: "file-loader",
                        options: {
                            outputPath: "img",
                            name: "[name].[ext]"
                        }
                    }
                },
            ],
            ],
        },
        plugins: [
            new HtmlWebpackPlugin({
                title: "Webpack HMR",
                template: "./src/index.html",
            }),
            // new webpack.HotModuleReplacementPlugin(), // 使用热替换插件
        ],
        devServer: {
            // hot: true, // 开启热更新
            hotOnly: true, // 报错后不刷新页面
        },
    };
    // 判断是不是生产环境
    if(env === "production") {
        config.mode = "production"
        config.devtool = false
        config.plugins = [
            ...config.plugins,
            // 下面两个插件时在开发阶段可以省略的插件
            new CleanWebpackPlugin(), // 清空打包目录
            new CopyWebpackPlugin({
                patterns: [
                    { from: "public", to: "./" }, // 将public目录下的所有文件拷贝到输出目录
                ],
            }),
        ]
    }
    return config
}
  • 多文件配置
  1. 要一个公共的配置文件,随便定义名称比如 webpack.common.js,里面设置基础的配置内容
  2. 后重新定义一个webpack.prod.js的文件,用来打包生产模式
// 引入公共配置文件
const common = require("./webpack.common");
// 生产环境下需要的两个插件
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const copyWebpackPlugin = require("copy-webpack-plugin");
// webpack中合并参数的方法,结构出来合并的方法
const { merge } = require("webpack-merge");
module.exports = merge(common, {
    mode: "production",
    plugins: [
        new CleanWebpackPlugin(),
        new copyWebpackPlugin({
            patterns: [
                { from: "public", to: "./" }, // 将public目录下的所有文件拷贝到输出目录
            ],
        }),
    ],
});


最后执行命令 进行指定文件的打包

npx webpack --config webpack.prod.js


指令较长,可以将这个指令设置在package.json文件的scripts对象内

"build": " webpack --config webpack.prod.js"


就这样,一个多文件不同环境的打包配置就完成了

webpack内置的优化功能

  • DefinePlugin
    • 该插件的作用是为代码注入全局成员
    • 该插件会自动启用,会向全局注入一个process.env.NODE_ENV常量,来表示当前的运行环境
  • Tree-Shaking
    • 在生产模式下自动开启,清除没有用到的代码
    • 实现原理:
      • webpack的配置文件内添加一个optimization对象
 // 集中配置webpack内部优化功能
optimization: {
    usedExports: true, // 只导出外部使用的代码,
    minimize: true, // 移除未引用过的代码并压缩
    sideEffects: true, //检查package.json中有没有 sideEffects这个字段,值是标识有副作用的代码文件,作用不打包没有标识且没有用到的模块
},


sideEffects 需要在package.json文件中配置参数来标识有副作用的代码,使得这些代码会被打包

"sideEffects": [
    "./src/extend.js",
    "*.css"
]

代码分割

  • 之前我们都在说webpack是将所有资源文件打包到一个文件里,现在又要分割开???
  • 正所谓物极必反,资源太大不行,太散也不行,所以模块打包时极其有必要的
  • 根据设置的规则打包到不同的bundle.js


代码分割的两种方式:

  1. 多入口打包
  2. 动态导入
  • 多入口打包
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    mode: "none",
    // 多入口配置
    entry: {
        index: "./src/index.js",
        album: "./src/album.js",
    },
    // 多出口配置(动态参数)
    output: {
        filename: "[name].bundle.js",
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: ["style-loader", "css-loader"],
            },
        ],
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: "Multi Entry index",
            template: "./src/index.html",
            filename: "index.html",
            chunks: ["index"], // 每一个打包入口会形成一个独立的chunk
        }),
        new HtmlWebpackPlugin({
            title: "Multi Entry album",
            template: "./src/album.html",
            filename: "album.html",
            chunks: ["album"], // 每个页面配置不同的chunk
        }),
    ],
};

在不同的入口文件中肯定存在公共的模块,如果项目很大,那么就会有很多重复的引入代码,这肯定是不好的
我们只需要在webpack优化配置中添加一个参数即可

optimization: {
    splitChunks: {
        chunks: "all", // 提取所有的公共模块到一个文件中
    }
}
  • 动态导入

让模块实现按需加载,提高响应速度
动态导入的模块会被自动分包


只要使用按需加载的方式导入模块,webpack打包时会自动处理分包和按需加载

import("./模块").then((module) => {
    console.log(module)
});


只不过这样打包出来的文件会以1、2、3数字开头,如果你想自定义打包后文件名称则可以使用魔法注释

import(/* webpackChunkName: "index" */ "./模块").then((module) => {
    console.log(module)
});
// 这样打包后的js文件就会以index开头,替换默认的1、2、3这些数字开头


mini-css-extract-plugin 将css代码提取出来
如果使用mini-css-extract-plugin这个插件的话就不需要style-loader这个加载器,因为style-loader是将样式通过style标签注入,使用了mini-css-extract-plugin插件提取出的css文件通过link引入到文件中,


但是只单纯使用mini-css-extract-plugin提取css代码的话,提取出来代码是未被压缩过的,因为webpack默认只会压缩js的文件,其他的文件都需要插件支持,同样的这里还需要结合另外一个插件optimize-css-assets-webpack-plugin来压缩打包后的css代码


官方推荐将该插件定义在minimizer数组中

optimization: {
    // 定义minimizer数组的话会认为自定义使用压缩插件 , 所以这里还需要再引入webpack内置压缩js的插件
    minimizer: [
        new OptimizeCssAssetsWebpackPlugin(), // 压缩打包后的css文件
        new TerserWebpackPlugin(), // 压缩打包后的js文件
    ], 
},

输出文件名Hash


缓存策略中,失效时间过长的话,一旦应用发生更新,不能及时更新到客户端,为了解决这个问题,我们可以在生产模式下的文件名使用Hash,一旦文件有变化,文件名称也会发生变化


[hash] 所有的文件拥有同样的hash值
[chunkhash] 同一路径的打包hash值相同
[contenthash]文件级别的hash,不同的文件不同的hash


最优的方式: 使用contenthash

output: {
    filename: "[name]-[contenthash].bundle.js", // contenthash修改哪个文件打包后哪个hash值才改变
},
讨论数量: 2

知识点挺多的

3年前

非常的详细,把webpack的工作思路说得很清楚

3年前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!