基于 Webpack 对前端工程化的理解

172 阅读3分钟

前言

学习声明:本文知识体系来源于哲玄前端(抖音ID:44622831736)大前端全栈实践课程,结合个人学习实践进行整理。


什么是工程化?

在工作中我们使用打包工具的进行配置,它帮我们做了什么?它是如何监听文件的?环境不同时它是如何处理的?等等...
所以让我们思考一下知道如何配置这个工具就是工程化了吗?我想答案不全是。
我认为理解前端工程化不仅仅是利用众多的打包工具进行项目配置,而是要理解它的设计思想,只有理解了思想我们才能在众多的打包工具(比如:Webpack、Vite、Rollup...)中更好的利用它。
我认为前端工程化是将前端开发流程标准化、自动化以提高效率和维护性的一种思想。

工程化如何进行?业务文件-->解析引擎-->打包产物

举个例子
  1. 开发人员编写好项目业务组件
  2. 解析引擎首先会从入口文件(通过 entry 配置)开始一层一层读取依赖业务文件
  3. 解析引擎通过模块加载器配置(module)一系列的loader(例如:vue-loader、babel-loader、css-loader、url-loader...)配合插件加载器(plugin),输出浏览器能够识别的前端三件套(.html, .css, .js)文件
  4. 将输出的文件进行依赖分析,然后进行模块分包(splitChunks)等一系列优化
  5. 区分不同环境,进入不同优化流程的处理,比如生产环境压缩js、css、处理图 片资源 等等然后输出产物到文件系统,开发环境除了入口文件输出到文件系 统,其他的资源注 入到内存中,提高开发效率
  6. 最后通过output配置,输出产物到文件系统
    image.png 上面的流程这就是一个项目工程化的体现

多页面打包如何处理?

从上面的图示中可以知晓,我们的项目有多个入口文件,这就意味着我们需要配置多入口文件打包。我们知道多页面打包意味着需要多个入口,然后我们需要通过处理后为每个入口生成独立的文件,同时注入每个文件需要的资源,然后通过SSR的方式给到前端访问。

多入口文件

image.png

多页面入口核心配置
let htmlWebpackPluginList = [];
// 约定所有的入口文件都是 entry.xxxx.js形式
const entryPath = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryPath).forEach(file => {
  // path.basename: 获取文件名并删除后缀
  const entryName = path.basename(file, ".js");
  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]
    })
  );
});
打包产物

产物输出了多个页面的入口,这些入口文件引入了各自需要的资源,服务端通过SSR的方式返回给前端需要的具体页面。

image.png

不同环境项目如何进行工程化?

生产环境:追求加载速度快,代码安全

对于生产我们需要考虑以下问题:

  1. 产物的体积:通过压缩代码体积,删除无用代码等等减小代码体积,图片资源可以通过CDN等等处理。
  2. 分包处理:产物通过配置optimization --> splitChunks进行分包,将第三方包、公共的包达成单独的文件进行共用,合理利用缓存体速。
  3. 保证代码的安全: 可以使用代码混淆,删除注释等等。
  4. 保证代码的兼容性。
开发环境:追求开发体验,调试方便

开发环境需要考虑的问题

  1. 更方便的调试、排查问题。可以利用source-map、或者不使用代码混淆来处理这方面的问题
  2. 开发体验感更好,即热更新的处理。
模块如何分包?遵循什么原则?

模块分包作为优化的一大重要手段,可以通过webpack 的 optimization splitChunks 进行分包,在这里遵循下面的原则把 js 分为三类模块包。可根据具体得项目进行其他一些个性化配置

  1. vender: 第三方 lib 库, 基本不会改动,除非依赖的版本升级
  2. common: 业务组件代码引入的公共模块,很少改动
  3. entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,经常改动

目的:把改动较大和引用频率不多的js区分出来,以便更好的利用浏览器缓存,提高性能能

