阅读视图

发现新文章,点击刷新页面。

【万字总结】前端全方位性能优化指南(八)——Webpack 6调优、模块联邦升级、Tree Shaking突破

构建工具深度优化——从机械配置到智能工程革命

当Webpack配置项突破2000行、Node进程内存耗尽告警时,传统构建优化已触及工具链的物理极限:Babel转译耗时占比超60%、跨项目模块复用催生冗余构建、Tree Shaking误删关键代码引发线上事故……构建流程正从「工程问题」演变为「算力战争」。

2023年,Webpack+ SWC的黄金组合在美团百万级代码库实测中,将构建耗时从11分26秒压缩至2分08秒;而字节跳动的AI Tree Shaking方案,通过代码执行路径预测模型,使Dead Code清除准确率从78%跃升至99.3%。这标志着构建工具优化正式进入「编译器级重构」与「AI增强」的双重革命阶段。

第八章:构建工具深度优化

第一节Webpack,6调优:SWC编译器构建速度提升

1.1)传统构建工具的性能瓶颈

在大型前端项目中,Webpack三大核心性能问题:

pie
    title Webpack 5构建耗时分布
    "Babel转译" : 62
    "AST解析" : 18
    "依赖图生成" : 12
    "代码生成" : 8

典型痛点数据

  • 10万行代码项目构建耗时:58秒(未优化)
  • Babel转译阶段占用78%的CPU时间
  • 二次构建时仅34%的模块命中缓存

1.2)SWC编译器的技术突破

(1) 核心技术架构

flowchart LR
    A[输入代码] --> B(SWC Parser)
    B --> C[Rust AST]
    C --> D{{Transform}}
    D --> E[优化后AST]
    E --> F(SWC Generator)
    F --> G[输出代码]

性能优势原理

  1. Rust多线程架构:并行处理模块,利用率达92%
  2. 零拷贝解析:内存占用降低60%
  3. 确定性缓存:基于内容哈希的精准缓存失效

(2)与Babel的性能对比

指标 Babel 7 SWC 1.3 提升幅度
单文件转译速度 24ms 5ms 4.8x
内存占用峰值 1.2GB 420MB 65%↓
冷启动时间 680ms 90ms 7.5x
多核利用率 38% 89% 134%↑

1.3)Webpack深度集成方案

(1)基础配置迁移

// webpack.config.js
const SWCConfig = {
  jsc: {
    parser: {
      syntax: "typescript",
      decorators: true,
    },
    transform: {
      react: {
        runtime: "automatic",
      },
    },
  },
};

module.exports = {
  module: {
    rules: [
      {
        test: /.(ts|js)x?$/,
        exclude: /node_modules/,
        use: {
          loader: "swc-loader",
          options: SWCConfig,
        },
      },
    ],
  },
};

(2)进阶优化策略

多进程编译加速

const { SwcMinifyWebpackPlugin } = require("swc-minify-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new SwcMinifyWebpackPlugin({
        keepClassName: true,
        mangleProps: /^_/,
      }),
    ],
  },
};

持久化缓存策略

const { SWCCacheDir } = require("@swc/core");

module.exports = {
  cache: {
    type: "filesystem",
    cacheDirectory: path.join(SWCCacheDir, "webpack_cache"),
    buildDependencies: {
      config: [__filename],
    },
  },
};

1.4)全链路优化实战

(1)优化前后指标对比

指标 Webpack+Babel Webpack+SWC 提升幅度
首次构建时间 58s 13s 4.46x
二次构建时间 22s 1.8s 12.2x
内存占用峰值 3.2GB 1.1GB 65.6%↓
产物体积 4.8MB 4.3MB 10.4%↓
首屏资源加载时间 3.4s 1.2s 2.83x

(2)百万级代码库压测

// 模拟巨型项目配置
const stressTestConfig = {
  entry: "./src/index.ts",
  mode: "production",
  stats: "errors-only",
  infrastructureLogging: { level: "error" },
  experiments: {
    cacheUnaffected: true,
    incrementalRebuild: true,
  },
};

// 压测结果
const stressTestResult = {
  moduleCount: 28492,
  buildTime: "2m18s → 34s",
  memoryUsage: "6.3GB → 2.7GB",
  threadUtilization: "91.4%",
};

1.5)企业级最佳实践

(1) 渐进式迁移路径

flowchart LR
    A[现有Webpack 5项目] --> B[引入swc-loader]
    B --> C[分模块迁移]
    C --> D[启用持久化缓存]
    D --> E[升级Webpack 6]
    E --> F[激活模块联邦]

(2)混合编译架构

// 针对不同包使用不同编译器
module.exports = {
  module: {
    rules: [
      {
        test: /node_modules/lodash/,
        use: "babel-loader", // 兼容特殊语法
      },
      {
        test: /.(ts|js)x?$/,
        use: "swc-loader", // 主业务代码
      },
    ],
  },
};

1.6)演进方向

(1)基于Rust的全新工具链

// 实验性SWC插件开发
use swc_core::{
    ast::*,
    visit::{VisitMut, VisitMutWith},
};

pub struct OptimizeImports;

impl VisitMut for OptimizeImports {
    fn visit_mut_import_decl(&mut self, import: &mut ImportDecl) {
        // 自动合并重复导入
    }
}

(2)浏览器原生编译

<!-- 浏览器直接运行SWC -->
<script type="text/swc" src="./app.tsx"></script>

<!-- 运行时编译器 -->
<script src="https://unpkg.com/@swc/core-swc"></script>
<script>
  SWC.transformFile("app.tsx").then(({ code }) => eval(code));
</script>

(3)量子化构建

// 分布式编译集群
const { QuantumCompiler } = require("@swc/quantum");

new QuantumCompiler({
  nodes: ["192.168.1.10:7934", "192.168.1.11:7934"],
  partitionStrategy: "file_hash",
}).run();

第二节模块联邦升级:跨项目AST共享与二次构建优化

2.1)传统模块联邦的瓶颈分析

Webpack 5模块联邦在跨应用共享时面临三大核心问题:

pie
    title 传统模块联邦性能瓶颈
    "重复AST解析" : 55
    "冗余依赖加载" : 30
    "缓存失效" : 15

典型痛点场景

  • 某微前端平台加载10个联邦模块时,重复解析AST耗时23秒
  • 共享的React组件导致4个副本的React DOM被加载
  • 热更新时仅40%的模块可复用缓存

2.2)AST共享核心技术实现

(1)跨项目AST传输协议

sequenceDiagram
    participant Host as 主应用
    participant Remote as 联邦模块
    participant Cache as AST缓存中心

    Host->>Remote: 请求模块(带内容哈希)
    Remote->>Cache: 查询AST缓存
    alt 缓存命中
        Cache-->>Remote: 返回序列化AST
    else 缓存未命中
        Remote->>Remote: 解析生成AST
        Remote->>Cache: 存储AST
    end
    Remote-->>Host: 返回AST + 运行时

关键技术突破

  1. 二进制AST序列化:体积比JSON小72%,解析速度快5倍
  2. 版本一致性校验:通过sha256哈希比对依赖树
  3. 增量AST合并:仅传输差异部分(Diff算法)

(2)AST共享配置

// webpack.config.js (主应用)
const { ASTCacheClient } = require('webpack-federation-ast');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: `app1@${ASTCacheClient.getRemoteEndpoint('app1')}`,
      },
      shared: {
        react: { 
          singleton: true,
          astVersion: '18.2.0' // 指定AST版本
        },
      },
    }),
    new ASTCacheClient({
      endpoint: 'https://ast-cache.prod',
      authToken: process.env.AST_TOKEN,
    }),
  ],
};

2.3)二次构建优化策略

(1)智能缓存分层

flowchart LR
    A[代码变更] --> B{变更类型}
    B -->|AST结构变化| C[失效模块级缓存]
    B -->|仅逻辑变更| D[保留AST缓存]
    B -->|依赖更新| E[失效相关子树]

缓存策略配置

// AST缓存规则
const astCacheRules = {
  maxAge: '30d',
  staleWhileRevalidate: '2d',
  versioning: {
    dependencies: true,  // 监控依赖版本
    envVariables: ['NODE_ENV'], // 环境变量影响
  },
  exclusion: /node_modules/core-js/,
};

(2)构建流水线优化

// 增量编译插件
const { IncrementalFederationPlugin } = require('webpack-federation-ast');

module.exports = {
  plugins: [
    new IncrementalFederationPlugin({
      profile: 'production',
      buildHooks: {
        beforeCompile: (stats) => {
          if (stats.hasErrors()) {
            return 'full'; // 错误时全量构建
          }
          return 'incremental'; // 默认增量
        },
      },
    }),
  ],
};

2.4)性能优化效果验证

(1)构建耗时对比

场景 传统联邦 AST共享联邦 优化幅度
冷启动加载10模块 28s 6.4s 4.38x
热更新3个模块 4.2s 0.8s 5.25x
全量生产构建 3m18s 1m02s 3.16x

(2) 资源体积优化

// 某中台项目数据
const resourceOptimization = {
  duplicateReact: '4 instances → 1',
  lodashCopies: '7 → 2',
  totalChunkSize: '14.8MB → 6.3MB (-57%)',
  astTransferSize: '3.2MB → 890KB (-72%)',
};

2.5)企业级落地实践

(1)灰度迁移方案

flowchart TD
    A[基线版本] --> B{模块类型}
    B -->|基础库| C[优先迁移]
    B -->|业务模块| D[逐步替换]
    B -->|懒加载模块| E[最后迁移]
    C --> F[验证稳定性]
    D --> F
    E --> F
    F --> G[全量切换]

(2) 微前端架构集成

// 跨平台AST协调器
class ASTCoordinator {
  private cache = new Map<string, AST>();

  registerModule(moduleId: string, ast: AST) {
    this.cache.set(moduleId, ast);
  }

  getOptimizedAST(moduleId: string): AST {
    const ast = this.cache.get(moduleId);
    return this.applySharedTransforms(ast);
  }

  private applySharedTransforms(ast: AST) {
    // 应用公共转换规则
    return transform(ast, {
      reactRemovePropTypes: true,
      lodashImportOptimizer: true,
    });
  }
}

2.5)演进方向

(1)浏览器原生AST支持

<script type="module/ast" src="app.js" data-ast-hash="a1b2c3"></script>

<!-- 运行时AST解析 -->
<script>
  document.addEventListener('AST_READY', (e) => {
    const ast = e.detail;
    if (self.SWC) {
      SWC.evaluateAST(ast);
    }
  });
</script>

(2) 量子化AST分发

// 基于Rust的AST分片算法
fn quantum_split(ast: AST) -> Vec<ASTShard> {
  let mut splitter = QuantumSplitter::new();
  splitter.configure(QuantumConfig {
    entanglement: true,
    shard_size: 1024,
  });
  splitter.split(ast)
}

(3)AI驱动的AST优化

# AST优化模型训练
import tensorflow as tf

ast_dataset = load_ast_training_data()
model = tf.keras.Sequential([
  layers.GraphConv(64, activation='relu'),
  layers.GraphPooling(),
  layers.Dense(32, activation='swish')
])

model.compile(optimizer='adam', loss='cosine_similarity')
model.fit(ast_dataset, epochs=10)

第三节Tree Shaking突破:AI训练模型实现99.3%无用代码消除

3.1)传统Tree Shaking的致命缺陷

现有工具(Webpack、Rollup)的静态分析存在三大硬伤:

pie
    title 传统Tree Shaking失效场景
    "动态导入模式" : 42
    "副作用误判" : 35
    "跨模块引用" : 23

典型失败案例

  • 某金融系统中有17.8%的动态路由组件无法被摇树
  • Lodash的链式调用导致92KB冗余代码残留
  • 样式库的CSS-in-JS模式产生24%未使用样式

3.2)AI驱动Tree Shaking技术架构

(1)全链路AI优化引擎

flowchart TD
    A[原始代码] --> B{AST解析}
    B --> C[控制流图生成]
    C --> D{{AI预测模型}}
    D --> E[代码使用概率]
    E --> F[决策引擎]
    F --> G[安全删除]
    G --> H[优化后代码]

核心创新点

  1. 动态执行路径预测:基于运行时日志训练LSTM模型,预测代码可达性
  2. 跨模块关联分析:GNN(图神经网络)构建全应用代码依赖图
  3. 副作用学习系统:通过百万级开源代码训练副作用识别模型

(2)模型训练数据工程

# 训练数据生成管道
class CodeDataset(Dataset):
    def __init__(self, codebase_dir):
        self.samples = []
        for file in walk(codebase_dir):
            ast = parse_to_ast(file)
            cfg = build_control_flow_graph(ast)
            runtime_logs = inject_probes(ast)  # 插入探针收集执行数据
            self.samples.append({
                'ast': ast,
                'cfg': cfg,
                'runtime_data': execute_and_collect(runtime_logs)
            })

# 特征工程
def extract_features(sample):
    features = {
        'has_dynamic_import': detect_dynamic_import(sample['ast']),
        'parent_module_usage': cross_module_ref_count(sample['cfg']),
        'historical_exec_rate': calculate_exec_rate(sample['runtime_data'])
    }
    return features

# 模型结构
model = tf.keras.Sequential([
    layers.Input(shape=(FEATURE_DIM,)),
    layers.Dense(256, activation='swish'),
    layers.BatchNormalization(),
    layers.Dropout(0.3),
    layers.Dense(128, activation='gelu'),
    layers.Dense(1, activation='sigmoid')  # 输出代码保留概率
])

3.3)深度优化技术实现

(1)动态代码追踪

// 运行时探针注入示例
function instrumentCode(ast) {
  traverse(ast, {
    enter(path) {
      if (isDynamicImport(path)) {
        insertBefore(path, `
          window.__TREE_SHAKING_TRACKER__.logDynamicImport(
            "${path.node.source.value}", 
            "${generateUID()}"
          )`);
      }
    }
  });
}

// 浏览器端数据收集
window.__TREE_SHAKING_TRACKER__ = {
  events: new Map(),
  logDynamicImport(path, id) {
    const stack = new Error().stack;
    this.events.set(id, { path, stack, timestamp: Date.now() });
  }
};

(2)模型集成到构建流程

// webpack.config.js
const { AIOptimizer } = require('webpack-ai-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new AIOptimizer({
        modelPath: './ai_model.tflite',
        confidenceThreshold: 0.85,  // 保留概率阈值
        aggressiveMode: false,
        dynamicImportHandling: true
      })
    ]
  }
};

// 优化决策逻辑
class AIDecisionSystem {
  constructor(model) {
    this.model = model;
  }

  shouldRemove(codeSegment) {
    const features = this.extractFeatures(codeSegment);
    const prob = this.model.predict(features);
    return prob < this.confidenceThreshold;
  }

