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();
      });
    }
  • 实例应用:在开发一个复杂的资源优化插件时,我们使用这些调试技巧快速定位了一个难以重现的问题,发现是在特定条件下资源路径解析错误导致的。通过添加适当的调试点,我们不仅修复了问题,还优化了插件的整体性能。

相关推荐
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端
爱敲代码的小鱼12 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte13 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc