【笔记类】构建工具

230 阅读6分钟

一、构建工具解决了什么问题

构建工具(如Webpack)的作用是帮助开发者自动化构建和打包应用程序的过程。它们解决了许多与前端开发相关的问题,包括以下几个方面:

  1. 模块化管理:构建工具允许开发者使用模块化的方式组织和管理代码。通过模块化,开发者可以将代码划分为多个小模块,提高代码的可维护性和重用性。

  2. 依赖管理:在前端开发中,通常会使用许多第三方库和框架。构建工具可以帮助自动处理这些依赖关系,确保它们正确地被引入和使用。

  3. 文件打包和压缩:构建工具可以将多个源代码文件打包为单个或多个输出文件。这样可以减少页面加载的请求数量,提高应用程序的性能。此外,构建工具还可以对这些输出文件进行压缩,减小文件大小,进一步提升加载速度。

  4. 代码转换和优化:构建工具可以对源代码进行转换和优化,以提高应用程序的性能和兼容性。例如,它可以将使用最新 JavaScript 特性编写的代码转换为支持更旧浏览器的版本。

  5. 开发环境支持:构建工具通常提供了开发环境的支持,例如自动刷新浏览器、热模块替换(Hot Module Replacement)等功能,使开发者可以更高效地进行开发和调试。

总而言之,构建工具简化了前端开发过程中的许多任务,提供了自动化的工作流程,使开发者能够更专注于业务逻辑的实现,同时提高了开发效率和应用程序的性能。

二、webpack 基础配置

const path = require('path')
console.log(path.default);
// 默认创建一个空的html文件,并引入打包后的文件,加入参数复制模版
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 取代style-loader,提取js中的css为单独文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 使用 cssnano 优化和压缩 CSS
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

// 设置nodejs的环境变量 NODE_ENV: production development none
// 也可以通过mode或在package.json里设置,优先级: mode > 此处 > package.json配置
process.env.NODE_ENV = 'development'

