【里程碑二】基于webpack5完成的多前端工程化建设

一、什么是前端工程化

前端工程化,前端工程化,就是把传统纯手动的前端开发模式 ,升级为标准化、自动化、模块化、易维护、易扩展的现代工程开发范式。

它早已不只是单纯写页面、写业务逻辑,而是依托工具链、规范体系、构建流程和协作机制 ,把前端开发完整链路:编码、代码校验、项目构建、自动化测试、部署上线、线上监控,打造成一套全自动闭环流程

一、整体架构

整个构建体系分三层组成:配置层、执行层、插件层

webpack.base.js 放所有环境共用的配置,webpack.dev.js 和 webpack.prod.js 各自叠加环境专属的部分,通过 webpack-merge 合并。

这样做的好处是:通用配置只写一份,各个环境都有专属的配置,各管各的。

二、入口(Entry) 找出所有的入口文件 利用 glob 遍历所有入口文件,提取文件名作为 入口名称

Webpack 需要知道从哪些文件开始打包,这就是 Entry。传统做法是手动在配置里写死每个入口,每新增一个页面就要改配置

用 glob 遍历所有目录

javascript 复制代码
// webpack.base.js
const pageEntries = {};
const htmlWebpackPluginList = [];

glob
  .sync(path.resolve(process.cwd(), "./app/pages/**/entry.*.js"))
  .forEach((file) => {
    const entryName = path.basename(file, ".js");
    pageEntries[entryName] = file;
    htmlWebpackPluginList.push(
      new HtmlWebpackPlugin({
        filename: path.resolve(
          process.cwd(),
          `./app/public/dist/${entryName}.tpl`,
        ),
        template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
        chunks: [entryName],
      }),
    );
  });

约定规则:在 app/pages/ 下任意目录,只要文件名符合 entry.{pageName}.js 的格式,就会被自动识别为入口。

每个入口同时会生成一个 .tpl 模板文件。这个模板是给 Koa 服务端用的------用户访问 /view/page1 时,Koa 通过 Nunjucks 渲染 entry.page1.tpl,Webpack 已经把打包后的 JS/CSS 注入到了这个模板里。

javascript 复制代码
// app/controller/view.js --- Koa 服务端渲染页面
async renderPage(ctx) {
  await ctx.render(`dist/entry.${ctx.params.page}`, {
    name: app.options?.name,
    env: app.env.get(),
  });
}
三、模块解析 module

用于解析不同模块下的依赖,把它转成浏览器可识别的语言,例如:

  • vue --> vue-loader
  • less --> less-loader
  • css --> css-loader
  • js --> bebal-loader
  • png --> file-loader

需要注意 :webpack 进行编译时,需要确保已经下载相关loader,因为 webpack 会自动从当前项目中的 node_modules 里面去找,如果找不到可能会 报错!

javascript 复制代码
module.exports = {
  // 模块解析配置(决定了要加载解析哪些模块,以及用什么方式去解析)
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader'
        }
      },
      {
        test: /\.js$/,
        include: [
          // 只对业务代码进行bebel,加快 webpack 打包速度
          path.resolve(process.cwd(), './app/pages')
        ],
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 300,
            esModule: false
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
        use: 'file-loader'
      }
    ]
  },
四、 模块解析设置别名 resolve
javascript 复制代码
resolve: {
    // 解析路径时,自动添加的扩展名
    extensions: ['.js', '.vue', '.less', '.css'],
    // 设置别名,方便后续开发
    alias: {
      $pages: path.resolve(process.cwd(), './app/pages'),
      $common: path.resolve(process.cwd(), './app/pages/common'),
      $widgets: path.resolve(process.cwd(), './app/pages/widgets'),
      $store: path.resolve(process.cwd(), './app/pages/store')
    }
  },
五、产物输出 output

output 是用来配置在不同环境下,产物输出的名称以及文件存放的路径,但是不同的环境下,所需要的内容又是不同的

开发环境 webpack.dev.js

javscript 复制代码
const webpackConfig = merge.smart(baseConfig, {
 // 开发环境 output 配置
 output: {
   filename: 'js/[name]_[chunkhash:8].bundle.js',
   path: path.resolve(process.cwd(), './app/public/dist/dev/'), // 输出文件存储路径
   publicPath: `http://${HOST}:${PORT}/public/dist/dev/`, // 外部资源公共路径
   globalObject: 'this' // 全局对象,用于在浏览器中访问 webpack 打包后的代码
 }
})

