从开发到生产构建工程化链路搭建

0 阅读5分钟

前言:什么是工程化

工程化是指用软件工程的方法来管理和优化开发流程,让代码从"能跑"变成"好维护、好协作、好交付",举例使用 Webpack5 进行工程化链路搭建(工具不是关键,工程化链路搭建思想才是核心

工程化带来的好处

  1. 开发效率 — 热更新、代码提示、自动补全
  2. 代码质量 — 规范约束、静态检查、单元测试
  3. 团队协作 — 统一规范、模块化、版本管理
  4. 性能优化 — 代码压缩、按需加载、缓存策略
  5. 可维护性 — 清晰的目录结构、组件化、文档化

一、环境概览对比

image.png

二、开发环境详解

2.1 执行链路

┌─────────────────────────────────────────────────────────────┐
│                    开发环境启动流程                         │
└─────────────────────────────────────────────────────────────┘

  1. Node 启动 dev.js
        │
        ├──► 加载 webpack.dev.js 配置
        │         │
        │         ├──► 合并 webpack.base.js(基础配置)
        │         │
        │         ├──► 设置 mode: 'development'
        │         │
        │         ├──► 为每个入口注入 HMR 客户端代码
        │         │         │
        │         │         └──► entry.page1: [
        │         │                 './page1/entry.page1.js',
        │         │                 'webpack-hot-middleware/client?path=...'
        │         │             ]
        │         │
        │         └──► 添加 HotModuleReplacementPlugin
        │
        ├──► 创建 Webpack Compiler 实例
        │
        ├──► 启动 Express 服务器
        │         │
        │         ├──► 挂载 webpack-dev-middleware
        │         │         │
        │         │         ├──► 编译代码存入内存文件系统
        │         │         ├──► 监听文件变化自动重编译
        │         │         └──► 提供静态资源访问服务
        │         │
        │         ├──► 挂载 webpack-hot-middleware
        │         │         │
        │         │         └──► 创建 EventSource 连接 /__webpack_hmr
        │         │
        │         └──► 监听端口
        │
        └──► 输出:服务启动成功,监听端口

2.2 核心配置解析

//webpack.dev.js

const webpackConfig = merge.smart(baseConfig, {
  mode: 'development',  // 开发模式:不压缩,启用 sourcemap

  output: {
    // 输出到内存,物理路径仅作参考
    path: path.resolve(process.cwd(), './app/public/dist/dev'),
    // 带 contenthash 用于缓存
    filename: 'js/[name]_[contenthash:8].bundle.js',
    // 浏览器访问路径(完整 URL)
    publicPath: 'http://127.0.0.1:xxxx/public/dist/dev/',
  },

  plugins: [
    // 热更新插件:生成 .hot-update.json/js 补丁文件
    new webpack.HotModuleReplacementPlugin({
      multiStep: false,  // 单步更新,更可靠
    }),
  ],
})
//dev.js(Express 服务器)

const app = express()

// 1. 静态资源目录(图片、字体等)
app.use(express.static(path.resolve(process.cwd(), './app/public')))

// 2. Webpack 开发中间件
app.use(devMiddleware(compiler, {
  // 关键:只有 .tpl 模板文件写入磁盘
  // 原因:后端 Koa 需要从磁盘读取模板渲染页面
  writeToDisk: (filePath) => filePath.endsWith('.tpl'),

  // 资源访问路径(必须和 webpack 配置一致)
  publicPath: webpackConfig.output.publicPath,

  // CORS 配置:允许后端服务(8080)访问前端资源(9002)
  headers: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
    'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type,Authorization',
  },
}))

// 3. 热更新中间件
app.use(hotMiddleware(compiler, {
  path: '/__webpack_hmr',  // EventSource 路径
  log: () => {},           // 关闭冗余日志
}))

app.listen(9002)

2.3 热更新(HMR)完整机制

┌─────────────────────────────────────────────────────────────┐
│                      热更新流程图                           │
└─────────────────────────────────────────────────────────────┘

  开发者保存代码
        │
        ▼
  Webpack 监听文件变化(watch)
        │
        ├──► 重新编译变更模块
        │
        ├──► HotModuleReplacementPlugin 生成热更新补丁
        │         ├──► 更新清单:xxx.hot-update.json
        │         └──► 更新代码:xxx.hot-update.js
        │
        ▼
  webpack-dev-middleware 更新内存中的文件
        │
        ▼
  webpack-hot-middleware 检测编译完成
        │
        ▼
  通过 EventSource 推送事件到浏览器
        │
        ▼
  浏览器 HMR Runtime 接收事件
        │
        ├──► 下载补丁文件(hot-update.json/js)
        │
        ├──► 执行模块替换(module.hot.accept)
        │
        └──► 更新成功:无刷新更新 / 失败:页面刷新
        
 ----------------------------------------------------------------------------------------------