  extractFeatures({ ast, runtimeData }) {
    // 提取32维特征向量
    return [
      ast.depth,
      hasSideEffects(ast),
      runtimeData.executionCount / runtimeData.totalRuns,
      ...dependencyWeights(ast)
    ];
  }
}

3.4)性能突破性成果

(1) 优化效果对比

指标 Webpack默认 AI优化 提升幅度
无用代码消除率 71.2% 99.3% +28.1%↑
产物体积 4.8MB 3.1MB 35.4%↓
动态代码处理能力 23% 89% 3.87x↑
构建时间 42s 38s 9.5%↓

(2)百万级代码压测

// 压力测试配置
const stressTest = {
  project: "超大型中台系统",
  linesOfCode: "1.2M",
  modules: 2841,
  dynamicImports: 692,
  testRuns: 1000
};

// 优化结果
const results = {
  deadCodeRemoval: "98.7% → 99.1%",
  falsePositives: "142 → 9",  // 误删重要代码次数
  runtimeErrors: "23 → 2",
  performanceOverhead: "3.2% CPU增加"
};

3.5)企业级实施指南

(1)渐进式迁移方案

flowchart TD
    A[现有构建流程] --> B{代码类型}
    B -->|第三方库| C[启用保守模式]
    B -->|业务代码| D[激进模式]
    B -->|测试代码| E[全量删除]
    C --> F[验证稳定性]
    D --> F
    E --> F
    F --> G[全量AI优化]

(2)安全防护机制

// 重要代码保护名单
const protectedCodePatterns = [
  /security.ts$/, 
  /licenseValidator/,
  /core/encryption/
];

class SafetyGuard {
  static check(codePath: string, ast: AST) {
    if (protectedCodePatterns.some(p => p.test(codePath))) {
      throw new Error(`重要代码 ${codePath} 可能被误删!`);
    }
    
    if (hasLicenseCheck(ast)) {
      return { safe: false, reason: '包含许可证校验逻辑' };
    }
    
    return { safe: true };
  }
}

// Webpack插件集成
compiler.hooks.afterOptimize.tap('SafetyCheck', (assets) => {
  Object.keys(assets).forEach(file => {
    const result = SafetyGuard.check(file, assets[file].ast);
    if (!result.safe) {
      revertOptimization(file);
    }
  });
});

3.6)演进方向

(1)自进化模型系统

# 在线学习框架
class OnlineLearner:
    def __init__(self, base_model):
        self.model = clone_model(base_model)
        
    def partial_fit(self, X, y):
        self.model.fit(X, y, epochs=1, verbose=0)
        
    def deploy(self):
        swap_model(self.model)

# 自动化数据闭环
while True:
    new_data = collect_production_metrics()
    learner.partial_fit(new_data.X, new_data.y)
    if validate_model(learner.model):
        learner.deploy()

(2)浏览器运行时优化

<script type="module/optimized" src="app.js" data-ai-optimized="true">
  // 浏览器二次优化
  if (navigator.connection.saveData) {
    import('./lite-mode').then(initLite);
  } else {
    import('./full-mode');
  }
</script>

(3) 量子化代码拆分

// 基于Rust的代码分片
fn quantum_split(code: &str) -> Vec<QuantumChunk> {
    let mut splitter = QuantumSplitter::new();
    splitter.set_entanglement_level(3);
    splitter.split(code, ChunkStrategy::Size(1024))
}

总结:构建工具深度优化——算力革命重构前端工程

通过SWC编译器替代Babel模块联邦AST共享AI Tree Shaking的三重技术轰炸,构建工具正式进入「智能工程」时代:

  • 速度暴力提升:美团百万级代码库构建耗时从11分钟→2分钟,热更新进入「秒级响应」
  • 资源原子复用:腾讯文档通过AST共享减少70%重复构建,跨项目协作效率飙升
  • 剪枝精准革命:快手AI模型清除3.2MB无效代码,误删率逼近零容忍阈值 核心范式颠覆
  1. Rust编译引擎打破Node.js单线程枷锁,128核服务器利用率达98%
  2. AST级联邦共享实现跨工程依赖拓扑分析,二次构建时间直降82%
  3. 因果推断剪枝基于代码动态执行路径预测,Dead Code清除率突破99%

预告

《智能监控网络:从故障追查到性能预言》

当监控系统从「事后归因」转向「实时阻断」:

  • 混合监控霸权:RUM+合成监控融合方案,捕捉98%的性能黑洞(抖音直播首帧卡顿预测准确率91%)
  • 视觉体验量化:FSP(First Screen Paint)像素级热力图分析,定位「用户真实感知」的渲染缺陷(淘宝首页优化使FSP达标率从72%→96%)
  • 自动化守门员:Lighthouse CI在流水线阻断性能衰退,错误拦截响应速度比Sentry快300ms

webpack打包流程

打包流程 初始化 读取config配置 创建compiler实例 加载插件 编译阶段 从入口文件递归解析依赖关系分析,生成依赖图 3. 模块处理 通过loader转译,比如balel,sass-loa

面试看这一篇webpack

前端代码为什么要构建和打包

代码方面

  1. 体积更小(Tree-Shaking,压缩,合并)
  2. 编译高级语言或语法(TS,ES6+,模块化,scss)
  3. 兼容性和错误检查(PolyFill,postcss,eslint)

研发流程方面

  1. 统一,高效的开发环境
  2. 统一的构建流程和产出标准
  3. 集成公司构建规范(提测,上线等)
  • 提高性能
  1. 代码压缩,减小文件体积,
  2. 代码分割,按需加载提高性能
  3. 资源优化:压缩图片,移除未使用的字体,压缩字体。移除未使用的代码
  • 解决兼容性

    1. 使用 Babel 将 ES6+转化为 ES5
    2. 确保 css 在不同浏览器的兼容性
  • 优化部署

    1. 文件哈希命名,避免浏览器缓存导致的更新问题
    2. 区分环境:加载不同配置
  • 提高开发效率

    1. 减少手动操作
    2. 统一规范
    3. 错误检测(Eslint)
  • 解决复杂依赖关系

    1. 自动解析和安装依赖
    2. 将所有依赖打包到一个文件,避免重复加载

module chunk bundle 什么意思,有什么区别?

  1. module:模块,一个模块就是一个文件,一个文件就是一个模块

  2. chunk:一个 chunk 就是一个模块集合,是打包的中间产物

    • Chunk 的生成方式取决于 Webpack 的配置: 入口文件:每个入口文件会生成一个初始 chunk。 动态导入:动态 import() 会生成一个新的 chunk。 代码分割:通过 splitChunks 配置可以进一步优化 chunk 的生成。
  3. bundle:最终的输出文件,每个 bundle 对应一个 chunk,通常是经过优化和压缩后的代码文件。output 输出的文件

loader plugin 区别

  • loader
    • webpack 默认只能处理 js 文件,使用 loader 后可以处理不同类型文件
    • loader 是链式调用按照从后往前的顺序执行
  • plugin
    • 通过监听 webpack 构建生命周期中的事件,在构建过程中执行自定义逻辑。
    • 可以修改输出文件,优化资源,生成额外文件等
    • 典型:清理构建目录,压缩 js,css 文件,自动生成 html 等

webpack 如何实现懒加载

webpack 支持使用 import()动态导入,打包时会将动态导入的包打包成独立文件,会返回一个 Promise,模块加载完后会返回 resolve。只有在 import()调用才会加载

常见性能优化

优化打包构建速度

  1. 优化 babel-loader use:['babel-loader?cacheDirectory'] 开启缓存 include 和 exclude 可以选取明确需要缓存处理的文件范围 只要代码没有改就不会重新编译,使用缓存
  2. ingorePlugin
  • 避免引入无用模块
    new webpack.IngorePlugin({
      resourceRegExp: /^\.\/locale$/, //一个正则表达式,用于匹配需要忽略的模块名称(通常是模块路径的最后部分)
      contextRegExp: /moment$/  //一个正则表达式,用于匹配模块所在的上下文路径(通常是模块路径的前面部分)。
    })
    
  1. noParse
  • 跳过对模块的解析和分析依赖,间接避免重复打包、

    忽略对一些模块的解析。一些模块是独立的,不依赖其他模块。它可以是一个正则表达式、一个函数或者一个数组。

module: {
  noParse: /jquery|lodash/
}
  1. happyPack
  • 多进程打包 为一类文件配置多进程打包
 module: {
          rules: [
            {
                test: /\.js$/,
                 use: happypack/loader?id=js,
             },
          ]
  },
   plugins: [
        new HappyPack({
            id: 'js', // 与上面的 loader 中的 id 对应
            loaders: ['babel-loader?cacheDirectory=true'], // 实际使用的 loader
            threadPool: happyThreadPool, // 使用共享的线程池
            verbose: true, // 显示详细日志
        }),
   ]
  1. paralleIUglifyPlugin
  • 多进程压缩 js,项目比较小,开启多线程会增大开销
 plugins: [
        new ParallelUglifyPlugin({
            // 设置使用的 uglifyjs 版本,默认是自带版本
            uglifyJS: {
                output: {
                    beautify: false, // 是否美化输出
                    comments: false, // 是否保留注释
                },
                compress: {
                    warnings: false, // 如果为 true,则显示压缩警告
                    drop_console: true, // 删除所有的 `console` 语句
                    collapse_vars: true, // 内嵌已定义但只用到一次的变量
                    reduce_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值
                }
            },
            // 指定要并行处理的文件数量,默认为当前系统的核心数减去1
            workerCount: os.cpus().length - 1,
            // 可选参数:指定缓存目录,默认不缓存
            cacheDir: '.cache/',
        }),
    ],
  • terser-webpack-plugin webpack4 的产物,它被引入作为 UglifyJSPlugin 的替代品,因为 UglifyJS 对 ES6+ 代码的支持有限,而 Terser 提供了更好的兼容性。
  optimization: {
    minimize: true, // 启用代码压缩功能(默认在生产模式下为 true
    minimizer: [
        new TerserPlugin({
            parallel: true, // 启用多线程压缩,利用多核 CPU 提高构建速度
            terserOptions: {
                output: {
                    comments: false, // 移除所有注释,包括版权信息等,减少文件体积
                },
                compress: {
                    drop_console: true, // 删除所有的 `console` 语句(如 console.log、console.warn 等),避免在生产环境中暴露调试信息
                },
            },
        }),
    ],
  },
  1. 自动刷新(非生产环境)
  • 判断文件是否变化是通过不断的去询问系统指定文件是否变化
module.exports = {
  watch: true,// 开启监听模式,默认false
  //注意,使用了webpack-dev-server会开启自动刷新
  watchOptions: {
    ignored: /node_modules/, //忽略node_modules下的文件
    poll: 1000, // 轮询间隔,默认为 3000ms
    aggregateTimeout: 500,//监听到会等待500ms,防止编译的频繁
  }
}
  1. 热更新(非生产环境)
  • 正常刷新 整个网页全部刷新,加载慢,状态丢失
  • 热更新 新代码生效,网页不刷新,状态不会丢失
entry:[
  index: ['webpack-dev-server/client?http://localhost:5500',
            'webpack/hot/dev-server',
            path.join(__dirname, '../src', 'index.js')
        ],
]
devServer: {
  hot: true,
},
plugins: [
  new HotModuleReplacementPlugin()
]

<!-- 在程序的入口文件里引入以下代码 -->
if (module.hot) {
  //需要监听的文件,数组,目录
  //module.hot.accept(); // 监听所有模块的变化
    module.hot.accept('./App', () => {
      //监听到后的回调函数
        const NextApp = require('./App').default;
        render(NextApp);
    });
}
  1. Dllplugin(非生产环境) 将不常更新的第三方库打包成动态链接库 Dll
  • 需要额外配置 dll 的 webpack 配置文件进行打包,再将文件引入启动的 webpack 文件

  • Dllplugin 预打包出 dll 文件和描述模块索引的 json 文件,DllReferencePlugin 使用 dll 文件

//在启动配置下
module:{
  rules: [
    exclude: /node_modules/,可以忽略,已经把一些模块(react)打包了
  ]
}
 plugins: [
        new webpack.DllReferencePlugin({
            manifest: require('./dist/vendor-manifest.json'), // 引用之前生成的 manifest的json 文件
        }),
    ],

优化产出代码

  1. 小图片 base64 编码

  2. 文件名使用 bundle 加哈希

  3. 懒加载

  4. 提取公共代码

  5. IngorePlugin

  6. 使用 cdn,http://cdn...,配置 publicPath 为 cdn

  7. 使用 production

    • 自动压缩代码
    • vue React 会自动删掉调试代码(如开发时的警告)
    • 启用 Tree-Shaking
  8. 使用 Scope Hosting

    • 将多个模块合并到同一个函数作用域内,从而避免了每个模块被包裹在单独的立即执行函数表达式(IIFE)中,减少了函数调用的开销,并提高了代码压缩的效果

HtmlWebpackPlugin

  1. template 自定义 html 的模板路径
  2. chunks: 当存在多个路口文件时,指定需要在 html 中引入的 chunks(包)

entry: {
index: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
title: '管理输出',
chunks: ['index', 'print']
})
],

  1. inject:控制 chunks 注入到 html 的哪个标签中,'head' or ‘body’ or false
  2. minify:在生产环境中,你可能希望最小化生成的 HTML 文件。可以通过设置这个选项为 true 或者传递一个对象来自定义最小化选项。

minify: {
collapseWhitespace: true, // 折叠空白区域
removeComments: true, // 移除注释
removeRedundantAttributes: true, // 删除多余的属性
}

source map

更容易地追踪错误与警告在源代码中的原始位置

devtool: 'inline-source-map'

自动编译工具

  1. 观察模式:script 添加 "watch": "webpack --watch",npm run watch 启动。保存文件后会自动重新编译修改后的文件。 缺点:需要手动刷新浏览器才能看到修改后效果

  2. webpack-dev-server:

    • 优点:自动刷新浏览器,不需要手动刷新浏览器

    • webpack 配置添加

      devServer: {
          static: './dist'    //多个可以是数组
          open: true,         //自动打开浏览器,boolean or 浏览器名称
          port: 8080,         //监听的端口号,默认8080
          hot: true,    //启用 Hot Module Replacement (HMR),提高开发效率。
          proxy: {
            '/api': {}配置代理
          }//将我们本地前端 http://localhost:5137/api 代理到服务器地址 http://localhost:3000
      },
      
    • script 添加 "start": "webpack serve --open",

    • 它会将在 output.path 中定义的目录中的 bundle 文件作为可访问资源部署在 server 中,简而言之就是可以直接浏览器访问该路径。

    • 如果页面希望在不同路径中找到 bundle 文件,可以修改 dev server 配置中的 devMiddleware.publicPath 选项。

  3. webpack-dev-middleware

    • 包装器:它可以把 webpack 处理过的文件发送到 server,这是 webpack-dev-server 内部的原理,但是它也可以作为一个单独的包使用