生产环境 webpack.prod.js

javascript 复制代码
module.exports = {
   output: {
   // 输出文件名;[name] 文件名称,[chunkhash:8] 哈希值8位
   filename: 'js/[name]_[chunkhash:8].bundle.js',
   // 产物输出目录
   path: path.join(process.cwd(), './app/public/dist/prod'),
   // 静态资源路径
   publicPath: '/dist/prod',
   // 让跨域资源获得正确的跨域权限
   crossOriginLoading: 'anonymous'
 }
}

filename 用于指定输出文件的命名格式,通常搭配 8 位哈希值,以此区分不同版本的文件。这样可以有效规避浏览器缓存问题,当文件内容未变更时,哈希值保持不变,浏览器会直接复用缓存资源。

path 与 publicPath 的作用会根据环境有所不同:

  • 开发环境 :path 是编译产物的本地输出目录;publicPath 是静态资源的公共访问路径,供 webpack-dev-middleware 使用,配合 webpack-hot-middleware 实现 HMR 热更新。
  • 生产环境:两者主要用于定义生产构建产物的输出目录与静态资源的存放路径
六、 Plugins配置

Loader 处理单个文件,Plugin 则作用于整个构建流程。

javascript 复制代码
plugins: [
  // 1. 必须:让 vue-loader 工作
  new VueLoaderPlugin(),

  // 2. 全局注入:业务代码中不需要 import 就能用 Vue、axios、lodash
  new webpack.ProvidePlugin({
    Vue: "vue",
    axios: "axios",
    _: "lodash",
  }),

  // 3. 定义编译时常量:Vue 3 的特性标志
  new webpack.DefinePlugin({
    __VUE_OPTIONS_API__: 'true',
    __VUE_PROD_DEVTOOLS__: 'false',
    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
  }),

  // 4. 每个入口生成对应的 HTML 模板
  ...htmlWebpackPluginList,
];

开发环境

看自己的需求来配置,但是如果要实现HMR的话,就需要做一下配置

javascript 复制代码
// 开发环境 webpack 配置
const webpackConfig = merge.smart(baseConfig, {
    // 开发阶段插件
    plugins: [
      // HotModuleReplacementPlugin 用于实现模块热替换(Hot Module Replacement 简称 HMR)
      // 模块热替换允许在应用程序运行时替换
      // 极大的提升开发效率,应为能在不刷新页面的情况下更新模块
      new webpack.HotModuleReplacementPlugin({
        multiStep: false // 是否启用 multiStep 模式,默认为 false,启用后会在编译完成后等待一段时间再进行下一轮编译,适用于大型项目
      })
    ]
})

生产环境

javascript 复制代码
 // 每次build 前, 清空 public/dist 目录
 const webpackConfig = merge.smart(baseConfig, {
    .....
    plugins:[
     new CleanWebpackPlugin(["public/dist"], {
      root: path.resolve(process.cwd(), "./app"),
      extends: [],
      verbose: true,
      dry: false,
    }),
    // 提取 css 的公共部分,有效利用缓存
    new MinCssExtractPlugin({
      chunkFilename: "css/[name]_[contenthash:8].bundle.css",
    }),
    // 优化并压缩 css 资源
    new CssMinimizerPlugin(),
    // 多线程打包 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,
          },
        },
      ],
    }),
    // 浏览器在请求资源不发送用户的身份凭证
    new HtmlWebpackInjectAttributesPlugin(),
    ]
    })
七、打包优化 optimization

webpack.base.js

javascript 复制代码
// 配置打包输出优化(代码分割, 模块合并, 缓存, TreeShaing, 压缩等优化策略)
  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[\\/]/, // 打包 node_modules 中的文件
          name: "vendor", // 模块名称
          priority: 20, // 优先级, 数字越大优先级越高
          enforce: true, // 强制执行
          reuseExistingChunk: true, // 复用已有的公共 chunk
        },
        common: {
          // 公共模块
          name: "common",
          minChunks: 2, // 被两处引用即被归为公共模块
          minSize: 1, // 最小分割文件大小(1 byte)
          priority: 10, // 优先级,
          reuseExistingChunk: true, // 复用已有的公共 chunk
        },
      },
    },
    // 将 webpack 生成的 runtime 代码提取到单独的文件中
    runtimeChunk: true,
  },