module.exports = {
  // 模式 production development
  // mode: 'production',

  // 单页面应用使用单入口
  entry: './src/index.ts',
  // entry: ['./src/index.ts','./src/test.js'] // 数组形式的也是单入口,会合并打包成一个文件,

  // 每个入口会单独打包成一个js文件,配合 HtmlWebpackPlugin 打包多页面应用
  // entry: {
  //   main: './src/index.ts',
  //   test: './src/test.js'
  // },

  // 输出
  output: {
    filename: 'js/[name]-[contenthash].js', // 输出的文件名称
    path: path.resolve(__dirname, 'dist'), // 输出的路径,必须是绝对路径
    publicPath: 'auto', // 指定在浏览器中引用打包后的资源时的公共路径
    clean: true // 在生成文件之前清空 output 目录
  },

  // loader,在构建过程中对源代码进行各种转换和处理
  module: {
    rules: [{
        // 匹配的文件
        test: /\.css$/,
        // 使用的具体loader,从下至上执行
        use: [
          // 创建style标签,将js中的样式资源插入到head标签中生效
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          // 将css文件以字符串的形式变成commonjs模块加载到js中
          'css-loader',
          // css兼容性插件
          'postcss-loader'
        ]
      }, {
        test: /\.less/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader',
          // 需要下载less-loader和less
          'less-loader',
          'postcss-loader'
        ]
      },
      // {
      //   test: /\.(jpg|png|gif)$/,
      //   // 需要下载url-loader和file-loader(webpack5不用这种方式了)
      //   loader: 'url-loader',
      //   options: {
      //     // 图片体积 小于 8kb,就会被base64编码
      //     // 优点:减少请求数量,减轻服务器压力
      //     // 缺点:本地解码可能会导致加载速度变慢
      //     limit: 8 * 1024,
      //     // webpack5需要加这行,解决:url-loader 的es6模块化,使用commonjs解析
      //     esModule: false
      //   },
      //   // v5需要加这行
      //   type: 'javascript/auto'
      // },
      {
        // 问题:处理不了html中的img标签,需使用html-loader
        test: /\.(jpg|png|gif)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          }
        },
        generator: {
          filename: 'img/[name].[contenthash][ext]'
        }
      },
      {
        test: /\.html/,
        loader: 'html-loader'
      },
      {
        /**
          js兼容性处理: 需安装babel-loader @babel/core @babel/preset-env
            1、基本js兼容性处理 --> @babel/preset-env
              问题:只能转换语法,不能转换api
            2、全部js兼容性处理 --> @babel/polyfill
              问题:引入了全部兼容性代码,体积太大
            3、需要做兼容性处理的就做:按需加载 --> core-js
              注意:代码也会变大不少,但比全部兼容小很多
         */
        test: /\.(js|ts)$/,
        exclude: /node_modules/,
        use: [
          /**
           * 开启多进程打包
           * 进程启动时间大概为600ms,进程通信也有开销
           * 只有工作消耗时间比较长,才需要多进程打包
           */
          // 'thread-loader',
          {
            loader: 'babel-loader',
            options: {
              // 预设: 指示babel做怎么样的兼容性处理
              presets: [
                ['@babel/preset-env',
                  {
                    // 按需加载
                    useBuiltIns: 'usage',
                    // 指定core-js版本
                    corejs: {
                      version: 3
                    },
                    // 指定兼容性做到哪个版本浏览器
                    targets: {
                      chrome: '60',
                      firefox: '60',
                      ie: '9',
                      edge: '17'
                    }
                  }
                ]
              ],
              // 开启babel缓存,第二次构建时,会读取之前的缓存
              cacheDirectory: true
            }
          }
        ]
      }
    ]
  },

  // 插件
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name]-[contenthash].css'
    }),
  ],

  // 在生产环境开启压缩
  optimization: {
    // 在开发环境也启用压缩
    // minimize: true,
    minimizer: [
      // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
      `...`,
      new CssMinimizerPlugin(),
    ],
    /**
     * 1、将node_modules中代码单独打包成一个名为vendors的js文件
     * 2、自动分析多入口chunk中,有没有公共的文件。如果有只会打包一次。
     */
    // splitChunks: {
    //   chunks: 'all'
    // },
    // runtimeChunk: 'single'
  },

  // 开发服务器,需安装 webpack-dev-server
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
      publicPath: '/',
    },
    // 启用gzip压缩
    compress: true,
    port: 3000,
    // 监控页面变化,自动刷新浏览器
    watchFiles: ['./src/index.html'],
    // 自动打开浏览器
    open: true
  },

  // 控制是否生成,以及如何生成 source map。
  // source-map:一种提供源代码到构建后代码映射技术(如果侯构建后代码出错,通过映射可以追踪源代码错误)
  devtool: 'source-map',

  // 防止将某些 import 的包(package)打包到 bundle 中
  externals: {
    // 将jquery模块视为外部依赖,并期望在运行时通过全局变量jQuery来引入它。
    jquery: 'jQuery'
  }
}

2.1、通过copy-webpack-plugin剪切文件

public文件夹通常用于放一些在项目里不使用的公共资源

npm install copy-webpack-plugin --save-dev
const CopyWebpackPlugin = require("copy-webpack-plugin");

plugins: [
  new CopyWebpackPlugin({
    patterns: [
      { from: path.reslove(__dirname, '../public'), to: '' },  // 将 public 目录中的文件复制到输出目录的根目录
    ],
  }),
],

2.2、通过@svgr/webpack将svg转成组件

{
  test: /\.svg$/,
  use: [
    {
      loader: '@svgr/webpack',
      options: {
        svgoConfig: {
          plugins: [
            {
              name: 'preset-default',
              params: {
                overrides: {
                  removeViewBox: false, // 不主动清除ViewBox
                },
              },
            },
          ],
        },
      },
    },
    'url-loader', // 支持url的方式
  ],
},
// .d.ts配置
declare module '*.svg' {
	import type * as React from 'react';