optimization

  • runtimeChunk:

    • 'single', 表示创建一个单独的运行时文件,该文件将在所有 chunk 之间共享。这有助于缓存优化,因为如果应用的入口点不变,则运行时文件也不变,从而允许浏览器使用缓存版本,而不是每次都重新下载。
  • 每页有多个入口点是否可以使用多个入口点而无需重复模块 (如果你的应用程序每页有多个入口点,并且你希望这些入口点之间共享模块而不重复打包相同的依赖)

多个 chunks 只需要实例化一次 多个 chunks 所依赖的模块可以提取到多个页面使用的公共包中,但是有些模块在很少的页面使用,打包器可能会将他们内联到每个 chunks 中而不需要提取到共享包。 无论提取还是内联,esmodule 和 commonJS 都规定模块只能在每个 js 上下文实例化一次,保证模块的顶级范围是全局并在该模块的所有用法之间共享 实例化多次会为正确的代码引入错误或者效率低下

基本配置

loader 的执行顺序是从后往前的

  • 拆分、merge公共的配置

    使用 const {smart} = require('webpack-merge'); module.exports = smart(baseConfig, {})// baseConfig 为公共配置

  • 处理 ES6

    • 使用 loader:babel-loader,还需要配置.babeirc 文件

      
          "presets": ["@babel/preset-env"]
          "plugins": []
      
      ```
      
  • 处理样式 loader:css-loader,style-loader... 使用 postcss-loader 需要配置 postcss-config.js

  • 处理图片

  • 使用 loader:url-loader,图片小可以直接使用 base64,减少请求

  • output


output: {
filename: '[name].[contenthash:8].js',//动态名称,为入口名称
//使用 8 位的哈希字符串设置文件,文件内容没有更改哈希就不变,请求会命中缓存,加载更快
},

高级配置

  1. 生成多入口 html 并引入指定包:

entry:{
index: './src/index.js',
print: './src/print.js',
}
output:{
filename: '[name].[contentHash:8].js',//动态名称,为入口名称
}
//多个 html 需要 new 多个 HtmlWebpackPlugin
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',//自定义引用的 html 的模板路径
filename: 'index.html',//自定义 html 的文件名
chunks: ['index', 'print']//指定需要在 html 中引入的 chunks(包),不指定会把入口的 js 文件全部引入
})
]

  1. 抽离压缩 css 文件
  • 使用 loader:mini-css-extract-plugin 在 module 的 rule 里面使用,在 plugins 里面使用。 压缩需要配置

module: {
    rules: [
    {
        test: /\.css$/,
        use: [
            MiniCssExtractPlugin.loader,
            'css-loader',
            'postcss-loader'
            ]
    }]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].bundle.css',
})
]

optimization: {
runtimeChunk: 'single',//生成共享文件
minimizer: [
// 默认情况下会包含 TerserPlugin 来压缩 JS 文件
`...`,
new CssMinimizerPlugin(), // 添加 CSS 压缩插件
],
},

  1. css 文件并没有在入口文件中引入,而是直接在 html 引入,所以需要手动引入,使用'copy-webpack-plugin'

new CopyWebpackPlugin({
patterns: [
{ from: 'src/index.css', to: 'assets' },//to 默认指向打包路径这里会在 dist 下创建 assets 文件夹存储
],
})

  1. 抽离公共代码 抽离公共代码,使得文件修改后不会重新请求,而是使用缓存

optimization: {
//分割代码
    splitChunks: {
        chunks: 'all',
        /\*\*
        _ all: 所有的 chunks
        _ initial: 直接引入了的入口 chunks,不处理异步文件
        _ async: 只针对所有的异步 chunks
        _ function: 自定义函数,返回 true  false
        \*/
        cacheGroups: {
            //第三方模块
            vendor: {
            name: 'vendor',//chunk 名称
            priority: 1,//优先级,越大优先级越高
            test: /node_modules/,
            minSize: 0,//最小尺寸,默认 0
            minChunks: 1,//最少被几个 chunk 引用
        },
        // 公共模块
        common: {
            name: 'common',//chunk 名称
            priority: 0,//优先级
            minSize: 0,//最小尺寸,默认 0
            minChunks: 2,//最少被几个 chunk 引用
        }
      }
    }


  1. 懒加载 使用 import('文件路径').then(res => {}),webpack 会打包出一个单独的 js 文件,然后异步加载,不会阻塞页面的渲染

  2. 处理 jsx 在.babelrc 里面使用 使用'@babel/preset-react' 在 module 的 rules 会使用 loader 处理 jsx,匹配 jsx 文件,他会自动处理 jsx

  3. vue vue loader 在 module 的 rules 会使用 loader 处理 vue 的规则,匹配 vue 文件,使用 vue loader

ESModule 和 CommonJS

ESModule 静态引入,编译时引入 CommonJS 动态引入,执行时引入 静态分析才能实现 Tree-Shaking,执行时引入无法分析

babel

  • 只解析语法,将箭头函数,解构,类等转换为向后兼容的语法。不会自动处理新的内置对象或方法,如 Promise,Array.from 等

  • 用于将 ES6+代码转换为兼容向下得代码

  • 删除类型注释,不做类型检查

  1. 环境搭建
  2. .babelrc 配置
  3. presets 和 plugins

babel-runtime 和 babel-polyfill 的区别

babel-polyfill

旨在填补不同环境之间的兼容性差距

  • core-js 和 regenerator-runtime 的集合

说明:从 7.4 开始被弃用。

  • 它通过修改原生对象(Object.prototype)实现向后兼容,可能与其他库产生冲突。 Array.prototype.includes = function () {}//重新定义方法
  • 会引入整个 core-js 的 polyfills,无法按需引入,增大打包体积

如何按需引入

"presets": [
    [
      "@babel/preset-env",
      //添加如下代码
      {
        "useBuiltIns": "usage",
        "corejs": 3 //版本
      }
    ]
  ],

babel-runtime

用于支持模块化和非全局污染的 polyfill 和 helper 函数。它与 @babel/polyfill 不同,后者会全局修改原生对象(如 Array.prototype),而 babel-runtime 通过模块化的方式引入 polyfills 和 helpers,避免了全局污染。

核心概念

  1. 核心概念 (1) Helper Functions Babel 在编译代码时会生成一些辅助函数(helper functions),例如 _classCallCheck、_defineProperty 等。这些函数通常用于实现类、继承等特性。

    默认情况下,这些辅助函数会被重复嵌入到每个文件中。这会导致打包体积增大,尤其是在大型项目中。

(2) Polyfills 为了支持新的 JavaScript API(如 Promise、Map 等),需要引入 polyfills。babel-runtime 提供了一种方式,通过模块化的方式引入这些 polyfills,而不是全局污染。

如何产出一个 lib

配置 webpack 文件,output 的 library 可以定义库的全局名称。 使用 npm publish 发布到 npm。更新需要更新版本号 npm version patch # 更新补丁版本

  • 支持 ts 需要 webpack 去处理 ts 文件

  • 支持多平台 为了让库能够同时支持浏览器和 Node.js 环境,建议:

    设置 libraryTarget: 'umd'。 使用 globalObject: 'this' 来兼容不同环境的全局对象。

Webpack在项目中的配置与工作原理解析

Webpack配置项逐项解析

Entry(入口配置)

在Webpack中,entry指定了应用程序的入口文件或文件组,是构建依赖图的起点。本项目的Webpack配置采用了动态多入口的方式:通过 glob 工具搜索 app/pages 目录下所有匹配 entry.*.js 的文件。也就是说,每个页面子目录里如果存在形如 entry.页面名.js 的文件,就会被自动收集为一个入口。Webpack将为每个找到的入口文件创建一个命名的入口点,入口名通常取决于文件名(例如 entry.page1.js 会对应入口名 entry.page1 或简化为 page1)。

这个配置的作用:允许项目按页面模块化管理,不同页面有各自的打包入口。每个入口文件及其依赖会打包成独立的输出文件。这种配置让各页面的代码彼此隔离,减少了单次加载的体积,同时也方便按需加载和代码分割。对于每个找到的入口,配置里还自动生成了一个对应的 HTML 模板插件实例(使用 HtmlWebpackPlugin),用于生成该页面的 HTML 文件(本项目中是 .tpl 模板文件)。这样,每个页面入口都会有自己的输出 HTML,里面自动注入打包后的对应脚本。

Output(输出配置)

output 决定Webpack打包后文件的存放路径、文件名等。本项目对开发环境和生产环境分别设置了不同的输出配置:

  • 输出路径:在开发环境中(webpack.dev.js),输出被指定到 app/public/dist/dev/ 目录下,而生产环境(webpack.prod.js)输出到 app/public/dist/prod/ 目录。这样可以将开发构建和生产构建的文件分开存储,避免互相干扰。
  • 文件名格式:采用了占位符来确保文件名具有意义和缓存友好。比如 filename: 'js/[name]_[chunkhash:8].bundle.js'表示输出的文件名前缀为入口名称([name]),加上8位长度的chunkhash用于区分文件版本,再加上 .bundle.js 后缀,存放在 js/ 子目录中。chunkhash根据文件内容生成,能有效利用浏览器缓存(内容不变则hash不变)。生产模式下CSS也有类似命名,如 css/[name]_[contenthash:8].bundle.css
  • publicPath:指定了发布后静态资源的基础访问路径。例如开发环境下 publicPath: http://127.0.0.1:9002/public/dist/dev/,表示页面引用打包资源时,会以该URL为基础。这在开发使用webpack-dev-middleware时很重要,因为资源实际上由开发服务器(Express)通过该路径提供。在生产环境,publicPath被设为 '/dist/prod',表示静态文件部署在服务器相对路径 /dist/prod 下,供客户端访问。
  • 其他输出选项:例如设置了 crossOriginLoading: 'anonymous'来指定,意味着Webpack会将运行时代码拆分到独立的chunk(通常命名为runtime~*.js),从而实现更好的缓存与模块热替换管理。

Module(模块规则与Loader)

module.rules 定义了文件类型与对应处理 Loader 之间的映射。Webpack遇到不同类型的模块文件时,会根据这里的规则进行预处理。本项目的配置为各种常见资源设置了相应的loader:

  • Vue单文件组件 (.vue) :使用 vue-loader 处理。Vue Loader可以解析.vue文件,将其中的模板、脚本和样式部分拆解并分别交由其他loader或Vue编译器处理,使得Vue单文件组件可以被Webpack理解并打包。
  • JavaScript (.js) :使用 babel-loader 转译。规则中限定了 include: app/pages 路径,表示只对本项目源码部分(pages目录)应用Babel转换,跳过诸如node_modules的库以加快构建。Babel-loader会调用Babel将ES6/ES7等新语法转换为更广泛兼容的ES5语法,确保代码能在大多数浏览器运行。
  • 图片资源 (.png, .jpg, .gif等) :使用 url-loader。它会将较小的图片文件转为Base64编码内嵌到打包内容中(通过配置的 limit: 300 字节阈值),以减少HTTP请求数。超过限制的图片则自动调用file-loader方式输出为单独文件,并返回其路径。配置里还设置了 esModule: false以确保url-loader导出的模块采用CommonJS规范(这样可以和Vue等环境更好兼容,避免默认导出对象的问题)。
  • CSS 文件 (.css) :在开发环境中使用 style-loadercss-loadercss-loader解析CSS文件中的 @importurl() 等引用,处理依赖关系;而style-loader则将解析后的CSS通过动态创建标签插入到网页中,使样式生效。在生产环境中,会改用 MiniCssExtractPlugin.loader 取代 style-loader,以将CSS提取成独立文件,避免样式通过JS注入(提高性能并便于缓存)。
  • Less 文件 (.less) :规则为 less-loader + css-loader + style-loaderless-loader将Less预处理语言编译成CSS,然后再交由css-loader和style-loader处理。这与CSS类似,只是多了一步将Less转换为CSS。
  • 字体和SVG等资源 (.eot, .ttf, .woff2, .svg) :使用 file-loader。它会直接复制文件到输出目录,并把引用替换为最终的文件URL。这样在CSS/JS中引用字体和SVG时,最终构建输出对应的文件,并能正确加载。
  • 线程优化加载器:在生产配置中,引入了 thread-loader 来加速繁重的转换过程。例如对JS使用了线程池:先用thread-loader开多个工作线程,然后再交由babel-loader执行实际转译(在配置中,通过 HappyPack 或 thread-loader 实现)。thread-loader可以让耗时的Babel转译并行处理,充分利用多核CPU,加快打包速度。类似地,CSS处理也用了线程loader。需要注意,thread-loader要放在实际工作loader(如babel-loader)之前引入。项目中还曾配置HappyPack插件实现多线程,功能类似,也是将特定loader任务分发到子进程并行处理。总之,这些配置都是为了提高构建性能,对用户透明。

Resolve(模块解析配置)

resolve 配置影响Webpack如何寻找模块,以及对模块路径的简化。本项目的resolve设置主要包括两个部分:

  • extensions(自动扩展后缀) :设置为 ['.js', '.vue', '.less', '.css']。这意味着当你在代码中导入模块时,如果没有写文件扩展名,Webpack会按照这个顺序尝试添加这些扩展去寻找文件。例如,import Foo from '@/common/utils',Webpack会尝试补全为 utils.js, utils.vue, utils.less, utils.css 等,直到找到存在的文件。这减少了书写文件名后缀的麻烦。

  • alias(路径别名) :定义了一些快捷路径映射。比如:

    • $pages 被映射为项目的 app/pages 目录,
    • $common$widgets 都映射为 app/pages/common (可能是不同语义的同一路径),
    • $store 映射为 app/pages/store

    有了这些别名,在源码中可以使用诸如 import stuff from '$common/utils' 这种写法,Webpack会将其解析为实际的绝对路径app/pages/common/utils.js。这样做提高了可读性,也避免了出现很多相对路径(例如 ../../../common/utils)的情况。

Plugins(插件配置)

Webpack插件可以在构建生命周期的不同阶段介入,扩展Webpack功能。本项目使用了多种插件来实现特定功能:

  • VueLoaderPlugin:这是处理.vue文件所必需的插件。Vue Loader本身在处理Vue单文件组件时需要该插件配合,以正确解析组件内的模板和样式部分并生成相应模块。这插件无需配置选项,只需在plugins数组中实例化一次即可。

  • HtmlWebpackPlugin:用于为打包输出生成HTML文件的插件。本项目对每个入口都push了一个HtmlWebpackPlugin实例。配置里指定了模板文件 app/view/entry.tpl 作为基础模板,以及输出文件名为 app/public/dist/${entryName}.tpl。插件会在Webpack构建完成后自动往模板中插入对应入口的

  • webpack.ProvidePlugin:提供全局变量的插件。在配置中,将 Vueaxios_(lodash的变量名)映射到相应的模块。这意味着在项目源码中,可以直接使用 Vueaxios_ 这些变量而不用每次import Vue from 'vue'等。Webpack遇到这些未定义的全局变量时,会自动帮你引入对应的库模块。这对使用第三方库(比如在很多文件里都用到lodash或Vue实例)非常方便,避免重复引用。

  • webpack.DefinePlugin:定义编译时常量的插件。在配置中,用它定义了一些全局常量,例如:

    • _VUE_OPTION_API_: 'true',启用Vue的选项式API支持,
    • _VUE_PROD_DEVTOOLS_: 'false',禁用Vue生产模式下的DevTools支持,
    • _VUE_PROD_HYDRATION_MISMATCH_DETTAILS_: 'false',禁用Vue生产环境下关于服务端渲染水合不匹配的详细提示。

    这些常量通常是给框架或库读取的,用于按环境开启/关闭某些功能。DefinePlugin会在编译阶段直接替换代码中出现的对应标识为给定的值(注意这里是直接文本替换,如果是字符串需要再加引号)。此外,常用的 process.env.NODE_ENV 也可通过DefinePlugin设置为 "production""development",以便前端代码中能根据环境执行不同逻辑。

  • webpack.HotModuleReplacementPlugin:热模块替换插件,仅在开发环境使用。它用于启用Webpack的HMR特性,使应用运行中可以实时替换更新过的模块而无需整页刷新。配置项 multiStep: false 表示不使用多步热更新(一般保持默认即可)。有了它,开发时修改代码,浏览器中只会更新变动的模块部分,状态不丢失,提升开发效率。需要注意,HMR插件需要配合HMR客户端代码和服务器端middleware共同工作(后续详述)。

  • CleanWebpackPlugin:清理输出目录的插件,在生产构建中使用。每次执行生产构建前,它会删除上一次构建留下的旧文件。配置中指定清理 app/public/dist 目录下的内容。这样可避免旧的无用文件堆积,并确保每次部署只包含最新的资源。

  • MiniCssExtractPlugin:CSS提取插件,用于生产环境。它会将原本由style-loader内嵌的CSS提取成单独的 .css 文件。结合它的loader(MiniCssExtractPlugin.loader),在打包时CSS不再通过JS插入页面,而是作为静态文件链接。在配置中指定了输出的CSS文件名格式 css/[name]_[contenthash:8].bundle.css,并会为按需加载的CSS生成 chunkFilename(同样使用contenthash)。这样CSS也可以独立缓存,并行加载,减少页面渲染阻塞。

  • CSSMinimizerPlugin:CSS压缩插件(基于cssnano),用于生产环境。Webpack5+默认会对JS压缩,而CSS需要单独配置这个插件。它会在构建优化阶段对生成的CSS文件进行代码压缩、去除空白和注释等优化,减小文件体积。

  • TerserWebpackPlugin:JS压缩混淆插件(Webpack默认的压缩器),在生产优化中启用。配置中设置了 drop_console: true,意味着会移除所有console.*语句,以减少不必要的日志输出并优化代码体积。同时开启了并行(parallel:true)和缓存(cache:true),充分利用多核CPU加快压缩速度,并缓存结果避免重复压缩相同内容。

  • HtmlWebpackInjectAttributesPlugin:一个用于给HtmlWebpackPlugin生成的标签添加属性的插件。本项目将其用于在输出的HTML模板中为所有 属性。这样浏览器加载这些静态资源时不会附带用户cookies等凭证,也便于错误追踪(例如配合Sentry可以获取跨域脚本的具体报错)。这个插件在HtmlWebpackPlugin生成HTML之后、写入文件之前执行,自动遍历标签进行属性注入,无需手工修改模板。

  • HappyPack:虽然在最终配置中并未通过module.rules直接使用HappyPack的loader(相关规则被注释掉了),但仍然初始化了两个HappyPack插件实例用于示例。一份用于多线程处理JS(配置了babel-loader及preset),一份用于多线程处理CSS(配置了css-loader)。HappyPack的作用与thread-loader类似,都是为了并行处理来加速打包。在本项目中可能是出于演示或兼容目的保留,但主要的并行处理已经通过thread-loader实现。了解HappyPack有助于理解Webpack构建提速的原理:它通过建立worker池,让多个文件的转换同时进行,而不是单线程依次执行。

DevServer(开发服务器配置)

Webpack通常可以通过devServer字段配置开发服务器(webpack-dev-server),如指定端口、启用HMR、设置代理等。然而本项目并未使用webpack-dev-server自带的devServer配置项,而是选择了自定义的Express服务器结合中间件的方式来实现相同功能。这种方式下,devServer字段在Webpack配置中实际上是不存在的,取而代之的是手写的服务器脚本。

在webpack.dev.js中,可以看到定义了一个DEV_SERVER_CONFIG对象,包含开发服务的HOST、端口、HMR路径等信息。随后,在 app/webpack/dev.js 脚本中,使用Express启动了一个服务器并结合 webpack-dev-middlewarewebpack-hot-middleware 来提供开发服务。简要来说:

  • Express服务器监听在配置的HOST和PORT上(默认127.0.0.1:9002)。
  • 使用 webpack-dev-middleware 将Webpack编译器挂载到Express上。它会实时监听文件变动、执行Webpack编译,并将打包后的文件暂存于内存中供访问。配置了 publicPath 来对应Webpack输出的publicPath,确保请求路径匹配。还设置了 writeToDisk: (filePath) => filePath.endsWith('.tpl'),表示只有 .tpl 结尾的文件会写入磁盘。这样做的原因是:我们的页面模板需要实际存在文件(供Koa去渲染),而JS/CSS等资源可以仅存在于内存提高构建速度。
  • 使用 webpack-hot-middleware 实现HMR的实时通讯。它通过配置的HMR路径(如/__webpack_hmr)建立与浏览器的长连接,当有模块更新时通知客户端进行热更新。项目中将其log设为空函数关闭了默认日志输出,使控制台更清爽。

虽然这里没有直接使用devServer字段,但作用是等价的:设定开发服务器的主机和端口热更新等。典型webpack-dev-server配置中,如果用了devServer, 可能会有:

devServer: {
  contentBase: path.join(__dirname, 'dist'),
  port: 9002,
  hot: true,
  open: true,
  proxy: { ... }
}

而本项目采用手动方式,所有这些配置通过代码实现。例如HMR这里就是通过在每个入口里注入 webpack-hot-middleware/client脚本并启用HotModuleReplacementPlugin实现的。对于学习者来说,devServer的核心工作无非是启动一个本地服务、实时重新编译、推送更新到浏览器。本项目展示了如何不用webpack-dev-server也能达到同样效果,这对于定制更复杂的开发流程是有益的。

Webpack整体执行流程

Webpack的工作可以分为开发模式下的实时编译流程和生产模式下的一次性打包流程。下面我们从运行npm run dev(开发)和npm run build:prod(生产构建)两个场景,描述Webpack从启动到生成输出的全过程。

开发环境下的流程(npm run dev

  1. 启动命令:开发时使用 npm run dev。根据package.json,这会设置环境变量_ENV=local并启动nodemon ./index.jsnodemon用于监听文件变化自动重启,但更重要的是它启动了项目的Koa服务器。与此同时,开发者还需要在另一个终端运行npm run build:dev 来启动Webpack的编译服务(或在项目中修改为在Koa启动时自动引入Webpack中间件)。

  2. 初始化Koa服务器index.js 调用自定义框架的 ElpisCore.start() 方法启动Koa应用。Koa加载各种中间件和路由(controllers等),其中包括渲染页面的控制器。此时Koa本身并不会构建前端资源,但它已经准备好在有人访问时提供接口数据或渲染模板。

  3. 启动Webpack开发服务:另一边,运行npm run build:dev 实际执行的是 node ./app/webpack/dev.js。这个脚本通过Express启动了Webpack开发服务器,并使用Webpack Dev Middleware编译前端代码。脚本内做了如下事情:

    • 加载Webpack配置(webpack.dev.js合并了webpack.base.js)并调用 webpack(webpackConfig) 创建编译器。
    • 为每个入口注入HMR客户端脚本,使浏览器能够接收热更新通知。
    • 使用 devMiddleware(compiler, {...}) 启动编译器的监听,指定输出publicPath和写入规则等。Webpack此时开始根据配置 编译构建 项目:解析入口、递归解析依赖模块、应用loader转换代码、打包模块为bundle、拆分代码块、等等。首次编译完成后,所有打包产物(包括各页面的JS、CSS和对应生成的.tpl模板)会存放在内存的文件系统中。由于配置了writeToDisk规则,.tpl文件会同步写到实际磁盘上供Koa使用。
    • 使用 hotMiddleware(compiler, {...}) 建立HMR连接。这样,当Webpack侦测到源代码改动并重新编译出增量更新时,会通过hot-middleware将更新消息推送给客户端浏览器。
    • Express服务器开始监听9002端口,等待浏览器请求静态资源。
  4. 浏览器访问与文件提供:当用户在浏览器访问应用时,例如访问 http://localhost:7001/page1(假设Koa监听在7001端口并路由到ViewController的render逻辑),Koa服务器会调用 ctx.render('dist/entry.page1', data)去渲染对应的页面模板。由于在开发编译时已经生成了最新的 entry.page1.tpl 并写入了 app/public/dist/entry.page1.tpl,Koa的视图引擎能找到这个模板文件,并插入相应数据后返回HTML给浏览器。

  5. 加载打包资源:浏览器收到HTML后,其中的引用脚本会指向类似 http://127.0.0.1:9002/public/dist/dev/js/page1_<hash>.bundle.js 这样的路径(因为HtmlWebpackPlugin模板里用的是publicPath指向9002)。于是浏览器向Webpack的开发服务器请求这些资源。Express的静态和devMiddleware会截获这些请求,从内存中返回对应的JS/CSS文件内容。由于使用了source-map,在开发工具中还能看到映射到源代码的调试信息,方便排错(devtool配置为eval-cheap-module-source-map)。

  6. 热更新循环:此后,如果开发者修改了前端代码(JS/Vue等文件),Webpack的devMiddleware会检测到变更,触发一次增量编译。Webpack根据改动的模块重新构建相应的模块和受影响的chunk,生成热更新补丁文件。在编译完成后,hotMiddleware通过长连接向浏览器推送更新信号。浏览器端的HMR客户端(此前插入的webpack-hot-middleware/client脚本)接收到通知,利用HotModuleReplacementPlugin的API动态获取更新模块并替换掉旧的模块。页面无需刷新,即可实时更新内容。这一过程对开发者是透明的,只会看到应用迅速地反映出代码修改结果。

整个开发流程可以概括为:启动两个服务器(Koa应用服务器 + Webpack构建服务器)协同工作。Koa负责业务逻辑和渲染模板,Webpack服务器负责实时编译和提供静态资源及热更新。最终效果是在开发时,用户访问得到的是最新编译的前端代码,而且可以在不刷新页面的情况下看到代码改动。

生产环境下的流程(npm run build:prod

  1. 启动构建:执行 npm run build:prod 时,脚本会运行 node ./app/webpack/prod.js。不像开发模式有持续的server,这个过程是一次性地运行Webpack进行构建。控制台会打印 “building...” 提示,表示开始构建。

  2. 加载配置并编译:prod.js 脚本载入了生产模式的Webpack配置(webpack.prod.js,内部已merge基础配置)并调用 webpack(webpackConfig, callback) 来执行编译。一旦开始:

    • CleanWebpackPlugin 首先清空之前的 app/public/dist 输出目录内容,确保旧文件不影响本次构建结果。
    • Webpack根据入口配置收集所有入口文件及其依赖模块。
    • 针对每个模块文件类型,按module.rules应用相应的loader进行转换,例如.vue -> 先经vue-loader处理, .js -> babel-loader转译, .less -> 编译成CSS等。因为是生产模式,CSS会被提取、JS会做压缩,所以此阶段会协调各插件一起工作:MiniCssExtractPlugin的loader提取CSS文本、HappyPack/thread-loader让Babel等转换多线程执行、VueLoaderPlugin处理.vue输出等等。
    • Webpack将处理后的模块根据入口和代码分割策略进行打包,生成若干chunk。比如每个页面入口会生成一个对应的chunk,另外还有可能把公用依赖拆分出的 vendorscommon chunk,以及运行时的 runtime chunk。
    • 进入优化阶段:Webpack会并行调用TerserPlugin压缩JS和CSSMinimizerPlugin压缩CSS。Terser根据配置丢掉了console语句并混淆压缩代码,CSSMinimizer则去除冗余优化CSS。
    • HtmlWebpackPlugin在所有chunk确定后,根据模板为每个入口生成最终的HTML文件(.tpl)。HtmlWebpackInjectAttributesPlugin随后给这些HTML里的资源标签加上必要的属性,如crossorigin。
  3. 输出文件:当编译完成并经过各种优化后,Webpack将把各个输出文件写入到 app/public/dist/prod/ 目录下。包括:

    • 每个入口对应的 .bundle.js 主文件,以及按需分出的 vendors.bundle.jscommon.bundle.js 等公共chunk文件,和一个 runtime~*.js 运行时文件(如果有启用runtimeChunk)。
    • 提取出来的 .bundle.css 样式文件(如果有CSS)。
    • 为每个页面生成的 .tpl 模板文件(例如 entry.page1.tpl),其中已经引用了以上输出的JS/CSS文件。
  4. 构建结果验证:prod.js 脚本在回调中输出了本次构建的统计信息stats。这包括打包生成了哪些文件、文件大小、耗时等概要(配置中设定了不显示过多模块细节,只关注总览)。如果有错误也会在这里体现。一切正常则提示构建成功。

  5. 部署与运行:生产构建完成后,一般就可以将 app/public/dist/prod 目录部署为静态资源目录。项目的服务器(通过 npm run prod 启动Koa,设置_ENV=production)会上线运行。Koa在生产模式下不会启用webpack-dev-middleware,而是直接使用打包好的模板和文件。用户访问某页面时,Koa的控制器会渲染 dist/entry.xxx.tpl 模板,返回包含版本化资源引用的HTML。浏览器再去请求这些JS/CSS静态文件(通常由静态服务器或Koa的静态中间件提供),最终呈现出页面。因为文件都已经压缩和带有hash,用户加载速度和缓存效率都显著提升。

总结来看,Webpack生产构建是一次性、严格优化输出的过程,从清理旧文件到生成新文件,全程自动化完成。开发构建则是持续监听、快速反馈的过程,为开发提供便利。两者使用了同一个基础配置,但通过不同的mode和插件组合,实现了截然不同的运行机制。

项目中Webpack的实际应用分析

以上我们解析了配置项和流程,下面结合本项目的细节,深入说明Webpack在此项目中的一些关键应用点:代码分包策略、热更新机制、所用Loaders和Plugins各自的作用原理。

代码分包与SplitChunks实现

本项目非常关注对**代码分割(Code Splitting)**的处理,以实现更高效的加载。Webpack提供了optimization.splitChunks配置用于自动分割chunk,本项目利用了这一功能:

  • 拆分策略:配置中设置 chunks: 'all',意味着无论同步或异步加载的模块,只要满足条件都进行分割。这保证即使初始加载时依赖的包,或动态import的包,都可能被提取到独立chunk。

  • 缓存组:定义了两个主要的缓存组(cacheGroups)用于分包:

    • vendors组:匹配所有来自node_modules的第三方库代码的一个chunk。通过这样做,所有页面共享的第三方库只需加载一次,而且这些库变动频率低,可以长时间缓存。priority: -10略高于默认值,enforce: true确保即便模块较小也强制分离。
    • common组:匹配项目中被多次引用的通用模块。条件设置为 minChunks: 2(至少被两个入口引用)且 minSize: 1(大小至少1字节,实际上任意非空模块都行),符合即提取。这样那些在不同页面入口间共享的业务代码(比如工具函数、公共组件)会打包到一个独立的“common”chunk。reuseExistingChunk: true允许重用已有的chunk,避免重复打包。
  • 效果:经过上述策略,打包输出时会多出文件如 vendors~*.jscommon~*.js(具体名称可能带hash)。页面在引用时,通过HtmlWebpackPlugin已经自动加上了这些公共chunk的让各页面只需加载各自独有的代码 + 公共依赖,大大减少了总体冗余。

  • runtimeChunk:此外,optimization.runtimeChunk: true也开启了将Webpack运行时拆分。Webpack运行时代码包括模块加载逻辑、HMR管理等,通常很小但每次构建可能会变化。如果将其内联或放在主bundle,会导致主文件hash频繁变动,不利于缓存。分离runtime可以使主代码更纯粹,只有真正业务代码变动才会影响其hash。此外在HMR场景,runtimeChunk也能更好地管理模块更新记录。启用后,输出一个runtime.js(名称视Webpack版本而定),由HTML引用。这样runtime本身也能被浏览器缓存,并隔离更新影响。

通过SplitChunks,本项目实现了按来源和复用频率进行模块拆分:第三方库、业务公共模块各自成包,最大程度实现复用与缓存优化。这对非单页的多入口应用尤其重要,每个页面初始加载更轻量,而公共部分后台统筹。

热模块替换(HMR)的启用与配置原理

在开发阶段,本项目启用了**热模块替换(Hot Module Replacement, HMR)**功能,以提升开发体验。虽然没有使用webpack-dev-server自带的简易配置,但通过组合中间件手动实现了HMR,其原理如下:

  • HMR客户端注入:在Webpack开发配置合并时,代码遍历了所有入口并为每个入口数组添加了 webpack-hot-middleware/client。这个特殊入口会在浏览器端运行,建立与开发服务器的连接(通过EventSource长连接或WebSocket),监听服务器推送的更新通知。加入 ?path=http://127.0.0.1:9002/__webpack_hmr&reload=true 参数表明客户端从指定的HMR路径订阅更新,并在无法热替换时回退到自动刷新页面。
  • 启用HMR插件:Webpack配置中包含了 HotModuleReplacementPlugin。没有这个插件,即使注入了HMR客户端也无法真正应用更新。HMR插件会在编译过程中为可以热更新的模块添加特殊标记,并拦截模块变化事件,从而在运行时执行替换逻辑。它还会影响bundle的输出,加入HMR所需的元数据(如每个模块的ID、依赖关系,以便动态查找更新模块)。
  • Dev Middleware 与 Hot Middleware:Express服务器一侧,webpack-dev-middleware负责监听文件改动并触发Webpack增量编译。当编译产生了新的模块代码块(hot update chunk)时,不会像正常刷新那样输出完整文件,而是生成补丁包。随后,webpack-hot-middleware感知到编译完成,通过前面提到的长连接向浏览器发送更新信号。它指定了路径 /__webpack_hmr 供客户端监听,并将变更信息以流的形式推送。
  • 模块接收更新:浏览器端,注入的HMR客户端脚本接收到通知后,会进一步调用Webpack HMR API。具体来说,对于发生变化的模块,Webpack会尝试调用该模块内部的module.hot.accept钩子(如果模块代码中有定义,通常框架会帮我们隐藏处理,例如Vue单文件组件由vue-loader自动接管HMR),或者向上冒泡到父模块。如果模块能安全替换,Webpack将把新的模块代码直接替换旧模块并运行新模块导出的内容,而应用状态(例如页面中已经渲染的部分)保持不变。比如修改了Vue组件的模板,HMR会直接更新组件的渲染函数,Vue会只重新渲染变化的部分。
  • 无法热更新的处理:如果某个改动模块无法安全地进行HMR(比如模块没有做HMR处理,或是意外错误),由于我们在URL参数中指定了reload=true,hot-middleware客户端会在HMR失败时执行一次整页刷新,以保证应用不会处于不一致状态。这相当于回退方案,确保开发调试至少能拿到最新代码。

小结:通过上述机制,开发时的每次保存代码文件,都触发Webpack编译并增量地把更新发送到前端应用,做到界面实时刷新而又保留先前状态。对于样式修改,style-loader本身支持HMR,能直接替换标签内容;对于Vue组件,vue-loader编译出的模块也支持HMR接口。所以开发者会感受到修改样式、修改组件模板/脚本,页面立即局部更新,非常高效。需要注意HMR仅用于开发,生产环境下没有注入相关代码,也不会建立这样的连接。

构建中使用的Loaders及作用

本项目用了多种Loader,它们各司其职地在Webpack构建流程中转换源码。在整个构建过程中,Webpack遇到特定类型的文件就会按规则链式调用相应loader,对文件内容进行编译或处理。以下是项目中用到的主要Loader及它们的作用,用通俗的话来说:

  • Babel Loader (babel-loader) :把高级的JavaScript语法转换为向后兼容的版本。开发者可以用ES6+/ES7等新特性写代码,Babel-loader会调用Babel编译器,根据预设(preset-env)将其转成大多数浏览器能识别的旧语法(比如箭头函数变普通函数,Promise变成基于Polyfill的实现等)。简单讲,它是代码的“翻译官”,翻译成“所有浏览器都看得懂”的语言。
  • Vue Loader (vue-loader) :专门处理.vue单文件组件。Vue单文件里可能写了模板、脚本、样式,格式特殊,浏览器不直接支持。vue-loader接管这些.vue文件,把里面的内容拆分出来:模板部分类似转换成渲染函数的JS代码,部分交给相应的CSS预处理loader,部分当普通JS对待。最终输出一个标准的JS模块,导出Vue组件选项对象。可以说,vue-loader让Webpack“认识”Vue组件文件,使开发者能够以单文件组件形式组织代码。
  • CSS Loader (css-loader) :让Webpack能够解析CSS文件中的内容。当遇到@import "other.css"url('image.png')这样的语法时,css-loader会帮忙处理这些依赖,把它们当作模块看待。它输出的其实是处理后的CSS字符串,供后续loader或插件使用。通俗地说,它把CSS变成了一段可以被JavaScript使用的模块,并解析了其中引用的其他资源路径。
  • Style Loader (style-loader) :在开发模式下,style-loader接上css-loader的输出,会动态地往HTML文档里插入一个<style>标签,把CSS字符串塞进去,让样式生效。也就是说,它把CSS“挂”到网页上去。这种方式适合开发调试,样式热更新迅速。但是在生产环境我们会改用MiniCssExtractPlugin把CSS拆出来,因为大量标签会降低性能且无法缓存。
  • Less Loader (less-loader) :把Less代码编译成普通CSS。Less是一种CSS预处理语言,支持变量、嵌套等特性。less-loader就像一个翻译,将Less文件转换为了CSS文本,然后交由css-loader处理。开发者因此可以用更简洁的Less语法写样式,最终仍旧得到浏览器可理解的CSS。
  • URL Loader (url-loader) :处理图片和字体等二进制文件的小帮手。它的策略是对于小文件直接读入并转成Base64编码的字符串,嵌入到打包内容里;对于超过设定大小的文件,则交给file-loader处理成单独文件。这样页面上很多小图标、背景图等可以直接内联,减少请求数,而大的图片仍独立加载以免主bundle过大。项目中配置的limit=300字节,说明非常小的图片才内联,大部分稍大的图片还是会独立成文件。无论哪种情况,url-loader都会返回一个可以在代码中使用的资源路径:对小图是一个data URI,对大图是发布后文件的路径。
  • File Loader (file-loader) :几乎所有非代码资源都可以用file-loader处理,包括图片、字体、媒体等。它的作用很直接:“接过”该文件,然后输出到指定的输出目录,并给出一个路径。这路径通常是根据文件内容hash或名称生成的,确保引用正确。url-loader在超过大小时实际上就是调用file-loader来完成工作的。在项目中,字体文件和SVG就是用file-loader输出的。简单说,如果把Webpack比作打包工厂,file-loader就是库管+搬运工,把源文件搬到输出仓库,并告诉其他模块“你需要的东西在那里,去拿吧”。
  • Thread Loader (thread-loader) :这是一个帮助其他loader提速的“多线程助手”。单个loader(例如Babel)处理大量文件时可能很慢,thread-loader会在它前面启动多个工作线程,把即将处理的文件分摊到不同线程中并行执行后续loader。项目在生产配置中就在babel-loader和css-loader前用了thread-loader,并指定了线程数量(通常等于CPU核心数)等参数。打个比方,本来100份活儿一个人干,现在开4个线程4个人一起干,每人25份,效率就上去了。线程池会管理这些工作,空闲超时还会自动回收线程以免资源浪费。对开发者而言,这一切都是幕后进行,只是构建速度变快了。
  • HappyPack Loader:本质上作用类似于thread-loader,只不过配置和使用方式略有不同。HappyPack需要把实际的loader配置写在它的loaders选项里,并在rules中用happypack/loader?id=...代替原本的loader。项目曾经配置过HappyPack用于JS和CSS的处理(id为'js'和'css'),指定了babel-loader和css-loader等。但是在最终rules里并没有启用这些HappyPack loader(被注释掉了),可能开发者改用了thread-loader直接简化处理。不管怎样,了解HappyPack有助于理解:它通过子进程池并行执行loader任务,实现和thread-loader类似的效果。两者都是为了解决Webpack构建瓶颈,让多核CPU充分运转起来。

总体来说,Loaders就像Webpack的“翻译和处理工” ,把各种类型的源文件转换为可以被Webpack捆绑的模块。其中有的翻译代码(Babel把高级JS翻译成低级JS,vue-loader把.vue翻译成JS模块),有的处理资源(url/file-loader搬运文件,style-loader把CSS塞进页面)。它们串联起来,使Webpack可以把不同格式的内容都统一处理打包。这些Loader大部分只在构建时运行,对最终产出的代码体积和运行性能没有直接影响(除了babel会影响代码形式),但是对开发体验、代码组织非常有帮助,让开发者可以自由使用高级语法和模块化方式,而无需手动转换。

构建中使用的Plugins及其作用与触发时机

Webpack插件体系为构建流程提供了高度的可扩展性。插件可以在Webpack运行过程的不同阶段介入,执行特定的任务或修改输出。本项目用到的插件我们在前文已罗列,这里从它们在构建生命周期中的作用和时机来做个通俗总结:

  • HtmlWebpackPlugin:在Webpack完成所有模块和chunk的处理后,进入生成文件(asset)阶段时运行。它为每个入口根据指定的模板生成了HTML文件(在本项目中是.tpl)并自动插入对应的 等标签,然后输出文件。在插件的生命周期钩子上,这是在emit阶段(即将写入输出目录前)完成的。对开发者而言,它省去了手动维护HTML引用的麻烦,保证引用准确无遗漏。
  • HtmlWebpackInjectAttributesPlugin:这个插件紧随HtmlWebpackPlugin之后工作。当HTML内容已经生成但尚未写出时,它介入遍历所有标签节点,加上我们需要的属性如 crossorigin="anonymous"。触发时机也是在emit阶段,但优先级在HtmlWebpackPlugin生成内容之后。这样保证属性正确地出现在最终写入的HTML文件里。它的作用对最终用户来说是静默的,但对安全和跨域请求有影响:通过anonymous属性,浏览器在请求这些静态资源时不会附带cookies,也允许JS跨域错误捕获(如结合CORS headers)。换句话说,它帮我们最后润色了一下输出的HTML文件。
  • ProvidePlugin:这个插件实质在编译阶段发挥作用。当Webpack解析每个模块的源码时,ProvidePlugin会检查其中用到的全局变量标识符(如Vue、axios等),如果发现对应配置了ProvidePlugin,它就自动在模块头部插入require('vue')等代码。比如某个文件中直接用了Vue.component(...)却没有import,ProvidePlugin会确保Webpack不报错并自动把Vue模块提供给它。这个过程发生在模块编译的解析阶段,属于加载前的准备工作。对最终打包输出,它不额外生成文件,只是影响模块内容。所以可以说ProvidePlugin是在幕后默默地“提供变量”,让我们在源码里少写很多import声明,属于构建时优化开发体验的手段。
  • DefinePlugin:DefinePlugin也是在编译阶段执行,它通过文本替换的方式注入常量。Webpack打包每个模块时,会查找代码中出现的特定标识符并用定义的值替换。例如代码里有 if (_VUE_OPTION_API_) { ... },在编译后就直接变成了 if (true) { ... }(因为配置中_define了_VUE_OPTION_API_为'true')。这些替换在源码转成AST语法树时进行,甚至可以结合Uglify/Terser在后续优化中删掉永远不会执行的分支。DefinePlugin的触发时机可以认为是每个模块源码处理的时候。它影响的是输出代码本身(嵌入不同的值),典型用例还有设置process.env.NODE_ENV。总之,这是一个编译期的“全局开关”插件,打包结果中不会保留原来的标识符,而是替换成具体的值。
  • HotModuleReplacementPlugin:HMR插件有点特殊,它既在编译阶段影响输出,又在运行时参与热更新流程。编译时,它为每个模块注入HMR相关的钩子代码(如检查模块的module.hot.accept调用,添加HMR标记等),并确保Webpack输出热更新所需的manifest和补丁chunk。当启用HMR插件时,Webpack会生成额外的 Hot Update 文件(.hot-update.json和.hot-update.js),这些都是HMR插件促成的结果。运行时,当dev服务器检测到文件变更,它也协助协调客户端的模块替换。所以可以认为HMR插件贯穿了编译->运行的周期:编译时为HMR做好准备,运行时真正执行模块热替换。如果没有它,webpack-dev-middleware侦测变化后只能回退到整页刷新。
  • CleanWebpackPlugin:此插件最先执行,基本在编译开始前就运行。当我们启动webpack( )开始构建,还未读入新的模块时,CleanWebpackPlugin按照配置把目标文件夹(如dist目录)内容删除。这是一个构建前置步骤,确保接下来输出时目录是干净的。它通过Node文件操作同步地清理指定路径,所以触发时机就是在Webpack准备输出文件前的hooks上。在实际运行中,你会发现每次构建开始控制台首先输出clean的操作日志(如果verbose:true的话),然后才继续编译模块。
  • MiniCssExtractPlugin:这个插件在编译阶段和输出阶段都各有动作。编译阶段,它配合其loader捕获到所有模块中的CSS代码段,将它们从模块的JS中抽离出来,暂存成独立的CSS文件块。输出阶段,它根据chunk把收集的CSS内容生成最终的.css文件并写入输出目录。可以说,它接管了CSS模块的输出。当Webpack处理到CSS模块时,原本style-loader会把CSS变成JS字符串注入,这时被MiniCssExtractPlugin拦截改为提取流程。所以在生成文件时就会出现.css文件。这插件典型触发点是在optimize assets过程中,将CSS作为asset输出。对于开发者,它的存在是感觉不到的,只是最终多出了CSS文件,但对用户来说好处是CSS以形式加载,避免JS长任务,并可以并行加载和缓存。
  • CSSMinimizerPlugin:这个插件在**优化优化阶段(Optimize Chunk Assets)**运行,具体在Webpack完成模块组装、开始优化输出文件的时候。它遍历所有产出的CSS文件,对每一个应用CSS nano等优化算法进行压缩。触发时机通常是在Webpack内部的 optimizeAssets 钩子。在此之后,CSS文件就变成压缩过的版本再写出。对开发者来说,这发生在构建末尾,看不到,但输出的CSS体积已经明显减小了。这种压缩不改变功能,只是去掉空格、注释、缩短颜色代码等,对浏览器透明。
  • TerserWebpackPlugin:类似地,TerserPlugin在优化阶段针对JS文件执行。Webpack在production模式默认会使用TerserPlugin,对每个JS chunk文件进行AST解析、变量名混淆、删除多余代码等压缩动作。项目里特别配置了parallel和drop_console,这些在插件初始化时设定,一旦进入压缩阶段,每个JS文件的压缩都会并行处理并执行移除console的额外步骤。它的运行顺序通常在loader处理、代码分割结束后开始,对最终要输出的JS代码进行处理。因此可以理解为最后一步深加工。执行完毕,JS代码就定型了,Webpack随后把它们写入文件系统。
  • HtmlWebpackPluginList (...HtmlWebpackPlugin) :这个并不是单一插件,而是根据每个页面入口push的一组HtmlWebpackPlugin实例。它们在构建流程中的触发顺序可能彼此并行,但总的来说,每当一个编译完成(emit前),各HtmlWebpackPlugin依次执行生成各自页面。这些插件实例并没有彼此依赖,但要确保在assets确定后才能正确插入引用。Webpack会在compilation优化完调用HtmlWebpackPlugin,为每个入口生成HTML。所以触发时机是编译接近尾声,在优化和chunk生成都结束后。由于项目有多个页面入口,实际上会生成多个模板文件(entry.page1.tpl, entry.page2.tpl等),但HtmlWebpackPlugin让这一切自动完成。每个实例的生命周期包括:读取模板 -> 插入资源列表 -> 输出文件。这一系列发生在构建流程的尾部,但在写入磁盘前完成。
  • HappyPack:HappyPack插件本身在构建一开始就启动,创建其内部的线程池(根据配置的cpu数量)。如果有使用HappyPack loader的规则,它会在loader执行阶段拦截,让任务交给它的线程去做。但由于本项目最终没有启用对应的happypack/loader(规则被注释),HappyPack插件实际上没有派上用场。一般而言,HappyPack在compile阶段监控特定类型文件的解析,当有文件匹配时,就把文件内容和loader发送到子线程处理。等子线程处理完,再将结果返回主线程,Webpack继续打包。这种插件运行时机算是与loader并行,但它本身是个管理者。因为这里没真正用,所以构建过程中它只是初始化了并未实际处理模块。
  • webpack-dev-middleware & webpack-hot-middleware(非Webpack自带插件) :虽然不是通过Webpack配置中的plugins字段引入的,但值得一提它们在开发流程的作用。这两个中间件不是Webpack内部插件,而是外部Express中使用的库。不过它们通过Compiler API与Webpack深度交互:dev-middleware在每次编译后接管输出,将文件存在内存并向Express提供;hot-middleware监听Compiler的done事件以获取更新信息,然后触发HMR流程。可以把它们看成是在Webpack和开发服务器之间架起桥梁的“插件”。它们的触发显然是在开发模式下,每次rebuild完成的时候,各自完成相应职责。

通过以上分析可以看出,每个插件都有特定的“介入点”:

  • 有的在编译前后(如CleanWebpackPlugin前置清理,HtmlWebpackPlugin尾部生成);
  • 有的贯穿编译全过程(如HMR、DefinePlugin始终影响模块处理);
  • 有的纯粹在输出环节(如压缩类插件,属性注入插件)。

它们共同辅助Webpack将源代码转化为最终产品:既提高了构建效率(多线程、自动刷新)、又优化了构建结果(分包、压缩、提取CSS)、还方便了开发使用(全局变量提供、自动生成HTML)。对于非专业人士,可以将Webpack想象成一个流水线工厂:配置里面的Loader是不同工序的机器,Plugin则是工厂里的“智能管家”和“助手”,在关键节点上协助或改变生产流程。经过这一系列流水线作业,我们的原始源码材料被加工成适合发布的成果。这就是Webpack在本项目中的工作原理和配置方式,全程自动、高效,并且通过合理的配置让开发与部署变得更加容易。

Webpack Loader 执行机制

一、Loader 链式调用机制

Loader 的执行分为 Pitch 阶段Normal 阶段,两者共同构成链式调用逻辑。


1. Pitch 阶段
  • 执行顺序:从左到右(与 Normal 阶段相反)。
  • 核心作用:拦截机制。如果某个 Loader 的 pitch 方法返回非 undefined 值,直接跳过后续 Loader,进入 Normal 阶段的逆向执行。
  • 伪代码逻辑
    const result = loaderA.pitch(remainingRequest, previousRequest, data);
    if (result !== undefined) {
      // 跳过后续 Loader,进入 Normal 阶段逆向执行
    }
    
2. Normal 阶段
  • 执行顺序:从右到左。
  • 核心作用:实际处理文件内容,上一个 Loader 的输出是下一个 Loader 的输入。

二、源码转换流程(runLoaders 核心逻辑)

Webpack 使用 loader-runner 模块处理 Loader 链。以下是简化后的源码分析:

关键源码:runLoaders 函数(简化版)
function runLoaders(resource, loaders, context, callback) {
  const loaderContext = context || {};
  let loaderIndex = 0; // 当前执行的 Loader 索引
  let processOptions = {
    resourceBuffer: null,
    readResource: fs.readFile.bind(fs)
  };

  // 迭代执行 Pitch 阶段
  iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
    if (err) return callback(err);
    callback(null, ...result);
  });

  function iteratePitchingLoaders(options, loaderContext, callback) {
    if (loaderIndex >= loaders.length) {
      // 所有 Pitch 执行完毕,读取资源
      return processResource(options, loaderContext, callback);
    }

    const currentLoader = loaders[loaderIndex];
    const pitchFn = currentLoader.pitch;

    loaderIndex++; // 移动到下一个 Loader

    if (!pitchFn) {
      // 没有 pitch 方法,继续下一个 Loader
      return iteratePitchingLoaders(options, loaderContext, callback);
    }

    // 执行当前 Loader 的 pitch 方法
    pitchFn.call(
      loaderContext,
      loaderContext.remainingRequest,
      loaderContext.previousRequest,
      (currentLoader.data = {})
    ), (err, ...args) => {
      if (args.length > 0) {
        const hasResult = args.some(arg => arg !== undefined);
        if (hasResult) {
          // Pitch 返回结果,跳过后续 Loader,逆向执行 Normal
          loaderIndex--;
          iterateNormalLoaders(options, loaderContext, args, callback);
          return;
        }
      }
      // 继续下一个 Pitch
      iteratePitchingLoaders(options, loaderContext, callback);
    });
  }

  function processResource(options, loaderContext, callback) {
    // 读取原始资源内容
    options.readResource(loaderContext.resource, (err, buffer) => {
      const resourceBuffer = buffer;
      iterateNormalLoaders(options, loaderContext, [resourceBuffer], callback);
    });
  }

  function iterateNormalLoaders(options, loaderContext, args, callback) {
    if (loaderIndex < 0) {
      // 所有 Normal 阶段完成
      return callback(null, args);
    }

    const currentLoader = loaders[loaderIndex];
    const normalFn = currentLoader.normal || currentLoader;

    loaderIndex--; // 逆向执行

    // 执行当前 Loader 的 Normal 方法
    normalFn.call(loaderContext, args[0], (err, ...returnArgs) => {
      if (err) return callback(err);
      iterateNormalLoaders(options, loaderContext, returnArgs, callback);
    });
  }
}

三、执行流程详解

  1. Pitch 阶段从左到右执行

    • 依次调用每个 Loader 的 pitch 方法。
    • 若某个 pitch 返回结果,跳过后续 Loader,直接进入 Normal 阶段。
  2. 读取资源文件

    • 若所有 pitch 均未拦截,读取原始文件内容。
  3. Normal 阶段从右到左执行

    • 将资源内容传递给最后一个 Loader 处理,结果逆向传递。

四、典型使用案例

案例:自定义 Loader 链观察执行顺序

Loader 配置

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          './loaders/loaderA.js',
          './loaders/loaderB.js',
          './loaders/loaderC.js'
        ]
      }
    ]
  }
};

Loader 实现

// loaderA.js
module.exports = function(source) {
  console.log('[Normal A]');
  return source + '-A';
};
module.exports.pitch = function() {
  console.log('[Pitch A]');
};

// loaderB.js
module.exports = function(source) {
  console.log('[Normal B]');
  return source + '-B';
};
module.exports.pitch = function() {
  console.log('[Pitch B]');
  // 返回非 undefined 值,拦截后续 Loader
  return '拦截内容';
};

// loaderC.js
module.exports = function(source) {
  console.log('[Normal C]');
  return source + '-C';
};
module.exports.pitch = function() {
  console.log('[Pitch C]');
};

执行结果

[Pitch A]
[Pitch B]  // B 的 pitch 返回拦截内容,跳过后续 Pitch
[Normal B] // 进入 Normal 阶段,从 B 开始逆向执行
[Normal A]
最终结果: "拦截内容-B-A"

五、关键总结

  1. Pitch 拦截:通过 pitch 方法提前返回结果,优化构建流程。
  2. 执行方向
    • Pitch:从左到右。
    • Normal:从右到左(若未拦截)。
  3. 资源处理runLoaders 通过 iteratePitchingLoadersiterateNormalLoaders 实现链式调用。

基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(2):分析入口文件

上一篇文章中我们寻找到了create-vite项目中,脚手架要运行的index入口文件,本篇主要讲解在入口文件,脚手架主要做了什么

最终实现效果:通过 pnpm create template-vue3-ts-preset 安装我们自己的项目

源码:这里

声明:本项目的核心源码也是基于开源vite修改而来,本质是想让大家明白创建一个脚手架并发布到npm上走完流程。

前置知识:了解npm、node 以及 《如何创建一个本地的脚手架》《基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(1):寻找 create-vite入口》

  1. 首先我们先观察代码最后一段,发现这样一句

image.png 此时我们可以看到有一个init方法执行了,那么经常编码的同学都知道这大概是一个程序的初始化功能,那么我们去文中寻找init这个方法

1. 首先第一句话为:

image.png 我们可以通过这句话拿到几个关键要素,argv以及formatTargetDir
2. 在全局搜索 这两个方法

image.png 首先这句话是采用了mri的方法 那么mri是啥,通过ai搜索我们可知,这个是一个命令参数解析器,我们先不管他的ts类型,先从这个方法的第一个参数看起

  • process.argv.slice(2): process是一个全局的node对象, argv是node中的一个方法,然后用slice(2)来截取,通过询问或查询资料可以得到:process.argv 是获取命令行参数数组的第三个值。
  • 第二个参数
      {
  alias: { h: 'help', t: 'template' },
  boolean: ['help', 'overwrite'],
  string: ['template'],
} 

这是一个对象 具体作用未知,然后我们查阅node文档可知 mri 方法的作用:就是用于解析命令行参数,并返回一个对象,接受两个参数,第一个参数是node中输入的命令,第二个参数是一些配置选项 所以这个方法其实是拿到我们cmd中输入的一些命令,例如:

    import mri from 'mri'
const argv = mri<{
    template?: string
    help?: boolean
    overwrite?: boolean
  }>(process.argv.slice(2), {
    alias: { h: 'help', t: 'template' },
    boolean: ['help', 'overwrite'],
    string: ['template'],
  })


const init = async () => {
    console.log(argv)
     const s=(argv._[0])
     console.log(s)
}
init()
运行:node index.js  1234
输出:{ _: [ '1234' ] }   1234


image.png 这个方法就比较简单了,接受一个参数 类型为string 去掉参数前后的空格以及末尾多余的“/”

所以:image.png 的作用是:获取用户输入内容的第三个字符串 首先判断它是否为空 如果为空则为undefined,如果不为空则去掉前后空格以及多余的“/”

接着我们继续往下看:

image.png 前三局句代码我们比较容易理解,他就是argv方法的第二个参数的前两个对象当中的,实际上我们获取到的:

  1. argTemplate 就是获取我们输入内容-t后的别名, string类型
  2. argOverwrite 则是判断目录是否要求被覆盖 boolean类型
  3. help 则是判断用户是否输入了 --help或者--h。boolean类型

下面的判断则是首先判断你是否输入了 --help或者--h 如果输入了就返回 帮助命令,然后推出 下面是 helpMessage: image.png 这些我们之后再研究,其实就是输出了一些文本命令,来帮助你需要vite做什么

接着我们继续往下看

image.png

第一句调用了pkgFromUserAgent 并传入了process.env.npm_config_user_agent

我们一步一步来分析:

  1. 首先pkgFromUserAgent 方法:
    function pkgFromUserAgent(userAgent: string | undefined): PkgInfo | undefined {
  if (!userAgent) return undefined
  const pkgSpec = userAgent.split(' ')[0]
  const pkgSpecArr = pkgSpec.split('/')
  return {
    name: pkgSpecArr[0],
    version: pkgSpecArr[1],
  }
}

很显然,这个方法就是将参数userAgent用空格进行切割,然后取第0位 然后将第0位在用/进行分割 将分割出来的数据第一个返回name的value 第二个为version的value

  1. 紧接着我们分析 process.env.npm_config_user_agent,通过经验我们可以分析到,这个似乎是node的某个信息,我们尝试运行 node process.env.npm_config_user_agent 发现打印出来为undefined 经过查询可知 这句话实际上是需要通过npm、pnpm 等包管理器运行才能拿到,因为是需要获取包管理器的信息的,(尝试:将这句话写到某个node脚本中,然后通过package的script 运行)所以我们大致可以得到这样一个结论:这个方法的意思是解析出我们当前使用的具体是哪一个包管理器(npm/pnpm/yarn....)和版本信息包括获取node版本信息等等 例如:

image.png

image.png

image.png

第二句,则是调用了prompts.cancel 的方法传入了“peration cancelled”。

我们接着分析prompts.cancel

  1. 首先我们通过全局搜索prompts 发现它来源于一个第三方库:

image.png 这是一个CLI交互库,其主要作用是在终端中与用户进行交互式对话。比如,我们在创建项目时,需要一步一步输入项目名称,选择模版等等,大概分为一下使用场景:

image.png 由此我们可知:prompts.cancel('Operation cancelled')实际上就是推出或者取消操作 并输出'Operation cancelled'

接着我们继续往下看

image.png 我们会发现其实这里的注释大致已经讲清楚了他的作用,主要是用来获取项目名称和目标目录的。 这里我会逐步进行分析:

  1. 首先:let targetDir = argTargetDir 在前面提到 argTargetDir是用来获取我们输入的用空格隔开的第三个内容的,其实就是我们输入命令之后的内容。
  2. 紧接着这里判断我们的输入是否存在,如果不存在的话就执行下面的内容,如果存在的话,首先是异步执行promps.text 方法,这个方法在上面的使用场景可知,他是获取文本输入的,其中message主要是提示用户的文字,defaultValue是默认选项,如果用户直接回车,则返回这个,placeHolder则是灰色的提示文字。
  3. 紧接着会用 prompts.isCancel来判断用户的输入,因为这里具有默认值,如果rojectName 为空只有一种可能:那就是用户进行了推出或者取消操作,那么此时我们直接推出就好了
  4. 接着是将我们输入的文字进行格式化,去掉前后空格之类的,这个方法我们前面已经说过了

接着我们继续往下看:

image.png 通过注释我们可知,这段代码的主要是在目录不为空的情况下进行的逻辑处理

  1. 首先这个判断中fs.existsSync(targetDir) fs是node当中查询本地文件的一个模块,这里主要是判断是否存在我们输入的文件夹名称,后半部分是判断 这个文件夹是否为空然后取反,结合起来就是:判断是否存在当前文件夹并且不为空。
  2. 紧接着就是判断目标文件夹不为空是否覆盖,如果覆盖则overwrite为yes,否则通过prompts,给用户提供了三个选择:推出 / 删除目录下的所有文件然后继续(等于清空文件夹)/ 不管里面有没有东西,都在里面创建项目
  3. 接着判断overwrite 为取消,如果是的话,就推出
  4. 这里的switch判断主要是对两种选择做出了操作,一个是yes,(清空文件夹),一个是no(推出程序)

接着我们继续往下看:

image.png

  1. 接着packageName是获取当前文件夹的绝对路径,作用是在创建我们的项目时指定路径,fs在对文件操作时需要绝对路径
  2. 紧接着使用isValidPackageName判断路径是否规范,如果中间有空格的话可能就会不合法
  3. 在路径不合法情况下为用户弹出一个输入内容,toValidPackageName(packageName)方法会给你一个合法的名称,如果你仍然坚持不合法的名称就会触发 输出 Invalid package.json name 但是此时程序并没有推出,但是也无法进行下一步,直到你输入一个正确的名称或者推出
  4. 这里判断你有没有推出,如果为true就推出
  5. 接着将这个绝对路径的名称赋值给packageName

接着我们继续往下看:

// Choose a framework and variant 内容中会有这一句注释(选择框架或者变体)这里就是需要我们执行create vite 输入名称之后 需要后续执行的操作

image.png

  1. 首先将我们在 create vite --t xxx 之后输入的内容赋值给 template ,然后将hasInvalidArgTemplate先设fasle
  2. 紧接着判断argTemplate 是否存在并且是否不在我们的模版名称之内:TEMPLATES的定义如下:

image.png 可以看到的是将 FRAMEWORKS中提取所有的variant.name 并扁平化一个数组,后续调用的时候用了includes方法来判断是否匹配

搜索FRAMEWORKS 我们大致可以看到这是一个数组,里面的内容其实就和我们外面的模版基本匹配,然后提取了一些要素, image.png

  1. 接着我们回到原文,当存在名称并不在模版中时候,将 template为空并将hasInvalidArgTemplate 修改为true

  2. 接着判断template是否存在,由上面可知,当输入--t后的名称存在并且不在预设模版中时 template就为undefined,此时就会执行这个if当中的代码,否则就跳过,(验证触发: pnpm create vite --t demo 输入这条命令 node就会执行 "${argTemplate}" isn't a valid template. Please choose from below: 当输入 pnpm create vite --t template-solid-ts 等时候,则会跳过这段代码

  3. 我们接着来看 判断中首先会触发一个互动,满足4的条件之后,option 展示FRAMEWORKS的所有选项,前文我们已经提到了,这里会展示第一层所有选项,最后将这个选项返回给framework

接着我们继续往下

image.png

  1. 这里首先会再次判断是否为空,为空直接推出
  2. 接着再次出发点一个互动,让选择以那种变体开发,也就是我们在创建项目时选择ts js...等一些操作
  3. 这里的逻辑操作基本和上面一样,所需要注意的只有getFullCustomCommand方法

image.png 这个方法实际上就是将 预设中的的包管理器(npm、pnpm等) 转换成我们使用的包管理器。 接下来我们一步一步分析这个方法

  1. 首先这个方法接受两个参数:customCommand:string以及pkgInfo?: PkgInfo 类型PkgInfo=interface PkgInfo { name: string version: string }
  2. 第一句的意思是判断当前是否使用了包管理器 有的话就用当前的,没有的话就用npm
  3. 第二句的意思是判断当前是不是yarn 如果是的话 版本必须为1.xxx 否则返回false这是一个boolean类型
  4. 接着返回customCommand 也就是我们输入的命令 首先用正则匹配npm 在满足条件的情况下,判断是否为 bun 、pnpm 满足的情况下返回对应的 包命令, 最后如果有其他情况则保留原有的格式
  5. 当不满足npm正则时候则匹配 isYarn 变量 如果满足就返回空,如果不满足的话就就去掉@latest
  6. 最后再用正则匹配下 pm exec 如果满足则将npm exec 替换成运行临时包的命令

好了,让我们在回到之前的代码当中, 然后判断下是否为空,为空推出。 最终将这个值赋值给template #我们接着来分析

image.png

  1. 首先我们将 向path.jion方法中传入全局变量中的cwd(也就是当前工作目录),在传入 我们targetDir(我们 在vite 后跟的第一个字符串),生成完整的绝对路径返回给root
  2. 紧接着通过 fs.mkdirSync 来创建新的文件夹
  3. 接着创建一个变量isReactSwc 默认为false
  4. 再然后我们拿到template 也就是包名,判断一下是否包含-swc这个字段,如果包含就把template设置为true,然后把名称中的swc删除掉,这一步其实是在针对react 因为这里reactswc模式要比babel-loader要快
  5. 再然后将获得的pkginfo做一下判断如果有就用输入的,如果没有默认用npm
  6. 再然后就用FRAMEWORKS循环对比判断其中的variants对象中的name是否与tamplate匹配 如果匹配就返回其中的customCommand解构给customCommand

我们接着往下

image.png

  1. 当结构完成之后,首先判断结构的值存不存在,如果存在的话,就调用getFullCustomCommand方法将名称以及包管理器名称作为参数传入 我们之前已经分析过getFullCustomCommand最后返回 成我们命令行中对应的包名+模版名称
  2. 然后通过结构拿到我们对应的包管理器名称,
  3. 再然后把命令参数中的TARGET_DIR 替换成我们所需的项目目录名称
  4. 接着同步执行这个命令,并传入管理器名称,目录名称 等,
  5. 当代码执行完毕以后,使用process.exit进行推出,
  6. 然后打印Scaffolding project in ${root}...

    我们接着往下

image.png

  1. 这里定义了一个templateDir 这个主要是用来获取模版的绝对路径的因为template之前源于variant variant本身并没有完整的模版名,所以这里需要拼接写 模版名,最终得到一个完整的外部的模版名称,
  2. 再然后就比较简单了,创建write利用fs模块 来创建文件了,首先先判断是文件还是文件夹,有如果是文件夹就创建文件夹没否则就拷贝文件(单个文件)
  3. 再然后就获取目前模板的绝对路径,赋值给files
  4. 然后就是设置package.json的一些关键信息
  5. 再然后我们需要写入自己的packge.json文件,因为package.json文件本身包含一些文件信息,所以不能直接拷贝
  6. 接下来的if (isReactSwc) 则是针对react-swc的操作,如果选择了这个那么就得是ts

然后接下来的就主要是输入到log中的内容了。

  1. 首先设置一个doneMessage为空字符串
  2. 然后获取我们创建的项目和当前目录之间的差异路径
  3. 将差异路径赋值给donemessage
  4. 然后判断创建目录的路径是否等于当前目录的路径,如果相等的话 ,就将其中的正则匹配通过三元表达式赋值给doneMessage 5.因为之前我们已经哪都pkgManager 他就包管理器名称,这里主要是针对yarn进行区分,然后拼接对应的字符串,最后通过 prompts.outro(doneMessage) 输出到页面

到此整个npm crete vite的功能基本已经实现了:接下来我们总结下这个主要干了些什么事情:

总结:

1. 首先是获取项目名称以及目标目录,
2. 对目标目录进行处理
3. 再然后就是获取我们想创建什么样的项目
4. 执行创建命令创建对应的项目。









 




Webpack 构建阶段:模块解析流程

一、流程概览 Webpack 的构建阶段核心任务是将入口文件及其依赖转换为模块依赖图。以下是关键步骤的源码解析及案例演示: 步骤 核心对象 源码关键文件 作用 1. 入口模块处理 EntryPlugi

为什么选择 tsup?

前端构建工具技术选型:为什么选择 tsup? 目录 什么是 tsup? 核心优势 与其他工具对比 适用场景 快速配置示例 何时不推荐使用 tsup? 总结 1. 什么是 tsup? tsup 是一个零

对webpack工程化的理解

webpack是什么?

webpack是一个现代的前端打包工具,用于构建和优化Web应用程序的前端资源,包括js,css,图片,字体等。它的主要目标试讲所有依赖项打包到一个或多个静态文件中,以便在浏览器中加载,提高了代码的可维护性和性能。下面是我对webpack配置的一些理解。

入口配置entry

首先关键就是需要配置入口文件,例如我有两个入口文件,entry.page1和entry.page2,则需要进行如下配置:

// 入口配置
  entry: {
      'entry.page1': '/app/pages/page1/entry.page1.js' // 文件路径
      'entry.page2': '/app/pages/page1/entry.page1.js'
  },

同时需要配置htmlWebpackPlugin如下:

// html-webpack-plugin 辅助注入打包后的 bundle 文件到 tpl文件中
   new HtmlWebpackPlugin({
      // 产物(最终输出路径)
      filename: path.resolve(
        process.cwd(),
        "./app/public/dist",
        `entry.page1.tpl`
      ),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
      // 要注入的代码块
      chunks: ['entry.page1'],
    })
    new HtmlWebpackPlugin({
      // 产物(最终输出路径)
      filename: path.resolve(
        process.cwd(),
        "./app/public/dist",
        `entry.page2.tpl`
      ),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
      // 要注入的代码块
      chunks: ['entry.page2'],
    })

通过上面可以看出这样配置显然不太合理,如果有多个入口文件,难道要一个一个去写吗?所以这时针对多文件就需要来动态构造,具体实现如下:

// 动态构造
const pageEntries = {};
const htmlWebpackPluginList = [];

// 获取app/pages 目录下所有入口文件(entry.xxx.js)
const entryList = path.resolve(process.cwd(), `./app/pages/**/entry.*.js`);
glob.sync(entryList).forEach((file) => {
  const entryName = path.basename(file, ".js");
  // 构造entry
  pageEntries[entryName] = file;
  // 构造最终渲染的页面文件
  htmlWebpackPluginList.push(
    // html-webpack-plugin 辅助注入打包后的 bundle 文件到 tpl文件中
    new HtmlWebpackPlugin({
      // 产物(最终输出路径)
      filename: path.resolve(
        process.cwd(),
        "./app/public/dist",
        `${entryName}.tpl`
      ),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
      // 要注入的代码块
      chunks: [entryName],
    })
  );
});

通过这种方法不管有多少入口文件,就不需要每个都去配置,直接使用 pageEntrieshtmlWebpackPluginList 即可,大大的提高了我们的开发效率。

分包策略

好的分包策略目的是把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果。大体上可以分为三类,第一种是第三方lib库,在开发过程中基本不会进行改动,除非依赖版本升级;第二种是业务组件代码的公共部分,改动较少;第三种就是我们的业务代码需要经常改动。具体分包方式如下:

// 配置打包输出优化(代码分割,模块合并,缓存,TreeShaking,压缩等优化策略)
  optimization: {
    /**
     * 把js文件打包成3种类型
     * 1.vendor: 第三方 lib 库,基本不会改动,除非依赖版本升级
     * 2.common: 业务组件代码的公共部分抽离出来,改动较少
     * 3.entry.{page}: 不同页面entry里的业务组件代码的差异部分,会经常改动
     * 目的:把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果
     */
    splitChunks: {
      chunks: "all", // 对同步和异步模块都进行分割
      maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
      maxInitialRequests: 10, // 入口点的最大并行请求数
      cacheGroups: {
        // 第三方依赖库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendor", // 模块名称
          priority: 20, // 优先级,数字越大优先级越高
          enforce: true, // 强制执行
          reuseExistingChunk: true, // 如果当前 chunk 包含的模块已经被抽取出去了,那么它将被忽略
        },
        // 公共模块
        common: {
          name: "common",
          minChunks: 2, // 最小引用次数
          minSize: 1, // 最小引用字节
          priority: 10, // 优先级,数字越大优先级越高
          reuseExistingChunk: true, // 复用已有的公共chunk
        },
      },
    },
    runtimeChunk: true,
  },

生产环境和开发环境的不同配置

生产环境

生产环境是最终需要打包发布上线的,所以通常需要一些额外的配置以达到更好的效果。

output(输出配置)

// 生产环境的output配置
  output: {
    filename: "js/[name]_[chunkhash:8].bundle.js",
    path: path.join(process.cwd(), "./app/public/dist/prod"),
    publicPath: "/dist/prod",
    crossOriginLoading: "anonymous",
  },

多线程打包配置

这里使用的happypack,还有另一种方式thread-loader也可完成。

const HappyPack = require("happypack");
const os = require("os");

// 多线程build设置
const happypackCommonConfig = {
  debug: false,
  threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
};

plugins: [
// 多线程打包js,加快打包速度
   new HappyPack({
      ...happypackCommonConfig,
      id: "js",
      loaders: [
        `babel-loader?${JSON.stringify({
          presets: ["@babel/preset-env"],
          plugins: ["@babel/plugin-transform-runtime"],
        })}`,
      ],
    }),
// 多线程打包css,加快打包速度
   new HappyPack({
      ...happypackCommonConfig,
      id: "css",
      loaders: [
        {
          path: "css-loader",
          options: {
            importLoaders: 1,
          },
        },
      ],
    }),
]

配置TerserWebpackPlugin

使用TerserWebpackPlugin的并发和缓存,提升压缩阶段的性能,同时清除console.log的打印信息。

const TerserWebpackPlugin = require("terser-webpack-plugin");
optimization: {
    // 使用TerserWebpackPlugin的并发和缓存,提升压缩阶段的性能
    // 清除console.log
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        cache: true, // 启用缓存来加速构建过程
        parallel: true, // 利用多核CPU的优势来加快压缩速度
        extractComments: false,
        terserOptions: {
          compress: {
            drop_console: true, // 删除console.log
          },
        },
      }),
    ],
  },

开发环境

开发环境为了提高开发效率需要配置热更新插件,以便在业务文件改动时可以实时的更新页面。主要流程就是通过一个devServer中间件,可以监控到业务文件的改动,并通知浏览器进行更新,总结来说就是需要拥有监控文件改动和通知页面更新的能力。具体实现方式如下:

// webpack.dev.js
const baseConfig = require("./webpack.base.js");
const merge = require("webpack-merge");
const webpack = require("webpack");
const path = require("path");

// devServer的配置
const DEV_SERVER_CONFIG = {
  HOST: "127.0.0.1",
  PORT: 9002,
  HMR_PATH: "__webpack_hmr", // 官方规定
  TIMEOUT: 20000,
};
const { HOST, PORT, HMR_PATH, TIMEOUT } = DEV_SERVER_CONFIG;

// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach((key) => {
  // 第三方包不作为hmr入口
  if (key !== "vendor") {
    baseConfig.entry[key] = [
      // 主入口文件
      baseConfig.entry[key],
      // hmr 更新入口,官方指定的 hmr 路径
      `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}&timeout=${TIMEOUT}&reload=true`,
    ];
  }
});

// 开发环境webpack配置
const webpackConfig = merge.smart(baseConfig, {
  mode: "development", // 指定开发环境
  // sourceMap 开发工具,呈现代码的映射关系,便于在开发过程中调试代码
  devtool: "eval-cheap-module-source-map",
  // 开发环境的output配置
  output: {
    filename: "js/[name]_[chunkhash:8].js",
    path: path.resolve(process.cwd(), "./app/public/dist/dev/"), // 输出文件存储路径
    publicPath: `http://${HOST}:${PORT}/public/dist/dev/`, // 外部资源公共路径
    globalObject: "this", // 全局变量
  },
  // 开发阶段插件
  plugins: [
    // 模块热替换允许在应用程序运行时替换,提高开发效率
    new webpack.HotModuleReplacementPlugin({
      multiStep: false,
    }), // 热更新插件
  ],
});

