Webpack 实战从入门到大师

Webpack 实战从入门到大师 大纲二

13.1 Webpack 4 到 Webpack 5 的迁移

  • 主要变化概述

    • Node.js 最低版本要求提高到 10.13.0
    • 移除了废弃的功能和 API
    • 引入了持久化缓存、资源模块等新特性
    • 改进了 Tree Shaking 和代码生成
    • 更新了默认配置和插件系统
  • 配置文件变更

    javascript 复制代码
    // Webpack 4 配置
    module.exports = {
      mode: 'production',
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[chunkhash].js'
      },
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    
    // Webpack 5 配置
    module.exports = {
      mode: 'production',
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[contenthash].js',
        clean: true // 替代 CleanWebpackPlugin
      },
      cache: {
        type: 'filesystem' // 新增持久化缓存
      },
      optimization: {
        moduleIds: 'deterministic', // 优化长期缓存
        splitChunks: {
          chunks: 'all'
        }
      }
    };
  • 实例应用:在我们的项目中,从 Webpack 4 迁移到 Webpack 5 后,通过启用持久化缓存,构建时间减少了 60%,同时通过 moduleIds: 'deterministic' 配置,优化了长期缓存,提高了生产环境的加载性能。

13.2 资源模块迁移

  • 从 file-loader/url-loader 迁移到资源模块

    javascript 复制代码
    // Webpack 4 配置
    module.exports = {
    module: {
      rules: [
        {
          test: /\.(png|jpg|gif)$/i,
          use: [
            {
              loader: 'url-loader',
              options: {
                limit: 8192,
                name: 'images/[name].[hash:8].[ext]'
              }
            }
          ]
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/,
          use: [
            {
              loader: 'file-loader',
              options: {
                name: 'fonts/[name].[hash:8].[ext]'
              }
            }
          ]
        }
      ]
    }
    };
    
    // Webpack 5 配置
    module.exports = {
    module: {
      rules: [
        {
          test: /\.(png|jpg|gif)$/i,
          type: 'asset',
          parser: {
            dataUrlCondition: {
              maxSize: 8192 // 8kb
            }
          },
          generator: {
            filename: 'images/[name].[hash:8][ext]'
          }
        },
        {
          test: /\.(woff|woff2|eot|ttf|otf)$/,
          type: 'asset/resource',
          generator: {
            filename: 'fonts/[name].[hash:8][ext]'
          }
        }
      ]
    }
    };
  • 实例应用:在我们的项目中,将所有资源加载器迁移到 Webpack 5 的资源模块后,减少了依赖项数量,简化了配置,并且保持了相同的资源处理行为,使小图片自动内联为 Data URL,大文件输出到指定目录。

13.3 废弃 API 的替代方案

  • 移除的 API 及其替代方案

    javascript 复制代码
    // Webpack 4 中使用 NamedModulesPlugin
    plugins: [
    new webpack.NamedModulesPlugin()
    ]
    
    // Webpack 5 中使用 optimization.moduleIds
    optimization: {
    moduleIds: 'named' // 开发环境
    // 或
    moduleIds: 'deterministic' // 生产环境
    }
  • 移除的 loader 上下文 API

    javascript 复制代码
    // Webpack 4 中的自定义 loader
    module.exports = function(source) {
    // 已废弃的 API
    this.options; // 获取 webpack 配置
    
    // Webpack 5 中的替代方案
    const options = this.getOptions(); // 获取 loader 选项
    
    return source;
    };
  • 实例应用:在我们的项目中,通过系统地更新所有使用废弃 API 的代码,包括自定义 loader 和插件,确保了与 Webpack 5 的完全兼容性,同时提高了构建性能和代码质量。

13.4 插件兼容性处理

  • 常见插件更新

    javascript 复制代码
    // Webpack 4
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    
    plugins: [
    new CleanWebpackPlugin(['dist'])
    ]
    
    // Webpack 5
    // 使用内置的 output.clean 选项
    output: {
    clean: true
    }
    
    // 或者使用更新的 CleanWebpackPlugin
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    
    plugins: [
    new CleanWebpackPlugin()
    ]
  • HtmlWebpackPlugin 更新

    javascript 复制代码
    // Webpack 4
    plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true
      }
    })
    ]
    
    // Webpack 5
    plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        keepClosingSlash: true,
        removeRedundantAttributes: true,
        removeScriptTypeAttributes: true,
        removeStyleLinkTypeAttributes: true,
        useShortDoctype: true
      }
      })
    ]
  • 实例应用:在我们的项目中,通过更新所有插件到与 Webpack 5 兼容的版本,并调整相应的配置,解决了迁移过程中的兼容性问题,同时利用了新版插件提供的增强功能,如 HtmlWebpackPlugin 的改进的压缩选项。

13.5 处理 Node.js 核心模块 polyfill

  • Node.js 核心模块自动 polyfill 的移除

    javascript 复制代码
    // Webpack 4 自动 polyfill Node.js 核心模块
    // 无需额外配置
    
    // Webpack 5 需要手动处理
    // webpack.config.js
    module.exports = {
    resolve: {
      fallback: {
        "path": require.resolve("path-browserify"),
        "stream": require.resolve("stream-browserify"),
        "crypto": require.resolve("crypto-browserify"),
        "buffer": require.resolve("buffer/"),
        "util": require.resolve("util/"),
        // 其他需要的模块...
      }
    },
    plugins: [
      // 为全局变量提供 polyfill
      new webpack.ProvidePlugin({
        Buffer: ['buffer', 'Buffer'],
        process: 'process/browser',
      }),
    ]
    };
  • 安装必要的依赖

    bash 复制代码
    # 安装需要的 polyfill
    npm install --save-dev path-browserify stream-browserify crypto-browserify buffer util process
  • 实例应用:在我们的项目中,通过分析构建日志识别出依赖 Node.js 核心模块的第三方库,然后有选择地添加必要的 polyfill,而不是全部引入,减小了最终的打包体积,同时保持了功能完整性。

13.6 长期缓存优化

  • 优化缓存配置

    javascript 复制代码
    // Webpack 5 优化缓存配置
    module.exports = {
    output: {
      filename: '[name].[contenthash:8].js',
      chunkFilename: '[name].[contenthash:8].chunk.js',
      assetModuleFilename: 'assets/[name].[hash:8][ext]',
    },
    optimization: {
      moduleIds: 'deterministic',
      chunkIds: 'deterministic',
      runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
          vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
            chunks: 'all',
          },
        },
      },
    },
    };
  • 实例应用:在我们的项目中,通过配置 moduleIds 和 chunkIds 为 'deterministic',确保了即使添加或删除模块,现有模块的 ID 也不会变化,大幅提高了缓存命中率,使重复访问的用户加载时间减少了 70%。

13.7 渐进式迁移策略

  • 分阶段迁移计划

    1. 准备阶段

      • 更新 Node.js 到 v12 或更高版本
      • 解决 Webpack 4 中的废弃警告
      • 创建当前构建的基准性能指标
    2. 基础迁移

      • 更新 Webpack 和相关依赖
      bash 复制代码
      npm install webpack@5 webpack-cli@4 webpack-dev-server@4 --save-dev
      • 调整最小配置使构建成功运行
    3. 功能迁移

      • 迁移资源加载器到资源模块
      • 处理 Node.js 核心模块 polyfill
      • 更新插件配置
    4. 优化阶段

      • 启用持久化缓存
      • 优化长期缓存配置
      • 利用 Webpack 5 新特性
  • 兼容性处理

    javascript 复制代码
    // 创建兼容性配置文件
    // webpack.compat.js
    module.exports = {
      // 根据 Webpack 版本应用不同配置
      module: {
        rules: [
          {
            test: /\.(png|jpg|gif)$/i,
            oneOf: [
              {
                // Webpack 5 配置
                type: 'asset',
                parser: {
                  dataUrlCondition: {
                    maxSize: 8192
                  }
                },
                // Webpack 4 回退配置
                use: [
                  {
                    loader: 'url-loader',
              options: {
                      limit: 8192,
                      name: 'images/[name].[hash:8].[ext]',
                      fallback: 'file-loader'
                    }
                  }
                ]
              }
            ]
          }
        ]
      }
    };
  • 实例应用:在我们的大型项目中,采用了渐进式迁移策略,首先在非关键模块上试验 Webpack 5,然后逐步扩展到整个项目,最后统一配置和优化,整个过程平稳过渡,没有影响正常的开发和发布流程。

13.8 迁移常见问题与解决方案

  • 常见错误与解决方案

    1. 模块解析错误

      javascript 复制代码
      Error: Can't resolve 'fs' in '...'

      解决方案:

    javascript 复制代码
      resolve: {
         fallback: {
           "fs": false,
           "path": require.resolve("path-browserify")
         }
       }
    1. 插件兼容性问题

      ini 复制代码
      Error: [plugin] does not contain a constructor

      解决方案:更新插件到兼容 Webpack 5 的版本

      bash 复制代码
      npm install [plugin]@latest --save-dev
    2. 缓存相关问题

      csharp 复制代码
      Cache is corrupted

      解决方案:清除缓存并重新构建

      bash 复制代码
      rm -rf node_modules/.cache
      webpack --cache
  • 性能问题诊断

    javascript 复制代码
    // 添加性能分析
    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
    const smp = new SpeedMeasurePlugin();
    
    module.exports = smp.wrap({
    // 你的 webpack 配置
    });
  • 实例应用:在我们的迁移过程中,遇到了多个第三方库依赖 Node.js 核心模块的问题,通过系统地分析构建日志,为每个必要的模块添加 polyfill,同时对不需要在浏览器中运行的模块设置为 false,成功解决了兼容性问题。