webpack.prod.js

javascript 复制代码
optimization: {
    // 使用 TerserPlugin 的并发和缓存,提升压缩阶段的性能
    // 清除console.log
    minimizer: true,
    minimizer: [
      new TerserPlugin({
        cache: true, // 启用缓存来加速构建过程
        parallel: true, // 利用多核 CPU 的优势来加快压缩速度
        terserOptions: {
          compress: {
            drop_console: true, // 移除所有的 console 语句
          },
        },
      }),
    ],
  },
八、 HMR 热模块替换:修改代码不刷新页面

利用 Express 搭建本地开发 devServer,搭配 webpack-dev-middleware 和 webpack-hot-middleware 中间件 实现热更新HMR

webpack-dev-middleware(编译 + 静态服务)

  1. 实时编译:监听文件变化,自动重新编译 Webpack 代码
  2. 内存存储 :把编译后的文件存在内存里,不写入硬盘(速度极快)
  3. 静态服务:把内存中的编译结果,作为静态资源暴露给浏览器访问
  4. 基础依赖 :是热更新中间件的前置依赖,没有它就无法运行热更新

只负责 编译 + 提供文件在 内存中访问,不负责热更新。

webpack-hot-middleware(热模块替换 HMR)

  1. 热更新通信:和浏览器建立 WebSocket 长连接,通知文件变化
  2. 热模块替换 :不刷新整个页面,只替换修改的代码模块
  3. 状态保留:修改代码后,页面表单输入、组件状态不会丢失

负责 「热更新」 ,让你改代码后页面无需刷新

webpack.dev.js的配置

开发阶段的 entry 配置 需要加入 hmr

javascript 复制代码
Object.keys(baseConfig.entry).forEach(v => {
  // 第三方包不作为 hmr 入口
  if (v !== 'vendor') {
    baseConfig.entry[v] = [
      // 主入口文件
      baseConfig.entry[v],
      // hmr 更新入口,官方指定的 hmr 路径
      `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}&timeout=${TIMEOUT}&reload=true`
    ]
  }
})

同时在配上热更新的插件 就可以在本地热更新了

javascript 复制代码
// 开发阶段插件
  plugins: [
    // HotModuleReplacementPlugin 用于实现模块热替换(Hot Module Replacement 简称 HMR)
    // 模块热替换允许在应用程序运行时替换
    // 极大的提升开发效率,应为能在不刷新页面的情况下更新模块
    new webpack.HotModuleReplacementPlugin({
      multiStep: false // 是否启用 multiStep 模式,默认为 false,启用后会在编译完成后等待一段时间再进行下一轮编译,适用于大型项目
    })
  ]
九、 总结

深入学习 Webpack 工程化后,我对前端工程化体系有了更深层次的理解。正如哲哥所说,我们学习的从来不是单纯会用工具,而是要学会思考:打包的本质是什么、该如何设计构建流程、以及整个打包构建过程都需要什么东西!!

相关推荐
liangshanbo12154 天前
面试题:Webpack 的 publicPath 有什么作用?
前端·webpack·node.js
liangshanbo12156 天前
Webpack vs Vite 底层原理高频追问的问题
前端·webpack·node.js
liangshanbo12156 天前
面试题:AI 对话上下文超限怎么办?
面试·webpack·devops
liangshanbo12157 天前
Webpack 文件指纹面试题整理
前端·webpack·node.js
liangshanbo12157 天前
Webpack 常见 Plugin 面试题
前端·webpack·node.js
liangshanbo12157 天前
Webpack Tree Shaking 面试题整理
前端·webpack·node.js
陪我一起学编程7 天前
vue3笔记
前端·webpack·vue3·vite·脚手架·vue3笔记·组件式
liangshanbo12157 天前
Webpack的分包策略面试题
前端·webpack·node.js
liangshanbo12158 天前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
晚风醉蝶10 天前
webpack 模块提取
前端·webpack·node.js·ast·逆向分析