	export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;

	const src: string;
	export default src;
}

三、vite 的使用

3.1、简介

vite 是下一代的打包构建工具,在开发环境通过 esbuild 预构建依赖,并以原生 esm 的方式提供源码, 可以达到快速的启动与更新;为了在生产环境中获得最佳的加载性能,仍需要打包,将代码进行 tree-shaking、懒加载和 chunk 分割等。

其中 esbuild 依赖预构建主要做了两件事:
1、将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块。(兼容性)
2、将具有许多内部模块的 ESM 依赖项转换为单个模块。(性能)

3.2、获取语法提示

// 方式一
import { defineConfig } from "vite";

export default defineConfig({
  optimizeDeps,
});

// 方式二:jsdoc注释
/** @type import('vite').UserConfigExport */
const viteConfig = {}

3.3、开发环境与生产环境配置区分

import { defineConfig } from "vite";

const envResolver = {
  build: () => {
    const prodConfig = {
      ...require("./vite.base.config").default,
      ...require("./vite.prod.config").default,
    };

    console.log("生产环境配置--->", prodConfig);

    return prodConfig;
  },
  serve: () => {
    const serveConfig = {
      ...require("./vite.base.config").default,
      ...require("./vite.dev.config").default,
    };

    console.log("开发环境配置--->", serveConfig);

    return serveConfig;
  },
};

export default defineConfig(({ command }) => {
  return envResolver[command]();
});

3.4、使用环境变量

创建 .env 文件,并写入环境变量

MY_CONST_A = 'A'

MY_CONST_B = 'B'

在 vite.config.js 中使用:

export default defineConfig(({ command, mode }) => {

  const env = loadEnv(mode, process.cwd(), 'MY_')

  console.log(env); // { MY_CONST_A: 'A', MY_CONST_B: 'B' }

  return envResolver[command]();
});

在 src 目录下的源代码中使用:

// vite.base.config.js
import { defineConfig } from "vite";

export default defineConfig({
  // 这里默认只支持 VITE_ 开头的环境变量,所以需要手动添加自己的前缀
  envPrefix: ["VITE_", "MY_"],
  
  // 如果你想暴露一个不含前缀的变量,可以使用 define 选项:
  define: {
    'import.meta.env.ENV_VARIABLE': JSON.stringify(process.env.ENV_VARIABLE)
  },
});
// 在源代码中使用如下语句使用
console.log(import.meta.env);

3.5、配置css

import { defineConfig } from "vite";

export default defineConfig({
  css: {
    // 对css模块化的默认行为进行覆盖,配置会交给 postcss-modules 进行处理
    modules: {
      hashPrefix: "cool",
    },
    // 对预处理器进行配置
    preprocessorOptions: {
      less: {
        globalVars: {
          themeColor: "tomato",
        },
      },
    },
    // 开启css的sourceMap
    devSourcemap: true,
    // postcss 配置(也可以通过写 postcss.config.js 来配置),可以在 .browserslistrc 写入需要支持的浏览器
    postcss: {
      plugins: [require("postcss-preset-env")],
    },
  },
});

3.6、配置别名

3.6.1、手动配置

import { defineConfig } from "vite";
const path = require("path");

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
      "@assets": path.resolve(__dirname, "./src/assets"),
    },
  },
});