module.exports = {
  webpackConfig,
  // devServer 配置,给dev.js 使用
  DEV_SERVER_CONFIG,
};

dev.js

// 本地开发启动 devServer
const express = require("express");
const path = require("path");
const webpack = require("webpack");
const devMiddleware = require("webpack-dev-middleware");
const hotMiddleware = require("webpack-hot-middleware");
const consoler = require("consoler");

// 从webpack.dev.js获取配置
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev.js");

const app = express();
const compiler = webpack(webpackConfig);

// 指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")));

// 引用devMiddleware中间件(监控文件改动)
app.use(
  devMiddleware(compiler, {
    // 落地文件
    writeToDisk: (filePath) => filePath.endsWith(".tpl"),
    // 资源路径
    publicPath: webpackConfig.output.publicPath,
    // headers 配置
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers":
        "X-Requested-With, content-type, Authorization",
    },
    stats: {
      colors: true,
    },
  })
);

// 引入hotMiddleware中间件(实现热更新通讯)
app.use(
  hotMiddleware(compiler, {
    path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
    log: () => {},
  })
);

consoler("请等待webpack初次构建完成提示....");

// 启动 devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
  console.log(`app listening on port ${port}`);
});

可以看到我们使用了 HotModuleReplacementPlugin 热替换插件允许在应用程序运行时替换,再借助 devMiddleware 监控文件改动,以及hotMiddleware 实现热更新通讯,同时也配置了sourceMap开发工具,便于在开发过程中调试代码。

