别催了~ 正在从服务器偷取页面 . . .

webpack学习


前言

为什么需要打包?

平时开发时,项目里用到的是前端框架Vue、React,还会用到ES6模块化语法,还有Less/Sass等CSS预处理器等来开发,这些都需要编译成JS、CSS、HTML、静态资源等才能在浏览器运行,所以需要打包来完成这些处理。

有哪些打包工具呢?

  • Grunt
  • Gulp
  • Parcel
  • Webpack
  • Rollup
  • Vite

Webpack目前最为流行

Webpack的介绍

官网的介绍:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

Webpack核心概念

webpack一般不配置也是可以用的,但是我们经常也需要用配置做一些调整,一般在项目的根目录下新建一个webpack.config.js来添加配置项

入口(entry)

entry就是打包的起点,进入入口后webpack会找出哪些模块和库是入口依赖的,每个依赖项随即被处理,最后输出到bundles文件中。
entry的默认值是./src/index.js,可以在配置文件中修改,可以有多个入口文件

module.exports={
    //入口
    entry:"./src/main.js",//绝对路径、相对路径均可
}

输出(output)

output就是告诉webpack它所创建的bundles应该输出到哪里,以及如何命名这些文件(默认值是./dist)

module.exports = {
  //输出
  output: {
    //path是文件输出目录,必须绝对路径
    path: path.resolve(__dirname, "../dist"),
    //输出文件名
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    clean: true, // 自动将上次打包目录资源清空
  },
}

加载器 (loader)

loader让webpack能够处理那些非JavaScript文件
loader有两个属性:

  • test:识别哪些文件需要转换
  • use::这些文件用什么loader进行转换
    module.exports={
      //加载器
      module:{
          rules:[
              {
                 // 用来匹配 .css 结尾的文件
                test: /\.css$/,
                /*  use 数组里面 Loader 执行顺序是从右到左,
                   css-loader:负责将 Css 文件编译成 Webpack 能识别的模块
                   style-loader:会动态创建一个 Style 标签,里面放置 Webpack 中 Css 模块内容
                */
                use: ["style-loader", "css-loader"],
              },
            ...
            ]
          }
    }
    

    插件(plugins)

    扩展webpack功能,通过在webpack构建流程中注入钩子实现。
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    module.exports={
     plugins:[
        new HtmlWebpackPlugin({
          // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
          template: path.resolve(__dirname, "../public/index.html"),
        }),
      ],
    }
    

模式(mode)

开发模式(development)
开发模式就是开发代码时使用的模式。
这个模式下我们主要做两件事:
1.编译代码,使浏览器能识别运行
开发时我们有样式资源、字体图标、图片资源、html 资源等,webpack 默认都不能处理这些资源,所以我们要加载配置来编译这些资源
2.代码质量检查,树立代码规范
提前检查代码的一些隐患,让代码运行时能更加健壮。
提前检查代码规范和格式,统一团队编码风格,让代码更优雅美观。
生产模式(production)
生产模式是开发完成代码后,我们需要得到代码将来部署上线。这个模式下我们主要对代码进行优化,让其运行性能更好。
优化主要从两个角度出发:

  1. 优化代码运行性能
  2. 优化代码打包速度

    compiler对象

    compilation对象

    高级进阶

    提升开发体验

    SourceMap

    SourceMap是什么?

    SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。它会生成一个 xxx.map 文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过 xxx.map 文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。

    使用

开发模式:cheap-module-source-map
优点:打包编译速度快,只包含行映射
缺点:没有列映射

module.exports = {
  // 其他省略
  mode: "development",
  devtool: "cheap-module-source-map",
};

生产模式:source-map
优点:包含行/列映射
缺点:打包编译速度更慢

module.exports = {
  // 其他省略
  mode: "production",
  devtool: "source-map",
};

提升打包构建速度

HotModuleReplacement

HotModuleReplacement是什么?

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

用法

module.exports = {
  // 其他省略
  devServer: {
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
  },
};

//入口文件的js代码需要更改
// main.js
import count from "./js/count";
import sum from "./js/sum";
// 引入资源,Webpack才会对其打包
import "./css/iconfont.css";
import "./css/index.css";
import "./less/index.less";
import "./sass/index.sass";
import "./sass/index.scss";
import "./styl/index.styl";

const result1 = count(2, 1);
console.log(result1);
const result2 = sum(1, 2, 3, 4);
console.log(result2);

// 判断是否支持HMR功能
if (module.hot) {
  module.hot.accept("./js/count.js", function (count) {
    const result1 = count(2, 1);
    console.log(result1);
  });

  module.hot.accept("./js/sum.js", function (sum) {
    const result2 = sum(1, 2, 3, 4);
    console.log(result2);
  });
}

但是在实际开发中不会这么用的,一般用vue-loader、react-hot-loader

OneOf

OneOf是什么?

打包时每个文件都会经过所有 loader 处理,虽然因为 test 正则原因实际没有处理上,但是都要过一遍。比较慢。Oneof的作用就是只能匹配上一个 loader, 剩下的就不匹配了

用法

module.exports = {
  entry: "./src/main.js",
  output: {
    path: undefined, // 开发模式没有输出,不需要指定输出目录
    filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
    // clean: true, // 开发模式没有输出,不需要清空输出结果
  },
  module: {
    rules: [
      {
        oneOf: [
          {
            // 用来匹配 .css 结尾的文件
            test: /\.css$/,
            // use 数组里面 Loader 执行顺序是从右到左
            use: ["style-loader", "css-loader"],
          }
          ...
          ]
      }
      ]
  }
}