13.9 Webpack 5 新特性的最佳实践

  • 持久化缓存最佳实践

    javascript 复制代码
    // 优化持久化缓存配置
    cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename], // 当配置文件变化时使缓存失效
    },
    name: process.env.NODE_ENV === 'production' ? 'production-cache' : 'development-cache',
    version: '1.0',
    cacheDirectory: path.resolve(__dirname, '.temp_cache'),
    compression: 'gzip',
    }
  • 资源模块最佳实践

    javascript 复制代码
    // 针对不同资源类型的优化配置
    module: {
    rules: [
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1021 // 4kb
          }
        },
        generator: {
          filename: 'images/[name].[contenthash:8][ext]'
        }
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[contenthash:8][ext]'
        }
      },
      {
        test: /\.txt$/i,
        type: 'asset/source'
      },
      {
        test: /\.svg$/i,
        oneOf: [
          {
            // SVG 作为 React 组件导入
            resourceQuery: /react/,
            use: ['@svgr/webpack']
          },
          {
            // 普通 SVG 文件
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 4 * 1021 // 4kb
              }
            },
            generator: {
              filename: 'images/[name].[contenthash:8][ext]'
            }
          }
        ]
      }
    ]
    }
  • 实例应用:在我们的项目中,通过精细配置资源模块,实现了更智能的资源处理策略,如 SVG 文件根据查询参数决定是作为 React 组件导入还是作为普通图片处理,大幅提高了开发体验和应用性能。

13.10 未来升级的准备

  • 保持代码库现代化

    • 定期更新依赖
    bash 复制代码
    # 检查过时的依赖
    npm outdated
    
    # 更新依赖
    npm update
    
    # 使用 npm-check-updates 进行主版本更新
    npx npm-check-updates -u
    npm install
    • 遵循最佳实践

      javascript 复制代码

    // 使用 ES 模块语法 // 而不是 CommonJS import { something } from 'some-package'; // 而不是 const something = require('some-package');

    // 使用动态导入进行代码分割 import('./module').then(module => { // 使用模块 });

    复制代码
  • 监控 Webpack 生态系统

    • 关注 Webpack 官方博客和 GitHub 仓库
    • 参与 Webpack 社区讨论
    • 尝试 Webpack 的实验性功能
  • 实例应用:在我们的团队中,建立了依赖更新和代码现代化的定期审查机制,每季度评估一次技术栈状态,确保代码库保持现代化,为未来的升级做好准备,减少技术债务。

19. Webpack 与微前端架构

14.1 微前端架构概述

  • 什么是微前端

    • 微前端是一种架构风格,将前端应用分解成独立的、可自治的小型应用
    • 每个微前端应用可以由不同团队独立开发、测试和部署
    • 最终组合成一个统一的应用呈现给用户
    scss 复制代码
    ┌─────────────────────────────────────────────────────┐
    │                   Shell Application                  │
    │  ┌───────────────┐  ┌───────────────┐  ┌──────────┐ │
    │  │  MicroApp 1   │  │  MicroApp 2   │  │MicroApp 3│ │
    │  │ (React Team)  │  │  (Vue Team)   │  │(Angular) │ │
    │  └───────────────┘  └───────────────┘  └──────────┘ │
    └─────────────────────────────────────────────────────┘
  • 微前端的核心原则

    • 技术栈无关:每个微前端可以使用不同的框架和库
    • 独立开发:团队可以独立开发自己的微前端,无需关心其他部分
    • 独立部署:每个微前端可以独立部署,不影响整体应用
    • 独立运行时:微前端在浏览器中独立运行,有自己的上下文
    • 统一体验:对用户来说,整个应用应该是一个无缝的整体
  • 微前端的实现方式

    • 基于 iframe 的隔离
    • 基于 Web Components 的自定义元素
    • 基于 JavaScript 的运行时集成
    • 基于 Webpack Module Federation 的构建时集成
  • 实例应用:在我们的企业级应用中,采用微前端架构将原本庞大的单体应用拆分为多个独立的微应用,不同团队负责不同的业务模块,大大提高了开发效率和部署灵活性。

14.2 Webpack Module Federation 基础

  • Module Federation 概念

    • Webpack 5 引入的新特性,允许多个独立构建的应用共享模块
    • 实现了真正的运行时代码共享,而不仅仅是构建时共享
    • 支持异步加载远程模块,实现按需加载
    javascript 复制代码
    // webpack.config.js
    const { ModuleFederationPlugin } = require('webpack').container;
    
    module.exports = {
      // ...其他配置
      plugins: [
        new ModuleFederationPlugin({
          name: 'app1',
          filename: 'remoteEntry.js',
          exposes: {
            './Button': './src/components/Button'
          },
          shared: ['react', 'react-dom']
        })
      ]
    };
  • 核心概念解析

    • Host:消费远程模块的应用
    • Remote:暴露模块的应用
    • Shared:在应用间共享的依赖
    • Bidirectional Hosts:应用既可以是 Host 也可以是 Remote
  • 基本配置参数

    • name:微应用的唯一标识
    • filename:生成的远程入口文件名
    • remotes:声明要使用的远程应用
    • exposes:声明要暴露的模块
    • shared:声明要共享的依赖
  • 实例应用:在我们的项目中,使用 Module Federation 实现了产品详情页和购物车功能的分离,两个团队可以独立开发和部署,同时共享公共组件和状态管理逻辑。

14.3 构建微前端应用

  • 主应用(Shell)配置

    javascript 复制代码
    // shell/webpack.config.js
    const { ModuleFederationPlugin } = require('webpack').container;
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    
    module.exports = {
    entry: './src/index',
    mode: 'development',
    devServer: {
      port: 3000,
      hot: true,
    },
    output: {
      publicPath: 'http://localhost:3000/',
    },
    module: {
      rules: [
        {
          test: /\.jsx?$/,
          loader: 'babel-loader',
          exclude: /node_modules/,
        },
      ],
    },
    plugins: [
      new ModuleFederationPlugin({
        name: 'shell',
        filename: 'remoteEntry.js',
        remotes: {
          products: 'products@http://localhost:3001/remoteEntry.js',
          cart: 'cart@http://localhost:3002/remoteEntry.js',
        },
        shared: {
          react: { singleton: true, eager: true },
          'react-dom': { singleton: true, eager: true },
        },
      }),
      new HtmlWebpackPlugin({
        template: './public/index.html',
      }),
    ],
    };
  • 微应用配置

    javascript 复制代码
    // products/webpack.config.js
    const { ModuleFederationPlugin } = require('webpack').container;
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    
    module.exports = {
      entry: './src/index',
      mode: 'development',
      devServer: {
        port: 3001,
        hot: true,
      },
      output: {
        publicPath: 'http://localhost:3001/',
      },
      module: {
        rules: [
          {
            test: /\.jsx?$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
          },
        ],
      },
      plugins: [
        new ModuleFederationPlugin({
          name: 'products',
          filename: 'remoteEntry.js',
          exposes: {
            './ProductList': './src/components/ProductList',
            './ProductDetail': './src/components/ProductDetail',
          },
          shared: {
            react: { singleton: true },
            'react-dom': { singleton: true },
          },
        }),
        new HtmlWebpackPlugin({
          template: './public/index.html',
        }),
      ],
    };
  • 在主应用中使用微应用组件

    javascript 复制代码
    // shell/src/App.js
    import React, { lazy, Suspense } from 'react';
    
    // 动态导入远程组件
    const ProductList = lazy(() => import('products/ProductList'));
    const Cart = lazy(() => import('cart/Cart'));
    
    const App = () => {
      return (
        <div>
          <h1>电商平台</h1>
          <Suspense fallback={<div>加载产品列表...</div>}>
            <ProductList />
          </Suspense>
          <Suspense fallback={<div>加载购物车...</div>}>
            <Cart />
          </Suspense>
        </div>
      );
    };
    
    export default App;
  • 实例应用:在我们的电商平台中,将产品展示、购物车、用户中心和订单管理拆分为四个独立的微应用,每个微应用由不同团队负责,通过 Module Federation 实现了无缝集成,用户体验一致,同时开发效率大幅提升。

