美文网首页
你必须要会的webpack

你必须要会的webpack

作者: 贰玖是只猫 | 来源:发表于2021-04-22 08:43 被阅读0次

    webpack是我们目前最流行的模块化打包工具,即使vue3.0推出的vite,也很难动摇webpack本身的大市场份额。

    模块化打包工具存在的意义

    模块化打包工具为了解决模块化过程中存在的问题而出现的。

    • ES Modules 存在环境兼容问题。虽然大多数浏览器目前都已经支持es module 语法,但是由于我们无法限定用户的浏览器使用方式,因此还需要我们去做此问题的兼容。
    • 模块文件过多导致的网络请求频繁问题
    • 不止js存在模块化,所有的前端资源都需要模块化
      之前我们说过webpack并不是一个自动化构建工具,而是一个模块化打包工具,就是因为这一点。

    目的

    • 新特性代码编译
    • 模块化 js 打包
    • 支持不同类型的资源模块

    Webpack 初识

    webpack 作为一个模块打包器(Module bundler)可以解决上述的问题。它利用模块加载器(Loader)对模块化的代码编译,并且有代码拆分的能力避免打包后的文件过大的问题产生。

    快速上手

    首先我们初始化一个示例项目

    //目录
    ├── index.html
    ├── package.json
    ├── src
    │   ├── index.js
    │   └── plugins.js
    └── yarn.lock
    
    // plugins.js
    export const createP = (text) => {
        let eleP = document.createElement("p")
        let temNode = document.createTextNode(text)
        eleP.appendChild(temNode)
        return eleP;
    }
    // index.js
    import {createP} from "./plugins.js"
    const crp = createP("hello world")
    document.body.appendChild(crp)
    
    //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 src="src/index.js" type="module"></script>
    </body>
    
    </html>
    

    安装 webpack & webpack-cli
    执行 yarn webpack
    会生成一个

    ├── dist
    │   └── main.js
    

    我们引入的时候只需要将目录修改为此目录并且删掉type = module即可
    <script src="dist/main.js"></script>
    在webpack 4.0之后,会给用户提供一些默认配置,我们刚才执行的时候可以看出入口文件以及输出路径都已经默认配置好了,如果我们需要调整或者需要更多的自定义配置需要我们再根目录创建一个 webpack.config.js

    const path = require("path")
    
    module.exports = {
        entry: "./src/index.js", //使用相对路径,输入路径
        output: { 
            filename: "bundle.js",  //输出文件名
            path: path.join(__dirname, 'dist') //输出路径
        }
    }
    

    倘若使用过以上代码打包过,会发现执行过程中会有一个警告

    WARNING in configuration
    The 'mode' option has not been set, webpack will fallback to 'production' for this value.
    Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
    You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
    

    提示我们的是缺号mode属性,默认使用了production属性,他的作用是在生产打包的时候默认使用一部分优化的方式。比如文件压缩等。但是在我们开发的时候很显然是不需要的,反而是需要一部分帮我们排查开发过程中bug的工具。
    在这里webpack提供了三种模式,具体使用为:

    • yarn webpack --mode development
    • yarn webpack --mode none
    • yarn webpack --mode production
      node什么都不提供,development会在代码里提供调试辅助
      同样我们也可以直接添加在配置文件中:
    const path = require("path")
    
    module.exports = {
        mode: "development",
        entry: "./src/index.js", //使用相对路径,输入路径
        output: { 
            filename: "bundle.js",  //输出文件名
            path: path.join(__dirname, 'dist') //输出路径
        }
    }
    

    进阶使用

    之前我们提到过webpack可以打包所有的前端资源,那么我们从打包css模块入手

    • yarn add css-loader style-loader
      css-loader的作用是将css打包,但是打包后是生成了一个模块,并没有加载到页面上,所以需要我们再引入style-loader这个加载器,使其能够渲染到页面上。
      配置:
    const path = require("path")
    
    module.exports = {
        mode: "none",
        entry: "./src/index.js", //使用相对路径,输入路径
        output: { 
            filename: "bundle.js",  //输出文件名
            path: path.join(__dirname, 'dist') //输出路径
        },
        module: {
            rules: [
                {
                    test: /.css$/,
                    use: [
                        "style-loader", //从后往前执行
                        "css-loader"
                    ]
                }
            ]
        }
    } 
    

    注意:使用多个loader的时候需遵循从后往前执行的逻辑

    • yarn add file-loader & url-loader
    rules: [
                {
                    test: /.png$/,
                    use: {
                        loader: "url-loader",
                        options: {
                            limit: 10 * 1024 //10KB
                        }
                    }
                }
            ]
    

    10KB以内会使用url-loader,将图片转成data64。此用法需要安装file-loader,超过10KB后会使用file-loader

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

    插件执行完之后会返回一个js形式的字符串,如果自行开发loader的话需要return这么一个js形式的字符串

    • loader 专注实现资源模块加载
    • plugin 解决其他自动化工作

    常见插件

    • clean-webpack-plugin
      用来打包前清理打包后的目录,避免脏数据
    • html-webpack-plugin
      用来打包html文件,模板渲染html文件并打包
    • copy-webpack-plugin
    const {CleanWebpackPlugin} = require("clean-webpack-plugin")
    const HtmlWebpackPlugin = require("html-webpack-plugin")
    const CopyWebpackPlugin = require("copy-webpack-plugin")
    ...
    plugins: [
           new CleanWebpackPlugin(), 
           new HtmlWebpackPlugin({  //可以new多个分别指向不同的template文件
               title: "your html title",  //对象里的属性可在页面中模板渲染
               meta: {
                   viewport: "width=device-width"
               }
               template: './index.html'
           }),
           new CopyWebpackPlugin({
               //"public/**"
               "public"
           })
       ] 
    

    自定义插件

    class MyPlugin {
       apply(complier) { // 插件要求 apply 方法
           
           complier.hooks.emit.tap("MyPlugin", compilation => {
               // compilation 是需要执行插件的上下文
               for (const name in compilation.assets) { //遍历所有文件
                   // name ===> 文件名
                   // compilation.assets[name].source() ===> 文件内容
                   if (name.endsWith(".js")) {
                        const contents = compilation.assets[name].source()
                        const withoutComment = contents.replace(/\/\*[a-z 0-9*]+\*\//g, "")
                        compilation.assets[name] = {
                            source: () => withoutComment,
                            size: () => withoutComment.length
                        }
                   }
               }
           })
       }
    }
    

    Webpack-dev-server

    webpack-dev-server为我们开发的时候提供服务器支持,并且还有强大的api代理功能,处理跨域问题。

        devServer: {
            contentBase: "./public", //静态资源
            proxy: {
                "/api": {
                    target: "http://xxx.xx.xx", //http://localhost:8080/api ==> http://xxx.xx.xx/api
                    pathRewrite: {
                        "^/api": "" //http://localhost:8080/api ==> http://xxx.xx.xx
                    },
                    changeOrigin: true //不能使用localhost:8080 作为主机名请求 xxx.xx.xx
                }
            }
        },
    

    Source Map

    source map 是对你编译过后的代码保留映射关系,使得我们开发过程中debug的时候,能够按照我们想象的逐行调试。
    而webpack中提供了12种不同的sourcemap模式,每种方式生成sourcemap的效率和效果都不同。


    image.png
    • eval
      eval 模式下是不生成source-map的,但是他会在打包后的bundle.js里保留一个映射关系
      # sourceURL=webpack://use-webpack/./src/index.js?"
      因此此模式是最快的,但是无法定位具体的代码行数
    • eval-source-map
      生成sourcemap 报错可定位到行、列
    • cheap-eval-source-map
      报错只可定位到行,而且是编译过后的
    • cheap-module-source-map
      报错定位到行,是源代码的行
    • inline-source-map
      采用给文件中以dataurl的格式附加映射的方式,提供sourcemap,会使文件体积变大
    • hidden-source-map
      在开发工具中看不到映射关系,通常用来接第三方包
    • nosources-source-map
      可在开发工具中看到报错的行列信息,但是不可以看到源代码

    推荐: cheap-module-source-map(开发) none(生产)

    模块热更新(HMR)

    • 启用热更新
    //开头引入
    const webpack = require("webpack")
    
    ...
    
    devServer: {
          hot: true,
         // hotOnly: true  可避免因热更新导致报错被刷新
     },
    
    ...
    
    plugins: [
            new webpack.HotModuleReplacementPlugin(),
    ]
    

    对于css模块,由于有style-loader的存在会默认热更新。
    由于对于js不同的模块有不同的业务逻辑,因此就需要去主动调用webpack提供的HMR api去处理因js模块变化导致的页面刷新。

    module.hot.accept("./plugins", () => {
    //处理逻辑
    })
    

    Mode

    我们开发和生产环境下对webpack的配置是不同的,因此需要我们去在不同的阶段执行不同的配置。通常有如下两种配置方法:

    • 配置文件根据环境不同导出不同的配置
    module.exports = (env, argv) => {
        const config = {
            ...
            // 这里用来存放两个环境的公用配置,可以默认为dev环境的配置
        }
    
        if (env === "production") {
            // 这里用来存放prod配置
            config.mode = "production"
            config.devtool = false
            config.plugins = [
                ...config.plugins, // 插件叠加 新增通常包含打包常用插件
                new CleanWebpackPlugin(),
                new CopyWebpackPlugin(["public"])
            ]
        }
    
        return config
    }
    
    • 一个环境对应一个配置文件
      常见是创建三个文件: 公共配置文件、dev配置文件、prod配置文件
    // webpack.common.js
    公共配置文件
    
    // webpack.prod.js
    const common = require("./webpack.commom")
    module.exports = merge(common, {
    mode: "production",
    plugins: [
      // production 配置
    ]
    })
    
    // webpack.dev.js
    与prod类似,只需要merge dev环境的配置即可
    
    

    使用 yarn webpack --mode production 生产环境打包就会注入环境变量

    我们还可以通过 definePath 这个插件去区分不同环境下的环境变量对象

            new webpack.DefinePlugin({
                BASE_URL: JSON.stringify("https://api.xx.xx")
            })
    

    直接在插件中使用 BASE_URL即可,这里需要强调的一点是value值是一个js代码块。

    Tree Shaking

    tree shaking 是webpack 的一种清理冗余代码、无效代码的功能。它会在生产模式下自动开启。

    module.exports = {
      optimization: {
        userExports: true, //对于未引用的不导出
        minimize: true //压缩清理未引用代码
      }
    }
    
    • 代码分割
      为了解决打包过程中可能存在的打包后文件体积过大,或者文件过多过碎的情况,可以利用webpack的代码分割功能,避免这一情况或者说将这种情况作出优化。
    • 多入口打包
      常用于多页面应用
      const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: {
        index: './src/entry1.js',
        album: './src/entry2.js'
      },
    optimization: {
      splitChunks: {
        chunks: "all"   //将多入口的所有公共模块分离打包
      }
    },
      output: {
        filename: '[name].bundle.js'
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index1.html',
          filename: 'index1.html',
          chunks: ['index1']
        }),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index2.html',
          filename: 'index2.html',
          chunks: ['index2']
        })
      ]
    }
    
    • 动态导入
      我们可以在模块文件中,通过import模块的形式进行动态导入。我们不需要配置代码,只需要在代码中按照逻辑按需导入即可
      形如
    import("./posts/posts").then(module => {
     
    })
    

    MiniCssExtractPlugin

    我们以上说的都是对js模块的打包优化,我们也可以通过MiniCssExtractPlugin这个插件对css进行打包优化处理。
    我们之前使用的”style-loader“是将样式通过style 标签的形式导入。
    yarn add mini-css-extract-plugin

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

    然而我们这个时候打包的话会发现只是将所有的css文件打包到了一个文件下,并没有压缩,那么这个时候就用到了我们的下面这个插件 - OptimizeCssAssetsWebpack

    OptimizeCssAssetsWebpack

    yarn add optimize-css-assets-webpack
    插件有两种使用方式:
    第一种:

    const OptimizeCssAssetsWebpack = require("optimize-css-assets-webpack")
    ...
    plugins: [
       new MiniCssExtractPlugin(),
       new OptimizeCssAssetsWebpack()
    ]
    

    第二种:

      optimization: {
        minimizer: [
          new OptimizeCssAssetsWebpackPlugin()
        ]
      },
    

    使用第二种方式的时候要注意,因为本身有默认的js压缩配置,但我们使用这种方式的时候,css可以正常压缩,反而js不能正常压缩了。是因为我们的当前配置覆盖了默认配置,需要我们去使用一个新的插件去满足js压缩的需求

    • TerserWebpackPlugin
      yarn add terser-webpack-plugin
    const TerserWebpackPlugin = require('terser-webpack-plugin')
    ...
      optimization: {
        minimizer: [
          new TerserWebpackPlugin(),
          new OptimizeCssAssetsWebpackPlugin()
        ]
    

    文件名 Hash

    由于当我们将项目部署到线上后,无论是我们再远端服务器还是浏览器都经常存在缓存的情况。当我们二次部署后,由于缓存尚未达到过期时间,相同的文件我们优先读缓存,在我们不去强制刷新的情况下,往往读到的不是最新的文件,所以就需要我们去使用 Hash 标记我们的文件

    • hash // 所有文件用同一个hash值,一个文件更新,所有hash值都变更
    • chunkhash // 更新同一个chunk的hash,引用的文件对应hash也变更
    • contenthash // 只更新变更的文件和引用的文件的hash

    相关文章

      网友评论

          本文标题:你必须要会的webpack

          本文链接:https://www.haomeiwen.com/subject/vijakltx.html