总结

以上就是生产环境和开发环境的一些不同配置,当然webpack还有许多其他的配置这里没有一一列出,例如一些loader,处理vue文件的vue-loader,处理css文件的css-loader等,大家可以根据自己需要进行各种不一样的配置。通过这一章节对webpack的学习,让我更加清晰的了解了工程化相关知识,对项目中需要配置哪些东西也有了更深的理解,重要的是了解了一些主要思想,以后再使用其他工具如vite,rollup等也能够很快上手啦。fighting~

引用:抖音“哲玄前端”《大前端全栈实践》

(在线CAD集成)网页CAD二次开发中配置属性的详细教程

一、前言

Mxcad SDK 能够在线预览编辑CAD图纸,用户可根据项目需求选择不同的实现方式,如通过Vite、CDN、webpack分别使用mxcad。如果不清楚mxcad实现在线预览编辑CAD图纸的方法,可参考mxcad开发文档:www.mxdraw3d.com/mxcad_docs/…

成功创建mxcad对象后,在后续的开发过程中可能会遇到设置图纸操作习惯、监听图纸完全打开、设置图纸多选等一系列配置,mxcad内部为了方便用户的操作实现了上述一些类配置相关的方法或属性API,用户可自定义设置相关配置来满足自己项目的需求。本章就为大家介绍mxcad中与图纸操作相关的配置属性。