14.4 共享依赖管理

  • 共享依赖配置

    javascript 复制代码
    // 共享依赖配置
    shared: {
      // 简单共享
      'lodash': {},
      
      // 指定版本范围
      'moment': { requiredVersion: '^2.29.1' },
      
      // 单例模式(确保只加载一个实例)
      'react': { singleton: true, requiredVersion: '^17.0.2' },
      
      // 预加载(不等待异步加载)
      'react-dom': { singleton: true, eager: true, requiredVersion: '^17.0.2' }
    }
  • 版本控制策略

    • 使用 requiredVersion 指定版本范围
    • 使用 singleton: true 确保只加载一个实例
    • 使用 strictVersion: true 在版本不匹配时抛出错误
    • 使用 eager: true 预加载共享模块
  • 处理版本冲突

    javascript 复制代码
    // 处理可能的版本冲突
    new ModuleFederationPlugin({
      // ...其他配置
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^17.0.0',
          strictVersion: false, // 允许不同的次要版本
        }
      }
    })
  • 实例应用:在我们的项目中,通过精细配置共享依赖,确保了所有微应用使用相同版本的 React 和状态管理库,避免了多个实例导致的状态不一致和内存占用问题,同时允许工具库使用兼容的不同版本。

14.5 微前端通信与状态共享

  • 基于共享依赖的状态管理

    javascript 复制代码
    // store/index.js (共享模块)
    import { createStore } from 'redux';
    
    const initialState = {
      cart: [],
      user: null
    };
    
    const reducer = (state = initialState, action) => {
      switch (action.type) {
        case 'ADD_TO_CART':
          return {
            ...state,
            cart: [...state.cart, action.payload]
          };
        // 其他 action 处理...
        default:
          return state;
      }
    };
    
    export const store = createStore(reducer);
    javascript 复制代码
    // 在微应用中使用共享 store
    // products/src/components/ProductDetail.js
    import React from 'react';
    import { store } from 'shell/store';
    
    const ProductDetail = ({ product }) => {
      const addToCart = () => {
        store.dispatch({
          type: 'ADD_TO_CART',
          payload: product
        });
      };
      
      return (
        <div>
          <h2>{product.name}</h2>
          <p>{product.price}</p>
          <button onClick={addToCart}>加入购物车</button>
        </div>
      );
    };
    
    export default ProductDetail;
  • 基于事件的通信

    javascript 复制代码
    // 创建一个事件总线
    // shell/src/eventBus.js
    class EventBus {
      constructor() {
        this.events = {};
      }
      
      on(event, callback) {
        if (!this.events[event]) {
          this.events[event] = [];
        }
        this.events[event].push(callback);
      }
      
      emit(event, data) {
        if (this.events[event]) {
          this.events[event].forEach(callback => callback(data));
        }
      }
    }
    
    export default new EventBus();
    javascript 复制代码
    // 在微应用中使用事件总线
    import eventBus from 'shell/eventBus';
    
    // 发送事件
    eventBus.emit('productSelected', { id: 123, name: '商品名称' });
    
    // 监听事件
    eventBus.on('cartUpdated', (cartData) => {
      console.log('购物车已更新:', cartData);
    });
  • 实例应用:在我们的微前端项目中,采用了双重通信策略:核心业务状态通过共享 Redux store 管理,确保数据一致性;非关键交互通过事件总线实现,降低耦合度。这种方式既保证了关键数据的一致性,又提供了足够的灵活性。