Include/Exclude

Include/Exclude是什么?

开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。所以我们在对 js 文件处理时,要排除node_modules 下面的文件
include,包含,只处理 xxx 文件
exclude,排除,除了 xxx 文件以外其他文件都处理

用法

module.exports = {
 ...
  module: {
    rules: [
      {
        oneOf: [
          ...
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            loader: "babel-loader",
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
    }),
     ...
  ],
 ...
};

Cache

Cache是什么?

每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了。

module.exports = {
  module: {
    rules: [
      {
        oneOf: [
         ...
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            loader: "babel-loader",
            options: {
              cacheDirectory: true, // 开启babel编译缓存
              cacheCompression: false, // 缓存文件不要压缩
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
    }),
   ...
  ],
}

Thead

Thead是什么?

多进程打包,可以开启多进程同时处理 js 文件。

const TerserPlugin = require("terser-webpack-plugin");

// cpu核数
const threads = os.cpus().length;

module.exports = {
  module: {
    rules: [
      {
        oneOf: [
         ...
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            use: [
              {
                loader: "thread-loader", // 开启多进程
                options: {
                  workers: threads, // 数量
                },
              },
              {
                loader: "babel-loader",
                options: {
                  cacheDirectory: true, // 开启babel编译缓存
                },
              },
            ],
          },
        ],
      },
    ],
  },
  plugins: [
    new ESLintWebpackPlugin({
      // 指定检查文件的根目录
      context: path.resolve(__dirname, "../src"),
      exclude: "node_modules", // 默认值
      cache: true, // 开启缓存
      // 缓存目录
      cacheLocation: path.resolve(
        __dirname,
        "../node_modules/.cache/.eslintcache"
      ),
      threads, // 开启多进程
    }),
   ...
  ],
  ...
};

减少代码体积

Tree Shaking

Tree Shaking是什么?

开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。这样将整个库都打包进来,体积就太大了。Tree Shaking 就是移除 JavaScript 中的没有使用上的代码。
Webpack 已经默认开启了这个功能,无需其他配置。

Babel

Babe是什么?

Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。可以将这些辅助代码作为一个独立模块,来避免重复引入。
@babel/plugin-transform-runtime: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime 并且使所有辅助代码从这里引用。

用法

module.exports = {
  module: {
    rules: [
      {
        oneOf: [
         ...
          {
            test: /\.js$/,
            // exclude: /node_modules/, // 排除node_modules代码不编译
            include: path.resolve(__dirname, "../src"), // 也可以用包含
            use: [
              {
                loader: "thread-loader", // 开启多进程
                options: {
                  workers: threads, // 数量
                },
              },
              {
                loader: "babel-loader",
                options: {
                  cacheDirectory: true, // 开启babel编译缓存
                  cacheCompression: false, // 缓存文件不要压缩
                  plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
                },
              },
            ],
          },
        ],
      },
    ],
  },

Image Minimizer

Image Minimizer是什么?

开发如果项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。
我们可以对图片进行压缩,减少图片体积。image-minimizer-webpack-plugin是用来压缩图片的插件

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

用法

const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");

module.exports = {
  ...
  plugins: [
  optimization: {
    minimizer: [
      // 压缩图片
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: [
              ["gifsicle", { interlaced: true }],
              ["jpegtran", { progressive: true }],
              ["optipng", { optimizationLevel: 5 }],
              [
                "svgo",
                {
                  plugins: [
                    "preset-default",
                    "prefixIds",
                    {
                      name: "sortAttrs",
                      params: {
                        xmlnsOrder: "alphabetical",
                      },
                    },
                  ],
                },
              ],
            ],
          },
        },
      }),
    ],
  },
 
};

优化代码运行性能

Code Split

Code Split是什么?

如果打包代码时会将所有 js 文件打包到一个文件中,体积会太大,应该实现当前渲染某一页就加载某一页的代码,所以需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。

用法

1.多个入口文件
//可以配置多个入口文件
module.exports = {
  
  entry: {
    main: "./src/main.js",
    app: "./src/app.js",
  },
}

配置多个入口文件,打包后会生成多个js文件(配置多少个入口,就会生成多少个js文件)

2. 提取重复代码

如果多入口文件中都引用了同一份代码,会导致代码重复,体积更大。可以提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它就好。

3. 按需加载,动态导入

想要实现按需加载,动态导入模块。

//点击事件发生时才导入
document.getElementById("btn").onclick = function () {
  // 动态导入 --> 实现按需加载
  // 即使只被引用了一次,也会代码分割
  import("./math.js").then(({ sum }) => {
    alert(sum(1, 2, 3, 4, 5));
  });
};
4. 单入口

开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。可以这样配置:

module.exports = {
  ...
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
    }
  }
}

Preload / Prefetch

Preload / Prefetch是什么?

当按需加载时加载一个很大的资源时会发生卡顿,可以在浏览器空闲时间,加载后续需要使用的资源可以用上 Preload 或 Prefetch 技术

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

它们共同点是:

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

它们区别:

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

总结:

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

    用法

    const PreloadWebpackPlugin = require("@vue/preload-webpack-plugin");
    module.exports={
       plugins: [
        new PreloadWebpackPlugin({
        rel: "preload", // preload兼容性更好
        as: "script",
        // rel: 'prefetch' // prefetch兼容性更差
       }),
         ]
    }
    


文章作者: John Doe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 John Doe !
评论
  目录