二、配置属性

mxcad在创建初始就可以直接设置配置属性,在创建mxcad对象的时候配置的属性将作为CAD项目加载的默认设置。下面以在vue3+ts构建的项目中创建mxcad对象并设置初始属性为例。

1. createMxCad()

在创建mxcad对象的时候,可以在createMxCad()方法内设置配置属性。

import { onMounted } from "vue";
import { createMxCad } from "mxcad";
onMounted(() => {
  const mode = "SharedArrayBuffer" in window ? "2d" : "2d-st";
  createMxCad({
    canvas: "#myCanvas",
    locateFile: (fileName) => {
      return new URL(
        `../../node_modules/mxcad/dist/wasm/${mode}/${fileName}`,
        import.meta.url
      ).href;
    },
    fileUrl: new URL("../../public/test2.mxweb", import.meta.url).href,
    fontspath: new URL("../assets/fonts", import.meta.url).href
  });
});

createMxCad方法中的初始必要属性:

A、canvas:canvas画布实例的id名

B、locateFile:mxcad的核心依赖mxcad库中/mxcad/dist/wasm目录下对应分类(2d|2d-st)中的 wasm 文件(该文件是c++编译生成的),其中 2d 目录下为多线程程序、2d-st 目录下为单线程程序,该参数用来指定 wasm 程序的网络路径。