14.6 路由管理与导航

  • 集中式路由管理

    javascript 复制代码
    // shell/src/App.js
    import React, { lazy, Suspense } from 'react';
    import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
    
    // 动态导入微应用路由组件
    const ProductRoutes = lazy(() => import('products/Routes'));
    const CartRoutes = lazy(() => import('cart/Routes'));
    const UserRoutes = lazy(() => import('user/Routes'));
    
    const App = () => {
      return (
        <BrowserRouter>
          <div>
            <nav>
              <Link to="/">首页</Link>
              <Link to="/products">产品</Link>
              <Link to="/cart">购物车</Link>
              <Link to="/user">用户中心</Link>
            </nav>
            
            <Suspense fallback={<div>加载中...</div>}>
              <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/products/*" element={<ProductRoutes />} />
                <Route path="/cart/*" element={<CartRoutes />} />
                <Route path="/user/*" element={<UserRoutes />} />
              </Routes>
            </Suspense>
          </div>
        </BrowserRouter>
      );
    };
  • 微应用路由配置

    javascript 复制代码
    // products/src/Routes.js
    import React from 'react';
    import { Routes, Route } from 'react-router-dom';
    import ProductList from './components/ProductList';
    import ProductDetail from './components/ProductDetail';
    
    const ProductRoutes = () => {
      return (
        <Routes>
          <Route path="/" element={<ProductList />} />
          <Route path="/:id" element={<ProductDetail />} />
        </Routes>
      );
    };
    
    export default ProductRoutes;
  • 路由同步与历史记录共享

    javascript 复制代码
    // shell/src/index.js
    import { createBrowserHistory } from 'history';
    
    // 创建共享的历史对象
    export const history = createBrowserHistory();
    
    // 在微应用中使用共享历史对象
    // products/src/index.js
    import { history } from 'shell/index';
    import { Router } from 'react-router-dom';
    
    ReactDOM.render(
      <Router history={history}>
        <App />
      </Router>,
      document.getElementById('root')
    );
  • 实例应用:在我们的企业应用中,采用了基于 React Router 的嵌套路由策略,主应用负责顶层路由和布局,各微应用负责自己的子路由。通过共享历史对象,确保了导航状态的一致性,用户可以正常使用浏览器的前进后退功能。

14.7 样式隔离与主题共享

  • CSS 模块化隔离

    javascript 复制代码
    // webpack.config.js
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'style-loader',
            {
              loader: 'css-loader',
              options: {
                modules: {
                  localIdentName: '[name]__[local]--[hash:base64:5]'
                }
              }
            }
          ]
        }
      ]
    }
  • Shadow DOM 隔离

    javascript 复制代码
    // 使用 Web Components 创建隔离的样式环境
    class MicroApp extends HTMLElement {
      constructor() {
        super();
        this.attachShadow({ mode: 'open' });
      }
      
      connectedCallback() {
        this.shadowRoot.innerHTML = `
          <style>
            /* 微应用的样式,不会影响外部 */
            h1 { color: blue; }
          </style>
          <div id="micro-app-root"></div>
        `;
        
        // 在 Shadow DOM 中渲染微应用
        const root = this.shadowRoot.getElementById('micro-app-root');
        ReactDOM.render(<MicroAppComponent />, root);
      }
    }
    
    customElements.define('micro-app', MicroApp);
  • 共享主题变量

    javascript 复制代码
    // 导出主题变量
    // shell/src/theme.js
    export const theme = {
      colors: {
        primary: '#1890ff',
        secondary: '#f5222d',
        background: '#f0f2f5'
      },
      fonts: {
        base: '"Segoe UI", Roboto, "Helvetica Neue", Arial',
        sizes: {
          small: '12px',
          medium: '14px',
          large: '16px'
        }
      }
    };
    javascript 复制代码
    // 在微应用中使用主题
    // products/src/components/ProductCard.js
    import React from 'react';
    import styled from 'styled-components';
    import { theme } from 'shell/theme';
    
    const Card = styled.div`
      background-color: white;
      border-radius: 4px;
      padding: 16px;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      
      h3 {
        color: ${theme.colors.primary};
        font-family: ${theme.fonts.base};
        font-size: ${theme.fonts.sizes.large};
      }
      
      button {
        background-color: ${theme.colors.secondary};
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 4px;
        cursor: pointer;
      }
    `;
    
    const ProductCard = ({ product }) => {
      return (
        <Card>
          <h3>{product.name}</h3>
          <p>{product.description}</p>
          <button>加入购物车</button>
        </Card>
      );
    };
    
    export default ProductCard;
  • 实例应用:在我们的微前端项目中,采用了 CSS-in-JS 方案结合共享主题变量,既保证了各微应用样式的隔离性,又实现了统一的品牌视觉体验。当需要更新品牌色时,只需修改主应用中的主题变量,所有微应用自动更新样式。

14.8 部署与运维策略

  • 独立部署流程

    bash 复制代码
    # 微应用构建脚本
    # package.json
    {
      "scripts": {
        "build": "webpack --config webpack.prod.js",
        "deploy": "aws s3 sync dist/ s3://my-bucket/products/ --delete"
      }
    }
  • 版本控制与回滚

    javascript 复制代码
    // 在主应用中使用带版本的远程入口
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        products: 'products@https://cdn.example.com/products/v1.2.3/remoteEntry.js',
        cart: 'cart@https://cdn.example.com/cart/v2.0.1/remoteEntry.js'
      }
    })
  • 配置中心管理

    javascript 复制代码
    // 动态加载远程模块配置
    async function loadRemotes() {
      // 从配置中心获取微应用配置
      const response = await fetch('https://config.example.com/micro-frontends');
      const remotes = await response.json();
      
      // 动态加载远程入口
      for (const [name, url] of Object.entries(remotes)) {
        const script = document.createElement('script');
        script.src = url;
        script.async = true;
        document.head.appendChild(script);
      }
    }
  • 健康检查与监控

    javascript 复制代码
    // 微应用健康检查
    async function checkMicroAppHealth() {
      try {
        // 尝试加载远程模块
        await import('products/ProductList');
        console.log('产品微应用加载成功');
        return true;
      } catch (error) {
        console.error('产品微应用加载失败:', error);
        // 上报错误到监控系统
        reportError(error);
        // 加载备用模块或显示错误提示
        loadFallbackModule();
        return false;
      }
    }
  • 实例应用:在我们的生产环境中,每个微应用都有独立的 CI/CD 流水线,部署到单独的 CDN 路径。主应用通过配置中心动态获取最新的微应用版本,支持灰度发布和快速回滚。同时,我们实现了完善的健康检查机制,当微应用加载失败时自动切换到备用模块,确保系统的可用性。

14.9 微前端架构的最佳实践

  • 设计原则

    • 保持微应用的独立性和自治性
    • 明确定义微应用之间的边界和接口
    • 共享核心库和组件,避免重复实现
    • 统一用户体验和设计语言
    • 建立团队间的协作规范
  • 性能优化

    • 合理配置共享依赖,避免重复加载
    • 使用预加载策略提前加载可能用到的微应用
    • 优化初始加载路径,减少关键渲染路径的阻塞
    • 实施代码分割,按需加载非核心功能
    • 监控和优化微应用的加载性能
  • 安全考虑

    • 实施内容安全策略 (CSP),防止 XSS 攻击
    • 限制微应用的权限范围,实现最小权限原则
    • 审查和验证远程模块的完整性
    • 保护敏感数据,谨慎处理跨微应用的数据共享
  • 实例应用:在我们的企业级微前端项目中,制定了详细的微前端开发规范,包括模块边界定义、状态管理策略、UI 组件库共享和性能预算等。通过这些最佳实践,我们成功地将一个有 50 多名开发人员的大型团队拆分为 7 个自治团队,每个团队负责不同的业务域,大大提高了开发效率和产品迭代速度。

14.10 案例研究:从单体应用迁移到微前端

  • 迁移策略

    1. 评估和规划
      • 分析现有应用的业务域和技术栈
      • 确定微前端的边界和拆分策略
      • 设计共享依赖和通信机制
    2. 渐进式迁移
      • 从边缘功能开始,逐步迁移到微前端
      • 使用"应用壳"模式包装现有应用
      • 新功能直接开发为微前端应用
    3. 重构与优化
      • 重构共享状态管理
      • 优化构建和部署流程
      • 完善监控和错误处理
  • 迁移前后对比

    diff 复制代码
    迁移前:
    - 单一代码库,超过 30 万行代码
    - 构建时间平均 15 分钟
    - 每周发布一次
    - 团队协作困难,频繁出现代码冲突
    
    迁移后:
    - 7 个独立的微前端应用
    - 构建时间减少到平均 3 分钟
    - 各团队可以独立发布,平均每天多次发布
    - 团队并行工作,代码冲突大幅减少
  • 实例应用:我们将一个大型电商平台从单体 React 应用迁移到基于 Module Federation 的微前端架构,采用渐进式迁移策略,首先将购物车和用户中心拆分为独立微应用,然后逐步迁移产品展示、订单管理和支付功能。整个迁移过程持续了 6 个月,期间业务正常运行,没有出现重大问题。迁移完成后,开发效率提升了 60%,部署频率从每周一次增加到每天多次。

14.11 未来趋势与发展方向

  • 服务端组件与微前端

    • React Server Components 与微前端的结合
    • 服务端渲染的微前端架构
    • 混合渲染策略的应用
  • WebAssembly 与微前端

    • 使用 WebAssembly 实现高性能微前端模块
    • 跨语言微前端架构(C++, Rust 等)
    • WebAssembly 系统接口 (WASI) 的应用
  • 边缘计算与微前端

    • 在 CDN 边缘节点运行微前端逻辑
    • 边缘渲染与客户端渲染的混合架构
    • 地理位置感知的微前端部署
  • 实例应用:在我们的研发路线图中,正在探索将 React Server Components 与微前端架构结合,实现部分组件在服务端渲染,部分在客户端渲染的混合策略。同时,我们也在评估使用 WebAssembly 重写性能关键的数据处理模块,以提升复杂数据可视化场景的性能。

20. Webpack 内部原理与架构

20.1 Webpack 工作流程

  • 初始化阶段

    • 读取与合并配置参数
    • 加载 Plugin
    • 实例化 Compiler
    javascript 复制代码
    // webpack 源码简化示例
    const webpack = (options) => {
      // 1. 初始化参数
      const mergedOptions = mergeOptions(options);
      // 2. 实例化 Compiler
      const compiler = new Compiler(mergedOptions);
      // 3. 加载所有配置的插件
      if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
          if (typeof plugin === 'function') {
            plugin.call(compiler, compiler);
          } else {
            plugin.apply(compiler);
          }
        }
      }
      return compiler;
    };
  • 构建阶段

    • 从 Entry 出发,针对每个 Module 调用对应的 Loader 去翻译文件内容

    • 再找出该 Module 依赖的 Module,递归地进行编译处理

      javascript 复制代码

    // 简化的模块构建过程 function buildModule(module) { // 1. 读取文件内容 let source = fs.readFileSync(module.path, 'utf8');

    // 2. 调用对应的 loader 处理文件 const loaders = getLoaders(module.path); for (const loader of loaders.reverse()) { source = loader(source); }

    // 3. 解析模块依赖 const dependencies = parse(source);

    // 4. 递归处理依赖模块 for (const dependency of dependencies) { buildModule(dependency); } }

    复制代码
  • 生成阶段

    • 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk

    • 再把每个 Chunk 转换成一个单独的文件加入到输出列表

    • 最后根据配置确定输出的路径和文件名,写入到文件系统

      javascript 复制代码

    // 简化的生成过程 function seal() { // 1. 根据依赖关系生成 chunks const chunks = createChunks(modules);

    // 2. 优化 chunks optimizeChunks(chunks);

    // 3. 生成输出文件内容 for (const chunk of chunks) { const content = generateChunkContent(chunk); const filename = getOutputFilename(chunk); emitAsset(filename, content); } }

    复制代码
  • 实例应用:通过理解 Webpack 的工作流程,我们在项目中能够更精确地定位构建问题,例如在一次性能优化中,通过分析构建阶段的耗时,我们发现某个 loader 处理大型文件时效率低下,通过调整配置和增加缓存,将构建时间减少了 40%。

20.2 Compiler 与 Compilation

  • Compiler 对象

    • Webpack 的核心引擎,代表了完整的 Webpack 环境配置
    • 负责监听文件变化,并在发生变化时触发重新编译
    • 全局唯一,整个生命周期中存在
    javascript 复制代码
    // Compiler 钩子示例
    class Compiler extends Tapable {
      constructor() {
        super();
        // 定义各种钩子
        this.hooks = {
          entryOption: new SyncBailHook(["context", "entry"]),
          afterPlugins: new SyncHook(["compiler"]),
          run: new AsyncSeriesHook(["compiler"]),
          compile: new SyncHook(["params"]),
          // ... 更多钩子
          done: new AsyncSeriesHook(["stats"])
        };
      }
      
      run(callback) {
        // 触发 run 钩子
        this.hooks.run.callAsync(this, err => {
          // 创建 compilation
          this.compile(onCompiled);
        });
      }
      
      compile(callback) {
        // 触发 compile 钩子
        this.hooks.compile.call(params);
        // 创建 compilation 对象
        const compilation = new Compilation(this);
        // 触发 compilation 钩子
        this.hooks.compilation.call(compilation, params);
        // 执行编译
        callback(null, compilation);
      }
    }
  • Compilation 对象

    • 代表了一次资源的构建,包含了当前构建环境的所有状态
    • 每次构建都会产生一个新的 Compilation 对象
    • 负责模块的加载、封装、优化等过程
    javascript 复制代码
    // Compilation 钩子示例
    class Compilation extends Tapable {
      constructor(compiler) {
        super();
        this.compiler = compiler;
        this.hooks = {
          buildModule: new SyncHook(["module"]),
          succeedModule: new SyncHook(["module"]),
          finishModules: new AsyncSeriesHook(["modules"]),
          // ... 更多钩子
          optimizeChunks: new SyncBailHook(["chunks", "chunkGroups"])
        };
        this.modules = [];
        this.chunks = [];
        this.assets = {};
      }
      
      addModule(module) {
        // 添加模块
        this.modules.push(module);
        // 触发钩子
        this.hooks.buildModule.call(module);
        // 构建模块
        this.buildModule(module, err => {
          this.hooks.succeedModule.call(module);
        });
      }
      
      createChunks() {
        // 根据依赖关系创建 chunks
        const chunks = createChunksFromModules(this.modules);
        this.chunks = chunks;
        // 触发优化钩子
        this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups);
      }
    }
  • 实例应用:在我们的项目中,通过编写一个自定义插件,利用 Compiler 和 Compilation 提供的钩子,我们实现了在构建过程中自动生成组件文档,大大提高了开发效率和文档的实时性。

20.3 Loader 机制详解

  • Loader 本质

    • Loader 本质上是一个函数,接收源文件内容,返回转换后的内容
    • 可以是同步的,也可以是异步的
    • 可以通过返回多个值来传递给下一个 loader
    javascript 复制代码
    // 简单的 loader 示例
    module.exports = function(source) {
      // this 是由 webpack 提供的上下文
      const options = this.getOptions();
      
      // 同步 loader
      const result = source.replace(/[abc]/g, '');
      return result;
      
      // 或者异步 loader
      const callback = this.async();
      someAsyncOperation(source, (err, result) => {
        if (err) return callback(err);
        callback(null, result);
      });
    };
  • Loader 链式调用

    • 多个 loader 可以串联使用,前一个 loader 的输出作为后一个 loader 的输入
    • 执行顺序是从右到左(或从下到上)
    javascript 复制代码
    // webpack 配置中的 loader 链
    module: {
      rules: [
        {
          test: /\.js$/,
          use: [
            'babel-loader',      // 第三个执行
            'eslint-loader',     // 第二个执行
            'my-custom-loader'   // 第一个执行
          ]
        }
      ]
    }
  • Loader 上下文

    • Webpack 为 loader 提供了丰富的上下文 API
    • 可以通过 this 访问这些 API
    javascript 复制代码
    module.exports = function(source) {
      // 获取配置选项
      const options = this.getOptions();
      
      // 缓存 loader 的结果
      this.cacheable && this.cacheable();
      
      // 解析依赖
      this.resolve(this.context, 'imported-module', (err, result) => {
        // 处理解析结果
      });
      
      // 发出警告或错误
      this.emitWarning(new Error("Warning message"));
      this.emitError(new Error("Error message"));
      
      // 添加额外的文件依赖
      this.addDependency(path.resolve('path/to/file'));
      
      return source;
    };
  • 实例应用:在我们的项目中,我们开发了一个自定义 loader 用于处理国际化资源文件,它能够自动提取代码中的文本并生成翻译文件,同时在构建时将翻译内容注入回代码中,极大地简化了国际化流程。

20.4 Plugin 机制详解

  • Plugin 本质

    • Plugin 本质上是一个具有 apply 方法的 JavaScript 对象
    • apply 方法会被 Webpack compiler 调用,并且可以访问整个编译生命周期
    javascript 复制代码
    // 简单的 plugin 示例
    class MyPlugin {
      constructor(options) {
        this.options = options || {};
      }
      
      apply(compiler) {
        // 注册钩子
        compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
          // 在生成资源到 output 目录之前执行
          console.log('资源即将写入文件系统!');
          
          // 修改或添加资源
          compilation.assets['new-file.txt'] = {
            source: () => 'New file content',
            size: () => 'New file content'.length
          };
          
          callback();
        });
      }
    }
    
    module.exports = MyPlugin;
  • Tapable 与钩子系统

    • Webpack 的插件架构主要基于 Tapable 提供的钩子系统
    • 不同类型的钩子支持不同的调用方式
    javascript 复制代码
    // 钩子类型示例
    const {
      SyncHook,           // 同步钩子
      SyncBailHook,       // 同步熔断钩子
      SyncWaterfallHook,  // 同步瀑布流钩子
      SyncLoopHook,       // 同步循环钩子
      AsyncParallelHook,  // 异步并行钩子
      AsyncSeriesHook     // 异步串行钩子
    } = require('tapable');
    
    // 创建钩子
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      beforeRun: new AsyncSeriesHook(['compiler'])
    };
    
    // 注册钩子的不同方式
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      // 同步注册
      console.log('构建完成!');
    });
    
    compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
      // 异步注册
      setTimeout(() => {
        console.log('异步任务完成!');
        callback();
      }, 1000);
    });
    
    compiler.hooks.emit.tapPromise('MyPlugin', (compilation) => {
      // 返回 Promise 的方式注册
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('异步任务完成!');
          resolve();
        }, 1000);
      });
    });
  • 常用钩子及其应用场景

    • compiler.hooks.entryOption: 在 webpack 处理 entry 配置后调用
    • compiler.hooks.compile: 在创建新的 compilation 之前调用
    • compiler.hooks.make: 在 compilation 创建后执行
    • compiler.hooks.emit: 在生成资源到 output 目录之前调用
    • compiler.hooks.done: 在 compilation 完成后调用
    • compilation.hooks.buildModule: 在模块构建开始前触发
    • compilation.hooks.optimizeChunks: 在 chunk 优化阶段开始时调用
  • 实例应用:在我们的项目中,我们开发了一个性能分析插件,它利用 Webpack 的钩子系统在不同构建阶段收集时间数据,最终生成一份详细的构建性能报告,帮助我们识别构建过程中的性能瓶颈。

20.5 模块依赖解析

  • 模块解析算法

    • Webpack 使用 enhanced-resolve 库来解析模块路径
    • 支持相对路径、绝对路径和模块路径三种形式
    javascript 复制代码
    // 相对路径
    import './relative/path/to/file';
    
    // 绝对路径
    import '/absolute/path/to/file';
    
    // 模块路径
    import 'module/lib/file';
  • 解析过程

    • 对于相对路径和绝对路径,直接根据路径查找文件
    • 对于模块路径,会在 resolve.modules 指定的目录中查找
    • 按照 resolve.extensions 指定的扩展名顺序尝试解析
    javascript 复制代码
    // webpack 配置中的解析选项
    resolve: {
      // 模块查找目录
      modules: ['node_modules', path.resolve(__dirname, 'src')],
      
      // 扩展名解析顺序
      extensions: ['.js', '.json', '.jsx', '.ts', '.tsx'],
      
      // 别名设置
      alias: {
        '@': path.resolve(__dirname, 'src'),
        'utils': path.resolve(__dirname, 'src/utils')
      },
      
      // 字段解析顺序
      mainFields: ['browser', 'module', 'main']
    }
  • 解析器源码分析

    javascript 复制代码
    // 简化的解析过程
    function resolve(context, request) {
      // 1. 检查是否是相对路径或绝对路径
      if (request.startsWith('./') || request.startsWith('/')) {
        return resolveAsFile(path.join(context, request));
      }
      
      // 2. 如果是模块路径,在 node_modules 中查找
      return resolveAsModule(request, context);
    }
    
    function resolveAsFile(path) {
      // 1. 检查路径是否直接指向文件
      if (fs.existsSync(path) && fs.statSync(path).isFile()) {
        return path;
      }
      
      // 2. 尝试添加扩展名
      for (const ext of extensions) {
        const fullPath = path + ext;
        if (fs.existsSync(fullPath)) {
          return fullPath;
        }
      }
      
      // 3. 尝试作为目录解析
      return resolveAsDirectory(path);
    }
    
    function resolveAsModule(request, context) {
      // 在 node_modules 中查找模块
      for (const dir of moduleDirs) {
        const modulePath = path.join(dir, request);
        const result = resolveAsFile(modulePath);
        if (result) return result;
      }
      
      // 向上级目录查找 node_modules
      if (context.parent) {
        return resolveAsModule(request, context.parent);
      }
      
      throw new Error(`Cannot resolve module '${request}'`);
    }
  • 实例应用:在我们的大型项目中,通过深入理解 Webpack 的模块解析机制,我们优化了项目的目录结构和导入方式,合理设置了 alias 和 modules 配置,使得模块导入更加清晰,同时也提高了构建性能,减少了不必要的解析操作。

21. Webpack 生态系统与替代方案

21.1 Webpack 生态系统概览

  • 核心工具与插件

    • webpack-dev-server: 提供开发服务器和热模块替换功能

      javascript 复制代码
      // webpack.config.js
      module.exports = {
        // ...其他配置
        devServer: {
          port: 3000,
          hot: true,
          open: true
        }
      };
    • webpack-merge: 合并 Webpack 配置,便于环境区分

      javascript 复制代码
      // webpack.prod.js
      const { merge } = require('webpack-merge');
      const common = require('./webpack.common.js');
      
      module.exports = merge(common, {
        mode: 'production',
        // 生产环境特定配置
      });
    • webpack-bundle-analyzer: 可视化分析打包结果

      javascript 复制代码
      // webpack.config.js
      const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
      
      module.exports = {
        // ...其他配置
        plugins: [
          new BundleAnalyzerPlugin()
        ]
      };
  • 常用 Loader 生态

    • 样式处理: css-loader, style-loader, sass-loader, less-loader, postcss-loader
    • 文件处理: file-loader, url-loader, raw-loader (Webpack 5 中已被资源模块替代)
    • 框架支持: babel-loader, ts-loader, vue-loader, svelte-loader
    • 优化相关: thread-loader, cache-loader
  • 常用 Plugin 生态

    • 优化类: TerserPlugin, CssMinimizerPlugin, CompressionPlugin
    • 资源管理: HtmlWebpackPlugin, MiniCssExtractPlugin, CopyWebpackPlugin
    • 环境与变量: DefinePlugin, EnvironmentPlugin, DotenvPlugin
  • 实例应用:在我们的项目中,使用 webpack-merge 管理不同环境的配置,使用 webpack-bundle-analyzer 定期分析和优化包体积,使用 webpack-dev-server 提供高效的开发体验,大大提高了开发效率和产品质量。

21.2 主流构建工具对比

  • Webpack vs Rollup

    • Webpack :

      • 优势:功能全面,生态丰富,适合复杂应用
      • 劣势:配置复杂,打包体积较大
      • 适用场景:大型应用,需要代码分割和动态导入
    • Rollup :

      • 优势:打包结果更清晰,体积更小,适合库开发
      • 劣势:插件生态相对较小,动态导入支持较弱
      • 适用场景:库和工具开发,简单应用
      javascript 复制代码
      // rollup.config.js
      export default {
        input: 'src/main.js',
        output: {
          file: 'bundle.js',
          format: 'esm'
        }
      };
  • Webpack vs Parcel

    • Webpack :

      • 优势:高度可配置,生态丰富
      • 劣势:学习曲线陡峭,配置繁琐
      • 适用场景:需要精细控制构建过程的项目
    • Parcel :

      • 优势:零配置,开箱即用,构建速度快
      • 劣势:定制性较弱,生态不如 Webpack 丰富
      • 适用场景:快速原型开发,小型项目
      bash 复制代码
      # 使用 Parcel 构建项目
      parcel build src/index.html
  • Webpack vs Vite

    • Webpack :

      • 优势:成熟稳定,生态丰富,兼容性好
      • 劣势:开发服务器启动慢,HMR 速度较慢
      • 适用场景:大型生产项目,需要广泛浏览器兼容性
    • Vite :

      • 优势:开发服务器启动极快,HMR 性能优异
      • 劣势:生产构建依赖 Rollup,插件生态较小
      • 适用场景:现代浏览器环境,追求开发体验
      javascript 复制代码
      // vite.config.js
      export default {
        plugins: [],
        build: {
          target: 'esnext'
        }
      };
  • 实例应用:在我们的项目选型中,对比了多种构建工具后,选择 Webpack 作为主力构建工具,同时在一些小型工具库项目中使用 Rollup,在原型验证阶段使用 Vite 提高开发效率。

21.3 新一代构建工具

  • Vite

    • 基于原生 ES 模块的开发服务器
    • 使用 Rollup 进行生产构建
    • 极快的冷启动和热更新
    javascript 复制代码
    // vite.config.js
    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    
    export default defineConfig({
      plugins: [react()],
      build: {
        rollupOptions: {
          output: {
            manualChunks: {
              vendor: ['react', 'react-dom']
            }
          }
        }
      }
    });
  • esbuild

    • 使用 Go 语言编写的极速 JavaScript 打包器
    • 比传统打包器快 10-100 倍
    • 支持 ES6 和 CommonJS 模块
    javascript 复制代码
    // esbuild.js
    require('esbuild').build({
      entryPoints: ['src/index.js'],
      bundle: true,
      minify: true,
      outfile: 'dist/bundle.js'
    }).catch(() => process.exit(1));
  • SWC (Speedy Web Compiler)

    • Rust 编写的高性能 JavaScript/TypeScript 编译器
    • 可作为 Babel 的替代品,速度提升 20 倍以上
    • 支持 TypeScript、JSX 和最新的 JavaScript 特性
    javascript 复制代码
    // .swcrc
    {
      "jsc": {
        "parser": {
          "syntax": "typescript",
          "tsx": true
        },
        "transform": {
          "react": {
            "runtime": "automatic"
          }
        },
        "target": "es2015"
      }
    }
  • Turbopack

    • Vercel 开发的 Webpack 继任者
    • 基于 Rust 构建,性能大幅提升
    • 与 Webpack 生态兼容
    javascript 复制代码
    // turbo.json
    {
      "pipeline": {
        "build": {
          "outputs": ["dist/**"]
        },
        "dev": {
          "cache": false
        }
      }
    }
  • 实例应用:在我们的新项目中,使用 Vite 作为开发服务器,显著提高了开发效率;同时在 CI 流程中使用 esbuild 进行预构建,将构建时间从几分钟减少到几秒钟。

21.4 如何选择合适的构建工具

  • 项目类型与规模

    • 大型应用: Webpack 仍是首选,生态丰富,功能全面
    • 库开发: Rollup 或 esbuild,产出更清晰,体积更小
    • 小型应用: Vite 或 Parcel,开发体验好,配置简单
  • 团队因素

    • 团队熟悉度: 考虑团队对工具的熟悉程度
    • 学习成本: 新工具可能带来学习成本
    • 社区支持: 活跃的社区意味着更多资源和更快的问题解决
  • 性能需求

    • 开发体验: Vite、esbuild 提供更快的开发体验
    • 构建速度: 新一代工具在构建速度上有明显优势
    • 产物优化: Webpack 和 Rollup 在产物优化方面更成熟
  • 迁移策略

    • 渐进式迁移: 可以先在开发环境使用新工具,生产环境保持原有工具
    • 混合使用: 在不同项目或不同构建阶段使用不同工具
    • 插件兼容: 利用兼容层,如 @rollup/plugin-commonjs
  • 实例应用:在我们的技术选型中,建立了一套评估矩阵,综合考虑项目类型、团队熟悉度、性能需求和生态支持,为不同项目选择最合适的构建工具。对于核心业务项目,我们仍然选择 Webpack 作为主力构建工具,同时在新项目中尝试引入 Vite 提升开发体验。

21.5 未来趋势与展望

  • 构建工具发展趋势

    • 更快的构建速度: 利用多核和增量构建
    • 更智能的优化: 自动代码分割和预加载
    • 更简单的配置: 约定优于配置,智能默认值
    • 更好的开发体验: 即时反馈,精准错误提示
  • Web 开发趋势对构建工具的影响

    • ESM 标准化: 浏览器原生支持模块,减少构建工具复杂度
    • WebAssembly 普及: 构建工具需要更好地支持 Wasm 模块
    • 微前端架构: 构建工具需要支持独立部署和运行时集成
    • 边缘计算: 构建工具需要支持针对边缘环境的优化
  • Webpack 的未来

    • 性能提升: 借鉴新工具的技术提升性能
    • 简化配置: 提供更智能的默认配置
    • 生态整合: 与新工具形成互补而非竞争
    • 新特性支持: 持续跟进 Web 平台新特性
  • 实例应用:我们的团队持续关注构建工具的发展趋势,定期评估新工具和新技术,建立了技术雷达来追踪和评估这些变化。同时,我们也积极参与开源社区,为 Webpack 和其他工具贡献代码和文档,确保我们的技术栈与行业最佳实践保持同步。

22. 自定义 Loader 开发

22.1 Loader 基础知识

  • Loader 本质

    • Loader 本质上是一个函数,接收源文件内容作为参数,返回转换后的内容
    javascript 复制代码
    module.exports = function(source) {
      // source 是文件的原始内容
      const transformed = someTransformation(source);
      // 返回转换后的内容
      return transformed;
    };
  • Loader 上下文

    • this.query: 获取 loader 的配置选项
    • this.callback: 返回多个结果,如转换后的内容、source map 等
    • this.async: 处理异步 loader
    • this.emitFile: 输出文件
    • this.addDependency: 添加文件依赖,使其参与监听
  • Loader 分类

    • 前置(pre): 预处理,如 eslint-loader
    • 普通(normal): 标准转换,如 babel-loader
    • 内联(inline): 通过 import 语句指定的 loader
    • 后置(post): 后处理,如 postcss-loader

22.2 开发一个简单的 Markdown Loader

  • 需求:将 Markdown 文件转换为 HTML 并导入到 JS 中

    javascript 复制代码
    // markdown-loader.js
    const marked = require('marked');
    
    module.exports = function(source) {
      // 获取 loader 的配置选项
      const options = this.getOptions() || {};
      
      // 设置 marked 选项
      marked.setOptions(options);
      
      // 将 markdown 转换为 html
      const html = marked(source);
      
      // 返回一个模块导出
      return `export default ${JSON.stringify(html)}`;
    };
  • 在 Webpack 中使用自定义 Loader

    javascript 复制代码
    // webpack.config.js
    module.exports = {
      // ...其他配置
      module: {
        rules: [
          {
            test: /\.md$/,
            use: [
              'html-loader',
              {
                loader: path.resolve('./loaders/markdown-loader.js'),
                options: {
                  headerIds: false,
                  gfm: true
                }
              }
            ]
          }
        ]
      }
    };
  • 使用示例

    javascript 复制代码
    // 在组件中使用
    import React from 'react';
    import markdownContent from './content.md';
    
    const MarkdownComponent = () => (
      <div dangerouslySetInnerHTML={{ __html: markdownContent }} />
    );
    
    export default MarkdownComponent;

22.3 开发一个国际化资源处理 Loader

  • 需求:自动提取代码中的国际化文本,并生成翻译资源文件

    javascript 复制代码
    // i18n-loader.js
    const { parse } = require('@babel/parser');
    const traverse = require('@babel/traverse').default;
    const fs = require('fs');
    const path = require('path');
    
    module.exports = function(source) {
      const callback = this.async();
      const options = this.getOptions() || {};
      const outputPath = options.outputPath || './i18n';
      
      // 解析 JS 代码为 AST
      const ast = parse(source, {
        sourceType: 'module',
        plugins: ['jsx']
      });
      
      const i18nKeys = new Set();
      
      // 遍历 AST 查找 i18n 函数调用
      traverse(ast, {
        CallExpression(path) {
          if (
            path.node.callee.name === 'i18n' || 
            (path.node.callee.object && path.node.callee.object.name === 'i18n' && path.node.callee.property.name === 't')
          ) {
            const arg = path.node.arguments[0];
            if (arg && arg.type === 'StringLiteral') {
              i18nKeys.add(arg.value);
            }
          }
        }
      });
      
      // 确保输出目录存在
      if (!fs.existsSync(outputPath)) {
        fs.mkdirSync(outputPath, { recursive: true });
      }
      
      // 读取现有翻译文件或创建新文件
      const locales = options.locales || ['en', 'zh'];
      
      locales.forEach(locale => {
        const filePath = path.join(outputPath, `${locale}.json`);
        let translations = {};
        
        // 如果文件存在,读取现有翻译
        if (fs.existsSync(filePath)) {
          translations = JSON.parse(fs.readFileSync(filePath, 'utf8'));
        }
        
        // 添加新的翻译键
        i18nKeys.forEach(key => {
          if (!translations[key]) {
            translations[key] = locale === 'en' ? key : '';
          }
        });
        
        // 写入翻译文件
        fs.writeFileSync(filePath, JSON.stringify(translations, null, 2));
      });
      
      // 返回原始源代码
      callback(null, source);
    };
  • 在 Webpack 中使用

    javascript 复制代码
    // webpack.config.js
    module.exports = {
      // ...其他配置
      module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: [
              'babel-loader',
              {
                loader: path.resolve('./loaders/i18n-loader.js'),
                options: {
                  outputPath: './src/i18n',
                  locales: ['en', 'zh', 'ja']
                }
              }
            ]
          }
        ]
      }
    };

22.4 开发一个样式变量注入 Loader

  • 需求:将主题变量注入到样式文件中

    javascript 复制代码
    // theme-loader.js
    const fs = require('fs');
    const path = require('path');
    
    module.exports = function(source) {
      const options = this.getOptions() || {};
      const themePath = options.themePath || './src/theme.json';
      
      // 添加主题文件作为依赖,当主题文件变化时重新编译
      this.addDependency(path.resolve(themePath));
      
      // 读取主题变量
      const theme = JSON.parse(fs.readFileSync(path.resolve(themePath), 'utf8'));
      
      // 生成 CSS 变量定义
      const cssVars = Object.entries(theme).map(([key, value]) => `--${key}: ${value};`).join('\n');
      
      // 在样式文件开头注入变量
      const result = `:root {\n${cssVars}\n}\n\n${source}`;
      
      return result;
    };
  • 在 Webpack 中使用

    javascript 复制代码
    // webpack.config.js
    module.exports = {
      // ...其他配置
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader',
              {
                loader: path.resolve('./loaders/theme-loader.js'),
                options: {
                  themePath: './src/themes/default.json'
                }
              }
            ]
          }
        ]
      }
    };
  • 使用示例

    css 复制代码
    /* 主题文件 themes/default.json */
    {
      "primary-color": "#1890ff",
      "secondary-color": "#f5222d",
      "text-color": "#333333"
    }
    
    /* 样式文件 style.css */
    .button {
      color: var(--primary-color);
      background-color: white;
    }
    
    /* 转换后 */
    :root {
      --primary-color: #1890ff;
      --secondary-color: #f5222d;
      --text-color: #333333;
    }
    
    .button {
      color: var(--primary-color);
      background-color: white;
    }

22.5 开发一个图片优化 Loader

  • 需求:自动压缩和优化图片

    javascript 复制代码
    // image-optimize-loader.js
    const sharp = require('sharp');
    const loaderUtils = require('loader-utils');
    
    module.exports = function(source) {
      const callback = this.async();
      const options = this.getOptions() || {};
      
      // 默认优化选项
      const defaultOptions = {
        quality: 80,
        format: 'webp',
        width: null,
        height: null
      };
      
      const config = { ...defaultOptions, ...options };
      
      // 创建 sharp 实例
      let transformer = sharp(source);
      
      // 调整尺寸
      if (config.width || config.height) {
        transformer = transformer.resize(config.width, config.height, {
          fit: 'inside',
          withoutEnlargement: true
        });
      }
      
      // 转换格式
      if (config.format === 'webp') {
        transformer = transformer.webp({ quality: config.quality });
      } else if (config.format === 'jpeg') {
        transformer = transformer.jpeg({ quality: config.quality });
      } else if (config.format === 'png') {
        transformer = transformer.png({ quality: config.quality });
      }
      
      // 处理图片
      transformer.toBuffer()
        .then(data => {
          // 生成文件名
          const filename = loaderUtils.interpolateName(
            this,
            `[name].[hash:8].${config.format}`,
            { content: data }
          );
          
          // 输出文件
          this.emitFile(filename, data);
          
          // 返回模块导出
          callback(null, `export default "${filename}";`);
        })
        .catch(err => {
          callback(err);
        });
    };
  • 在 Webpack 中使用

    javascript 复制代码
    // webpack.config.js
    module.exports = {
      // ...其他配置
      module: {
        rules: [
          {
            test: /\.(png|jpe?g|gif)$/i,
            use: [
              {
                loader: path.resolve('./loaders/image-optimize-loader.js'),
                options: {
                  quality: 75,
                  format: 'webp',
                  width: 800
                }
              }
            ]
          }
        ]
      }
    };

22.6 Loader 开发最佳实践

  • 保持单一职责

    • 每个 loader 应该只做一件事,遵循 Unix 哲学
    • 复杂功能可以通过多个 loader 链式调用实现
  • 利用缓存

    • 默认情况下,webpack 会缓存 loader 的结果
    • 可以通过 this.cacheable(false) 禁用缓存
    javascript 复制代码
    module.exports = function(source) {
      // 默认可缓存
      // 如果处理结果依赖外部因素,可以禁用缓存
      if (someCondition) {
        this.cacheable(false);
      }
      return transformedSource;
    };
  • 处理依赖关系

    • 使用 this.addDependency() 添加文件依赖
    • 确保当依赖文件变化时,loader 会重新执行
    javascript 复制代码
    module.exports = function(source) {
      const configPath = path.resolve('./config.json');
      this.addDependency(configPath);
      // 处理逻辑
      return result;
    };
  • 提供清晰的错误信息

    • 使用 this.emitError() 或在回调中返回错误
    javascript 复制代码
    module.exports = function(source) {
      try {
        // 处理逻辑
        return result;
      } catch (err) {
        this.emitError(new Error(`处理失败: ${err.message}`));
        return source; // 返回原始内容,避免构建中断
      }
    };
  • 编写测试

    • 使用 webpack-loader-test 等工具测试 loader
    • 测试不同的输入和边界情况
    javascript 复制代码
    // loader.test.js
    const compiler = getCompiler('fixture.js', {
      module: {
        rules: [
          {
            test: /\.js$/,
            use: {
              loader: path.resolve(__dirname, './my-loader.js'),
              options: {/* 测试选项 */}
            }
          }
        ]
      }
    });
    
    const stats = await compile(compiler);
    const output = getModuleSource('fixture.js', stats);
    expect(output).toMatchSnapshot();
  • 实例应用:在我们的项目中,通过遵循这些最佳实践,我们开发了一套高效、可维护的自定义 loader,大大提高了开发效率和构建性能。例如,我们的国际化 loader 不仅自动提取文本,还能在开发过程中实时更新翻译文件,减少了手动维护的工作量。

23. 自定义 Plugin 开发

23.1 插件基础知识

  • 插件的本质

    • Webpack 插件是一个具有 apply 方法的 JavaScript 对象,该方法会在 Webpack 编译生命周期中被调用。
    javascript 复制代码
    class MyPlugin {
      apply(compiler) {
        compiler.hooks.done.tap('MyPlugin', (stats) => {
          console.log('编译完成!');
        });
      }
    }
  • 插件的生命周期

    • Webpack 提供了丰富的钩子,插件可以在编译的不同阶段进行操作,如 compileemitdone 等。

23.2 开发一个简单的日志插件

  • 需求 :在每次构建完成后输出构建时间和资源信息。

    javascript 复制代码
    class LogPlugin {
      apply(compiler) {
        compiler.hooks.done.tap('LogPlugin', (stats) => {
          console.log(`构建耗时: ${stats.endTime - stats.startTime}ms`);
          console.log(`生成资源数: ${Object.keys(stats.compilation.assets).length}`);
        });
      }
    }
    
    // 在 webpack 配置中使用
    module.exports = {
      // ...其他配置
      plugins: [
        new LogPlugin()
      ]
    };

23.3 插件开发最佳实践

  • 使用 Tapable 提供的钩子

    • 选择合适的钩子类型(如 SyncHookAsyncSeriesHook)以适应插件的同步或异步需求。
  • 处理异步操作

    • 使用 AsyncSeriesHookAsyncParallelHook 处理异步任务,确保在完成后调用 callback
  • 避免副作用

    • 插件应尽量避免对 Webpack 配置和编译过程产生不可预期的副作用。
  • 提供配置选项

    • 插件应提供合理的默认配置,并允许用户通过选项进行自定义。

23.4 开发一个资源压缩插件

  • 需求 :在构建过程中压缩输出的 JavaScript 文件。

    javascript 复制代码
    const TerserPlugin = require('terser-webpack-plugin');
    
    class CompressionPlugin {
      constructor(options = {}) {
        this.options = {
          test: /\.js$/,
          ...options
        };
      }
      
      apply(compiler) {
        compiler.hooks.emit.tapAsync('CompressionPlugin', (compilation, callback) => {
          // 遍历所有资源
          for (const filename in compilation.assets) {
            // 检查文件是否匹配测试规则
            if (this.options.test.test(filename)) {
              const asset = compilation.assets[filename];
              const source = asset.source();
              
              // 使用 Terser 压缩代码
              const result = TerserPlugin.minify(source);
              
              if (result.error) {
                compilation.errors.push(new Error(`压缩 ${filename} 时出错: ${result.error}`));
              } else {
                // 替换原始资源
                compilation.assets[filename] = {
                  source: () => result.code,
                  size: () => result.code.length
                };
                
                console.log(`已压缩: ${filename} (${source.length} -> ${result.code.length} 字节)`);
              }
            }
          }
          
          callback();
        });
      }
    }
    
    // 在 webpack 配置中使用
    module.exports = {
      // ...其他配置
      plugins: [
        new CompressionPlugin({
          test: /\.(js|css)$/
        })
      ]
    };

23.5 开发一个代码分析插件

  • 需求 :分析代码中的导入导出情况,生成依赖关系报告。

    javascript 复制代码
    const fs = require('fs');
    const path = require('path');
    
    class DependencyAnalyzerPlugin {
      constructor(options = {}) {
        this.options = {
          outputFile: 'dependency-report.json',
          ...options
        };
      }
      
      apply(compiler) {
        compiler.hooks.done.tap('DependencyAnalyzerPlugin', (stats) => {
          const modules = stats.toJson().modules;
          const dependencies = {};
          
          // 分析模块依赖
          modules.forEach(module => {
            if (module.name && !module.name.includes('node_modules')) {
              const normalizedName = module.name.replace(/\\/g, '/');
              
              dependencies[normalizedName] = {
                size: module.size,
                imports: module.reasons
                  .filter(reason => reason.moduleName)
                  .map(reason => reason.moduleName.replace(/\\/g, '/')),
                exports: module.providedExports || []
              };
            }
          });
          
          // 生成报告
          const outputPath = path.resolve(compiler.options.output.path, this.options.outputFile);
          const report = {
            timestamp: new Date().toISOString(),
            totalModules: Object.keys(dependencies).length,
            dependencies
          };
          
          fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
          console.log(`依赖分析报告已生成: ${outputPath}`);
        });
      }
    }
    
    // 在 webpack 配置中使用
    module.exports = {
      // ...其他配置
      plugins: [
        new DependencyAnalyzerPlugin({
          outputFile: 'reports/dependencies.json'
        })
      ]
    };

23.6 开发一个自动版本控制插件

  • 需求 :根据构建内容自动生成版本号并注入到应用中。

    javascript 复制代码
    const fs = require('fs');
    const path = require('path');
    const crypto = require('crypto');
    
    class VersionPlugin {
      constructor(options = {}) {
        this.options = {
          fileName: 'version.json',
          hashLength: 8,
          additionalData: {},
          ...options
        };
      }
      
      apply(compiler) {
        compiler.hooks.emit.tapAsync('VersionPlugin', (compilation, callback) => {
          // 计算所有资源的哈希值
          const assetsHash = this.getAssetsHash(compilation.assets);
          
          // 生成版本信息
          const versionInfo = {
            version: this.generateVersion(assetsHash),
            buildTime: new Date().toISOString(),
            ...this.options.additionalData
          };
          
          // 将版本信息添加到输出资源中
          const content = JSON.stringify(versionInfo, null, 2);
          compilation.assets[this.options.fileName] = {
            source: () => content,
            size: () => content.length
          };
          
          // 注入到 DefinePlugin 中,使应用可以访问版本信息
          if (compilation.options.plugins) {
            const definePlugin = compilation.options.plugins.find(
              plugin => plugin.constructor.name === 'DefinePlugin'
            );
            
            if (definePlugin) {
              definePlugin.definitions = definePlugin.definitions || {};
              definePlugin.definitions['process.env.VERSION'] = JSON.stringify(versionInfo.version);
            }
          }
          
          callback();
        });
      }
      
      getAssetsHash(assets) {
        const hash = crypto.createHash('md5');
        
        Object.keys(assets).sort().forEach(filename => {
          hash.update(filename);
          hash.update(assets[filename].source());
        });
        
        return hash.digest('hex');
      }
      
      generateVersion(hash) {
        const date = new Date();
        const datePart = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`;
        const hashPart = hash.substring(0, this.options.hashLength);
        
        return `${datePart}-${hashPart}`;
      }
    }
    
    // 在 webpack 配置中使用
    module.exports = {
      // ...其他配置
      plugins: [
        new VersionPlugin({
          additionalData: {
            environment: process.env.NODE_ENV,
            appName: 'MyAwesomeApp'
          }
        })
      ]
    };