┌─────────────────────────────────────────────────────────────┐
│                      开发环境架构图                         │
└─────────────────────────────────────────────────────────────┘

  浏览器
    │
    ├──► 请求页面 ───────────────┐
    │                            ▼
    │                    ┌───────────────┐
    │                    │  Koa 后端服务 │
    │                    │       │
    │                    │               │
    │                    │  渲染 .tpl 模板 │
    │                    │  注入初始数据   │
    │                    └───────┬───────┘
    │                            │
    │◄───────────────────────────┘
    │  返回 HTML(含 JS 引用)
    │
    ├──► 请求 JS/CSS 资源 ─────────┐
    │                              ▼
    │                      ┌───────────────┐
    │                      │ Webpack Dev   │
    │                      │   Server      │
    │                      │    :xxxx      │
    │                      │               │
    │                      │ 内存文件系统  │
    │                      │ 热更新推送    │
    │                      └───────────────┘
    │
    ▼
  建立 EventSource 连接
  接收热更新事件

三、生产环境详解

3.1执行链路

┌─────────────────────────────────────────────────────────────┐
│                    生产环境构建流程                         │
└─────────────────────────────────────────────────────────────┘

  1. Node 启动 prod.js
        │
        ├──► 加载 webpack.prod.js 配置
        │         │
        │         ├──► 合并 webpack.base.js
        │         │
        │         ├──► 设置 mode: 'production'
        │         │
        │         ├──► 配置多线程构建(thread-loader)
        │         │
        │         ├──► 配置 CSS 提取(MiniCssExtractPlugin)
        │         │
        │         ├──► 配置代码压缩(TerserPlugin)
        │         │
        │         └──► 配置目录清理(CleanWebpackPlugin)
        │
        ├──► 创建 Webpack Compiler
        │
        ├──► 执行 compiler.run()(单次构建)
        │         │
        │         ├──► 清空输出目录
        │         │
        │         ├──► 多线程编译 JS/CSS
        │         │
        │         ├──► 代码分割(vendor/common/runtime)
        │         │
        │         ├──► 提取 CSS 到独立文件
        │         │
        │         ├──► Terser 压缩 JS(删除 console)
        │         │
        │         ├──► CssMinimizer 压缩 CSS
        │         │
        │         └──► 生成 HTML 模板(注入资源链接)
        │
        └──► 输出构建统计信息
                    │
                    ▼
            产物输出到 app/public/dist/prod/

3.2 核心配置解析

//webpack.prod.js

const webpackConfig = merge.smart(baseConfig, {
  mode: 'production',  // 生产模式:自动优化、压缩

  output: {
    // 物理输出目录
    path: path.join(process.cwd(), 'app/public/dist/prod'),
    filename: 'js/[name]_[contenthash:8].bundle.js',
    // 相对路径(生产环境同源部署)
    publicPath: '/dist/prod/',
    crossOriginLoading: 'anonymous',  // 跨域加载不发送 cookie
  },

  module: {
    rules: [
      // CSS 处理:提取到独立文件(非内联)
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,  // 提取 CSS
          {
            loader: 'thread-loader',     // 多线程
            options: {
              workers: os.cpus().length - 1,  // CPU 核心数-1
            },
          },
          'css-loader',
        ],
      },
      // JS 处理:多线程 babel 转译
      {
        test: /\.js$/,
        use: [
          'thread-loader',
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
              plugins: ['@babel/plugin-transform-runtime'],
            },
          },
        ],
      },
    ],
  },

  plugins: [
    // 1. 清空输出目录
    new CleanWebpackPlugin(['public/dist'], {
      root: path.resolve(process.cwd(), './app/'),
      verbose: true,
    }),

    // 2. 提取 CSS 到独立文件
    new MiniCssExtractPlugin({
      filename: 'css/[name]_[contenthash:8].bundle.css',
    }),

    // 3. 注入 crossorigin 属性(用于 CDN 场景)
    new HtmlWebpackInjectAttributesPlugin({
      crossorigin: 'anonymous',
    }),
  ],

  optimization: {
    minimize: true,
    minimizer: [
      // JS 压缩:并发 + 缓存
      new TerserPlugin({
        cache: true,      // 启用缓存
        parallel: true,   // 并发压缩
        terserOptions: {
          compress: {
            drop_console: true,  // 删除 console.log
          },
        },
      }),
      // CSS 压缩
      new CssMinimizerPlugin(),
    ],
  },
})