C、fontspath:指定cad图纸中的字体文件加载路径。默认路径为dist/fonts,你可以在该目录中添加打开图纸需要的各种字体文件。

D、fileUrl:指定打开mxweb图纸的网络路径。

初始运行效果演示:

image-20250327170421832.png

其他属性:

1.1、openParameter:设置打开文件的参数,可以设置打开文件是否使用缓存,或者是否使用工作线程打开文件等。

   // 设置打开文件不使用缓存
   openParameter:{fetchAttributes:FetchAttributes.EMSCRIPTEN\_FETCH\_LOAD\_TO\_MEMORY}

1.2、onOpenFileComplete:监听打开文件成功的回调事件,在图纸打开完成后进行的操作可在该方法内执行。

   // 在图纸完全打开后控制台输出信息
   onOpenFileComplete:()=>{
       console.log('图纸完全打开!')
   }

1.3、viewBackgroundColor:设置视区背景颜色,值为rgb。

   // 初始打开图纸的背景颜色设置为白色
   viewBackgroundColor:{red:255,green:255,blue:255}

1.4、browse:是否设置为浏览模式,值为true或1时启用浏览模式,且CAD对象不能选中;值为2时启用浏览模式,CAD对象能选中,但不能夹点编辑;值为false时启用编辑模式。

   // 启动用浏览模式,图纸中的CAD对象均不能被选中和编辑
   browse:true
   /\*\*
    或
    browse:1
    \*/
   // 启动用浏览模式,CAD对象能被选中显示夹点但并不能进行夹点编辑
   browse:2
   // 编辑模式,图纸中的所有CAD对象均能被选中编辑
   browse:flase