23.7 开发一个多页面应用插件

  • 需求 :自动为多页面应用生成入口配置和HTML文件。

    javascript 复制代码
    const fs = require('fs');
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    class MultiPagePlugin {
      constructor(options = {}) {
        this.options = {
          pagesDir: './src/pages',
          template: './src/template.html',
          filename: '[name].html',
          chunks: ['common', '[name]'],
          ...options
        };
      }
      
      apply(compiler) {
        // 在 entryOption 钩子中修改入口配置
        compiler.hooks.entryOption.tap('MultiPagePlugin', (context, entry) => {
          const pagesDir = path.resolve(this.options.pagesDir);
          const pages = this.getPages(pagesDir);
          
          // 设置多入口
          const newEntry = {};
          
          pages.forEach(page => {
            const entryName = path.basename(page, path.extname(page));
            newEntry[entryName] = path.resolve(pagesDir, page);
            
            // 为每个页面添加 HtmlWebpackPlugin
            const htmlPlugin = new HtmlWebpackPlugin({
              template: this.options.template,
              filename: this.options.filename.replace('[name]', entryName),
              chunks: this.options.chunks.map(chunk => chunk.replace('[name]', entryName)),
              title: entryName.charAt(0).toUpperCase() + entryName.slice(1),
              inject: true
            });
            
            // 添加到编译器插件列表
            compiler.options.plugins.push(htmlPlugin);
          });
          
          // 替换原始入口配置
          compiler.options.entry = newEntry;
        });
      }
      
      getPages(pagesDir) {
        // 获取所有页面入口文件
        return fs.readdirSync(pagesDir)
          .filter(file => /\.(js|ts|jsx|tsx)$/.test(file));
      }
    }
    
    // 在 webpack 配置中使用
    module.exports = {
      // ...其他配置
      plugins: [
        new MultiPagePlugin({
          pagesDir: './src/pages',
          template: './src/templates/page.html'
        })
      ]
    };