具体配置
splitChunks: {
      // 对同步异步模块都进行分割
      chunks: "all",
      // 每次异步加载的最大并行次数
      maxAsyncRequests: 10,
      // 入口点的最大并行请求数
      maxInitialRequests: 10,
      cacheGroups: {
        // 第三方包配置
        vender: {
          // 匹配第三方包,正则兼容不同平台的兼用性
          test: /[\\/]node_modules[\\/]/,
          // 模块包的名称
          name: "vender",
          // 缓存组的优先级, 数字越大,优先级越高
          priority: 20,
          // 是否强制执行
          enforce: true,
          // 是否共享缓存,即如果其他模块已经分出来这个包,那么它将被重用,不会再次分出来一样的包
          reuseExistingChunk: true
        },
        // 业务组件公共配置
        common: {
          name: 'common',
          // 模块包中引用次数,如果引用次数大于等于 minChunks,则被分割到公共包中
          minChunks: 2,
          // 模块包最小分割分拣大小,单位字节
          minSize: 1,
          priority: 10,
          reuseExistingChunk: true
        }
      }
    },

通过上述的配置最终的产物效果如下。

  • vender文件是我们在具体的文件中引用的node_nodules中的包打成的单独文件。
  • common文件是在page1.vue、page2.vue文件中共同引用的文件,在配置中我们设置minChukns:2,当包被引用2次即2次以上时就会打出来单独的文件。
  • entry.{page} 文件是把各自资源注入到这个文件的具体页面文件。

image.png

热更新(Hot Module Replacement)如何实现?

在这里我们实现热更新并非通过配置webpack 的 devServer: { hot: true }实现,而是使用express结合webpack-dev-middleware、webpack-hot-middleware 实现一个开发服务器图示实现热更新的设计

webpack-dev-middleware: 监听业务文件的变化。
webpack-hot-middleware:通过在代码中注入一段 HMR客户端 代码来通知并监听浏览器的更新。 image.png

注入的HMR代码

image.png

具体关键代码实现
/* webpack.dev.js */
// devServer配置
const DEV_SERVER_CONFIG = {
  HOST: "127.0.0.1",
  PORT: "9002",
  HMR_PATH: "__webpack_hmr",
  TIMEOUT: 20000
};

// 配置开发环境支持热更新(HMR)
Object.keys(baseConfig.entry).forEach(key => {
  // 解构
  const { HOST, PORT, HMR_PATH, TIMEOUT } = DEV_SERVER_CONFIG;
  // 第三方包不需要作为 HMR 入口
  if (key !== "vendor") {
    baseConfig.entry[key] = [
      baseConfig?.entry[key],
      // 在打包产物中注入HMR代码,webpack官方指定的 HMR 路径
      `webpack-hot-middleware/client?path=HTTp://${HOST}:${PORT}/${HMR_PATH}?timeout=${TIMEOUT}&reload=true`
    ]
  }
});

/* dev.js */
// 引入 webpack-dev-middleware,监听文件改动
// 它可以把 webpack 处理过的文件发送给 server
app.use(devMiddleware(compiler, {
  // 将模板文件写入磁盘
  writeToDisk: (filePath) => filePath.endsWith(".tpl"),
  // 资源路径,需要与 webpack.dev.js 设置的 output.publicPath 一致
  publicPath: webpackConfig.output.publicPath,

  // 设置允许跨域请求
  headers: {
    "Access-Control-Allow-Origin": "*",
    // 设置请求方法
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
    // 跨域请求中允许的 HTTP 请求信息
    "Access-ConTroll-Allow-Headers": "Content-type, Authorization, X-Requested-With"
  },
  // 彩色打印
  stats: {
    colors: true
  }
}));
// 引入 webpack-hot-middleware, 实热更新通讯
app.use(hotMiddleware(compiler, {
  log: () => { },
  path: `/${DEV_SERVER_CONFIG.HMR_PATH}`
}));

总结

前端工程化是前端开发的重要的一环,它的核心是通过模块化、自动化和优化来提高开发效率和用户体验,它扮演着基础设施的重要角色。当然,工程化不只是只有这一块,持续集成/持续部署(CI/CD)也是工程化的重要一环。

学习声明:本文知识体系来源于哲玄前端(抖音ID:44622831736)大前端全栈实践课程,结合个人学习实践进行整理。