1.5、middlePan:设置移动视区的操作方式。设置为0,点击鼠标左键移动视区;设置为1,点击鼠标中键移动视区;设置为2,点击鼠标中键和鼠标左键均可移动视区。

   // 点击鼠标中键移动视区
   middlePan:1
   // middlePan:2
   // middlePan:0

1.6、enableUndo:是否启用回退功能。设置为true则可以调用Mx_Undo命令回退操作;设置为false则禁用回退命令。默认设置为false。

   //设置启用回退功能
   enableUndo:true

1.7、enableIntelliSelect:是否启用对象选择功能。设置为true则启用;设置为false则不启用。

   // 启用对象选择功能
   enableIntelliSelect:true

1.8、multipleSelect:是否启用多选。设置为true则启用;设置为false则不启用。

   // 启用多选
   multipleSelectL:true

更多createMxCad方法内部的属性设置可参考:

www.mxdraw3d.com/mxcad_docs/…。  

2.MxFun.setIniset()

由于mxcad是依赖于mxdraw去显示的图纸,因此mxdraw中也提供了实现各种初始配置的方法:MxFun.setIniset(),我们可以在该方法中配置更多CAD项目的初始配置,其调用方法如下:

import { MxFun } from "mxdraw"
MxFun.setIniset({
    // 启动夹点编辑功能, 开启单选图形(mxcad默认开启)
    "EnableGripEidt": true,
    // 开启多选
    "multipleSelect": true
    /**
     * ......可配置更多iniConfig参数
     */
})
常用iniConfig参数:

2.1、 EnableIntelliSelect:是否启用智能选择。设置为true则启用;设置为false则不启用。

   // 启用智能选择
   EnableIntelliSelect:true 

2.2、EnableGripEdit:是否启动夹点编辑。设置为true或1表示启用,设置为0或fase表示禁用,设置为2表示选中对象后只显示夹点,但不响应响应夹点编辑。  

   // 启用夹点编辑
   EnableGripEdit:true

2.3、multipleSelect: 是否启动多选,启动多选设置后用户一次选择多个实体。设置为true则启用;设置为false则不启用,其默认值是false。

   // 启动多选-框选
   multipleSelect:true

2.4、IntelliSelectType:多选方式控制。设置为1:多选,但不支持连续多选,设置为2:多选,并支持连续多选,默认值为1。该设置生效的前提是要先启动多选。

   // 多选方式可以连续点选
   IntelliSelectType:2

2.5、autoResetRenderer:是否启用自动重置渲染器。设置为true则启用;设置为false则不启用

   // 启用自动重置渲染器
   autoResetRenderer:true

2.6、ForbiddenDynInput:是否禁用动态输入框。设置为true为禁用;设置为false则启用。

   //禁用动态输入框
   ForbiddenDynInput:true

2.7、inputRectWidth:设置夹点和拾取框的宽度,其单位是屏幕上的像素。

   //将夹点宽度设置为5px
   inputRectWidth:5

2.8、gripPointColor: 设置夹点颜色,其值为十六机制颜色值,如:0xFFFFFFFF,0xNRGB等。

   // 将夹点颜色设置为白色
   gripPointColor:0xFFFFFFFF

2.9、EnableDrawingViewAngle:是否使用图纸中的视区角度设置,默认为true 。

  // 不使用图纸中的视区角度
   EnableDrawingViewAngle:flase

更多MxFun.setIniset()方法内部的属性设置可参考: mxcad.github.io/mxdraw_api_…

3.McObject对象方法API

处理了上面介绍的两种方式外,mxcad中的McObject对象里也提供了设置CAD项目配置参数的方法。下面以常用的几种设置方法为例:

3.1、McObject.setBrower():是否设置为浏览模式。 

  import { MxCpp } from 'mxcad';
   // 设置为浏览模式
    MxCpp.getCurrentMxCAD().setBrowse(true);
   // 设置为编辑模式
    MxCpp.getCurrentMxCAD().setBrowse(false); 

3.2、McObject.setViewBackgroundColor():设置视区的背景色。

   import { MxCpp } from 'mxcad';
   // 将视区背景色设置为白色,值为rgb
    MxCpp.getCurrentMxCAD().setViewBackgroundColor(255,255,255);

3.3、McObject.setAttribute():设置mxcad对象的一些属性设置。

   import { MxCpp } from 'mxcad';
    MxCpp.getCurrentMxCAD().setAttribute({
        //启用回退功能
        EnableUndo:true,
        //显示精度设置,默认为0,可以取0 \~1000,1000为最高精度
        DisplayPrecision:1000
    });

更多McObject方法设置可参考: www.mxdraw3d.com/mxcad_docs/…

三、在线演示

用户可在我们提供的在线demo的编辑器中直接运行设置,查看实时效果,在线demo地址:demo2.mxdraw3d.com:3000/mxcad/

实时运行效果演示:

import { MxCpp } from 'mxcad';
// 将视区背景色设置为白色,值为rgb
 MxCpp.getCurrentMxCAD().setViewBackgroundColor(255,255,255);

如下图: image-20250328120924230.png

❌