23.8 插件调试技巧

  • 使用 console 输出调试信息

    javascript 复制代码
    apply(compiler) {
      compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
        console.log('compilation 阶段开始');
        console.log('当前钩子:', Object.keys(compilation.hooks));
      });
    }
  • 使用 Node.js 调试器

    javascript 复制代码
    // 在插件代码中添加调试点
    apply(compiler) {
      compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
        debugger; // 这里会触发调试器断点
        // 处理逻辑
        callback();
      });
    }
    
    // 使用 --inspect 启动 webpack
    // node --inspect-brk ./node_modules/.bin/webpack
  • 检查钩子和参数

    javascript 复制代码
    apply(compiler) {
      // 列出所有可用的钩子
      console.log('可用的编译器钩子:', Object.keys(compiler.hooks));
      
      // 检查特定钩子的参数
      compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
        console.log('compilation 对象属性:', Object.keys(compilation));
        console.log('assets:', Object.keys(compilation.assets));
        callback();
      });
    }
  • 实例应用:在开发一个复杂的资源优化插件时,我们使用这些调试技巧快速定位了一个难以重现的问题,发现是在特定条件下资源路径解析错误导致的。通过添加适当的调试点,我们不仅修复了问题,还优化了插件的整体性能。

相关推荐
人帅是非多2 分钟前
基于Compose桌面的Material You风格ADB文件管理器实现
前端
MiyueFE28 分钟前
bpmn-js 源码篇8:Featrues 体验优化与功能扩展(三)
前端·javascript
野猪佩奇00736 分钟前
Vue项目的 Sass 全局基础样式格式化方案,包含常见元素的样式重置
前端·css·vue.js·sass
独立开阀者_FwtCoder37 分钟前
penAI重磅发布GPT-4o文生图:免费、精准、媲美真实照片!
前端·后端·面试
IBELIEVE1 小时前
前端打包文件本地简易部署
前端
逆袭的小黄鸭1 小时前
仿 ElementPlus 组件库(九)—— Switch 组件实现
前端·vue.js·typescript
curdcv_po1 小时前
Vue 项目线上更新无需强制刷新的方案
前端
dchen771 小时前
xhr和fetch的一些区别对比
前端·javascript·面试