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

一、什么是前端工程化

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

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

一、整体架构

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

webpack.base.js 放所有环境共用的配置,webpack.dev.jswebpack.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 位哈希值,以此区分不同版本的文件。这样可以有效规避浏览器缓存问题,当文件内容未变更时,哈希值保持不变,浏览器会直接复用缓存资源。

pathpublicPath 的作用会根据环境有所不同:

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

相关推荐
Hello--_--World3 天前
Webpack:Webpack 核心配置、什么是 Loader? 什么是plugin?webpack 构建流程
前端·webpack·node.js
前端若水4 天前
安装 markdown-it 后项目报错,可能是 Vue/Webpack 项目中 Quill 的问题(ES6+ 语法不支持)
vue.js·webpack·es6
李白的天不白4 天前
代码引用错误和性能优化建议。
webpack
Beginner x_u5 天前
前端八股整理(工程化 02)|CommonJS/ESM、Webpack Loader/Plugin 与Vite 对比
前端·webpack·node.js·plugin·loader
tzy2335 天前
梳理一下前端模块化规范:CommonJS ESM AMD CMD UMD
前端·webpack·cmd·commonjs·amd·esm·umd
李白的天不白8 天前
webpack 与 webpack-cli 版本匹配问题
前端·webpack·node.js
李白的天不白8 天前
webpack 与axios 版本冲突问题
前端·webpack·node.js
李白的天不白8 天前
webpack 与 vue-loader 版本冲突问题
前端·vue.js·webpack
光影少年9 天前
Webpack打包性能优化方面的经验
前端·webpack·性能优化