若编辑器无法自动提示路径,在 jsconfig.json 中写入:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@assets/*": ["src/assets/*"]
    }
  }
}

若 vscode 无法实现快捷跳转,安装扩展别名路径跳转,并在 settings.json 中写入自己的配置:

"alias-skip.mappings": {
  "@assets": "/src/assets",
},

有可能安了扩展也没得用,这点不如 WebStorm 啊!

3.6.2、使用 vite-aliases 插件配置

若出现报错:Failed to resolve entry for package "vite-aliases". The package may have incorrect main/module/exports specified in its package.json: No known conditions for "." specifier in "vite-aliases" package [plugin externalize-deps]。请将 vite-aliases 降级至 0.9.2 版本。

注:若想获得编辑器提示,还是需要配置 jsconfig.json。

3.6.3、手写简易版 vite-aliases 插件

读取文件的时候务必使用绝对路径,若使用相对路径,node会将用process.cwd()和相对路径进行拼接,若执行node命令的目录不对,将会读取失败

const path = require("path");
const fs = require("fs");

module.exports = () => {
  return {
    // Vite Specific Hooks
    // config() 作用:在 Vite config 被解析之前调整配置文件
    config() {
      // 拿到src目录下的文件(夹)列表
      const res = fs.readdirSync(path.resolve(process.cwd(), "./src"));

      // 过滤出里面的文件夹
      const dir = res.filter((item) => {
        return fs
          .statSync(path.resolve(process.cwd(), "./src", item))
          .isDirectory();
      });

      // 获取 alias 配置对象
      const getAliasObj = () => {
        const aliasObj = {};

        dir.forEach((item) => {
          aliasObj[`@${item}`] = path.resolve(process.cwd(), "./src", item);
        });

        return {
          "@": path.resolve(process.cwd(), "./src"), // 这里手动将 @ 映射为 src 目录
          ...aliasObj,
        };
      };

      return {
        resolve: {
          alias: getAliasObj(),
        },
      };
    },
  };
};

3.7、配置mock

使用诸如 vite-plugin-mock-server 之类的插件,照着官网整就完事儿了。

下面我们手搓一个超简易版(不含hrm、这玩意有点复杂)的mock插件。

const fs = require("fs");
const path = require("path");

module.exports = () => {
  return {
    // 配置开发服务器的钩子
    configureServer(server) {
      // 获取 mock 文件夹下的 mockApis
      const getMockApis = () => {
        const files = fs.readdirSync(path.resolve(process.cwd(), "./mock"));

        const mockApis = [];

        files.forEach((item) => {
          mockApis.push(
            ...require(path.resolve(process.cwd(), `./mock/${item}`))
          );
        });

        return mockApis;
      };

      // 对 mockApis 进行去重
      const unique = (mockApis) => {
        const map = new Map();

        mockApis.filter((item) => {
          return (
            !map.has(`${item.pattern} ${item.method}`) &&
            map.set(`${item.pattern} ${item.method}`, item.handle)
          );
        });

        return map;
      };

      // 拿到去重后的 mockApis
      const mockApisMap = unique(getMockApis());

      // 配置中间件,请求都会打到这里
      server.middlewares.use((req, res, next) => {
        if (mockApisMap.has(`${req.url} ${req.method}`)) {
          mockApisMap.get(`${req.url} ${req.method}`)(req, res);
        } else {
          next();
        }
      });
    },
  };
};

mock文件如下:

module.exports = [
  {
    pattern: "/api/get",
    method: "GET",
    handle: (req, res) => {
      const data = {
        name: "Jay",
        age: 18,
      };
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify(data));
    },
  },
  {
    pattern: "/api/post",
    method: "POST",
    handle: (req, res) => {
      res.end("hello");
    },
  },
];

3.8、开发服务器跨域配置

import { defineConfig } from "vite";

export default defineConfig({
  server: {
    proxy: {
      "/api": {
        target: "https://www.baidu.com", // 将请求到开发服务器的且以/api开头的请求,由开发服务器转发到https://www.baidu.com,而服务器之间不存在跨域
        rewrite: (path) => path.replace("/api", ""), // 替换路径中的/api,真实请求地址为:https://www.baidu.com
        changeOrigin: true, // 修改请求头中的host为target(注:并非修改origin字段)
      },
    },
  },
});

js文件如下:

// 浏览器自动拼接,实际请求为:http://127.0.0.1:5173/api
fetch('/api').then(data => {
  console.log('data', data)
})