3.3 生产环境构建产物

app/public/dist/prod/
├── js/
│   ├── entry.page1_a1b2c3d4.bundle.js      # 页面主代码
│   ├── entry.page2_e5f6g7h8.bundle.js
│   ├── vendor_i9j0k1l2.bundle.js           # 第三方库(VueElement Plus)
│   ├── common_m3n4o5p6.bundle.js           # 公共代码
│   └── runtime~entry.page1_q7r8s9t0.bundle.js  # Webpack runtime
├── css/
│   ├── entry.page1_u1v2w3x4.bundle.css     # 提取的 CSS
│   └── entry.page2_y5z6a7b8.bundle.css
└── dist/
    ├── entry.page1.tpl                     # HTML 模板(注入资源链接)
    └── entry.page2.tpl

3.4 多线程构建原理

┌─────────────────────────────────────────────────────────────┐
│                   thread-loader 多线程构建                  │
└─────────────────────────────────────────────────────────────┘

  主进程(Webpack)
        │
        ├──► 启动 worker 线程池(CPU 核心数 - 1)
        │         │
        │         ├──► Worker 1 ──► 处理模块 A、B、C...
        │         ├──► Worker 2 ──► 处理模块 D、E、F...
        │         ├──► Worker 3 ──► 处理模块 G、H、I...
        │         └──► Worker N ──► ...
        │
        ├──► 分发模块到各个 Worker
        │
        ├──► Worker 执行 babel-loader/css-loader 等
        │
        └──► 收集结果,合并到主构建流程

  配置参数:
  - workers: os.cpus().length - 1  # 保留一个核心给主进程
  - workerParallelJobs: 50         # 每个 worker 最多 50 个任务
  - poolTimeout: 2000              # 2 秒空闲后关闭线程

四、完整请求链路对比

4.1 开发环境请求链路

┌─────────────────────────────────────────────────────────────┐
│                    开发环境请求链路                         │
└─────────────────────────────────────────────────────────────┘

  用户访问 http://localhost:8080/view/page1
        │
        ▼
  Nginx / Koa (8080)
        │
        ├──► 匹配路由 /view/:page
        │
        ├──► Controller 渲染模板
        │         │
        │         ├──► 读取 app/public/dist/entry.page1.tpl
        │         │
        │         └──► Nunjucks 注入数据
        │                   ├──► {{name}} = "Elpis"
        │                   ├──► {{env}} = "local"
        │                   └──► {{options}} = {...}
        │
        ▼
  返回 HTML(含 xxxx 端口的 JS 引用)
        │
        ▼
  浏览器解析 HTML
        │
        ├──► 请求 JS/CSS ──► Webpack Dev Server
        │                         │
        │                         └──► 从内存读取返回
        │
        └──► 建立 EventSource ──► 热更新推送

4.2 生产环境请求链路

┌─────────────────────────────────────────────────────────────┐
│                    生产环境请求链路                         │
└─────────────────────────────────────────────────────────────┘

  用户访问 http://example.com/view/page1
        │
        ▼
  Nginx / Koa (80/443)
        │
        ├──► 匹配路由 /view/:page
        │
        ├──► Controller 渲染模板
        │         │
        │         └──► 读取 app/public/dist/prod/entry.page1.tpl
        │
        ▼
  返回 HTML(含相对路径的 JS/CSS 引用)
        │
        ▼
  浏览器解析 HTML
        │
        ├──► 请求 JS/CSS ──► Nginx 静态资源
        │                         │
        │                         └──► 返回物理文件
        │                               ├──► /dist/prod/js/xxx.js
        │                               └──► /dist/prod/css/xxx.css
        │
        └──► Vue 挂载激活页面

五、总结

  • 自动化:自动扫描入口、自动注入 HTML、自动清理
  • 模块化:页面级分割、公共代码提取、组件复用
  • 标准化:目录规范、命名约定、代码风格
  • 性能优先:代码分割、缓存策略、压缩优化
  • 可维护:分层架构、路径别名、环境分离