里程碑2:基于 webpack5 完成工程化建设

基于 webpack5 对工程化的理解

一、什么是前端工程化

  前端工程化,是指将传统手动式的前端开发 ,转变为标准化、自动化、模块化、可维护、可扩展的,现代主流工程模式 。它不再局限于编写页面,而是通过工具链、规范体系、构建流程、协作机制,将前端开发从编码 → 校验 → 构建 → 测试 → 部署 → 监控,形成完整自动化的流程

二、前端工程化的意义

传统开发模式 与 现代开发模式 对比:

开发流程 传统开发模式 现代开发模式
前期准备 无需安装额外工具,直接手写HTML、CSS、JS,无需配置,打开浏览器即可编写 需安装构建工具(Webpack/Vite)、代码校验工具,配置环境变量、依赖管理等
开发过程 无模块化,代码直接书写,公共部分复制粘贴;无热更新,修改后需手动刷新浏览器 模块化开发,组件可复用;支持热更新(HMR),修改代码实时生效,无需手动刷新
代码规范 无统一规范,代码风格、缩进、命名全凭个人习惯,多人协作易混乱 通过ESLint、Prettier、husky等工具,统一代码风格、命名规范、缩进格式,强制标准化
依赖管理 手动下载第三方库,手动引入,无版本管理,更新需替换文件 用npm/pnpm管理依赖,一键安装、更新、降级,自动处理依赖关系
上线优化 人工合并代码、压缩图片,无系统优化,全靠手动操作,易出错 构建工具自动完成代码压缩、分包、缓存优化,无需人工干预,性能更有保障
部署 纯人工部署,具体步骤: 1. 人工完成代码、图片压缩优化; 2. 下载FTP工具(如FlashFXP); 3. 输入服务器IP、账号、密码连接服务器; 4. 手动选中本地所有前端文件(HTML、CSS、JS、图片等); 5. 拖拽上传至服务器指定目录; 6. 上传完成后,手动在浏览器访问服务器地址,校验部署是否成功 自动化部署,依据CI/CD工作流程,步骤: 1. 代码提交至Git仓库; 2. 触发自动构建、自动测试; 3. 测试通过后,自动将构建后的dist文件部署至目标服务器; 4. 部署完成后自动校验,全程无需人工干预,可追溯部署记录
维护 只要涉及代码改动,都需要重新手动打包,重新走 123456 部署流程 上传代码后,靠构建工具,实现自动化部署,依据CI/CD工作流程

  根据表格开发流程的对比,可想而知前端工程化的重要性,使用前端工程化开发流程的好处就是,提升了开发的效率,保证了代码的质量(统一性),上线流程的优化,降低维护成本等。

三、根据 webpack5 理解工程化

  实现工程化建设的工具有很多,例如:Webpack 、 Vite 、Rollup 、Parcel 、esbuild 等。在这里,选择了 webpack 这个构建工具主要不并不是为了学习如何使用这个工具,而是 webpack 的功能全面 且 生态强大,可以更好地、更方便地让我们了解和学习 工程化的思想 与 搭建流程。

  打包构建的工具,一切都是围绕三个方向去实现:解析编译模块、模块分包、压缩优化,想要达到这些效果就需要做不同配置去实现

基础配置

  一个基础的 webpack 配置会有哪些 ?包括但不限于以下几点:

1、模式 mode

  开发模式(development) 和 生产模式(production),但是一般不会在基础配置文件 webpack.base.js 中去指定具体的模式,而是分别在 webpack.dev.js 和 webpack.prod.js 中定义好,在不同的环境下使用时再合并进 webpack.base.js

这里只举一个例子:webpack.dev.js

js 复制代码
// 开发环境 webpack 配置
const merge = require('webpack-merge')
const webpackConfig = merge.smart(baseConfig, {
  // 指定开发环境模式
  mode: 'development'
})
2、入口 entry

  入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

js 复制代码
module.exports = {
  // 入口配置:
  entry: './index.js' (例子),
}

由于此次学习的项目中涉及到多页面:

  • 找出所有的入口文件
  • 利用 glob 遍历所有入口文件,提取文件名作为 入口名称
js 复制代码
const glob = require('glob')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

// 获取所有 app/pages 目录下所有入口文件 (entry.xx.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'),
      // 要注入的代码块(对应 entry 配置中的 key)
      chunks: [entryName]
    })
  )
})

module.exports = {
  // 入口配置:
  entry: pageEntries,
}
3、模块解析 module

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

  • vue --> vue-loader
  • less --> less-loader
  • css --> css-loader
  • js --> bebal-loader
  • png --> file-loader
js 复制代码
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'
      }
    ]
  },
4、产物输出 output

  output 是用来配置在不同环境下,产物输出的名称以及文件存放的路径,但是不同的环境下,所需要的内容又是不一样的,这一点需要我们注意!

开发环境 webpack.dev.js

js 复制代码
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

js 复制代码
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

  其中 filename 是指定输出文件名的格式,一般利用哈希值8位来区分文件命名,有利于避免缓存问题,代码没有更新的情况。

path 和 publicPath

   在 开发环境 中,path 是构建编译出开发环境产物输出目录,publicPath 是外部资源公共路径,便于 webpack-dev-middleware 中间件使用该路径作为静态资源路径,监听文件的改动,配合 webpack-hot-middleware 中间件 实现 HRM 热更新

   在 生产环境 中,path 和 publicPath 仅是为了构建编译出生产环境产物输出目录,静态资源存放路径。

5、插件 plugins

   对于我自己的理解,插件的作用是为了更好地去做一些扩展功能的配置,解决一些 module无法处理的工作,以及加快打包速度等。

基础的插件配置

涉及到对一些模块功能的解析,例如:一些特定的文件、第三方库等

下面的例子是对vue做一些相应的处理

js 复制代码
module.exports = {
  // 配置 webpack 插件
plugins: [
  // 处理 .vue文件, 这个插件是必须的
  // 它的职能是将你定义过的其他规则复制并应用到 .vue文件里
  // 例如,有一条匹配规则 /\.js$/ 规则, 那么它会应用到 .vue文件中的 <script> 板块中
  new VueLoaderPlugin(),
  // 把第三方库暴露到 window context下
  new webpack.ProvidePlugin({
    Vue: 'vue',
    axios: 'axios',
    _: 'lodash'
  }),
  // 定义全局常量
  new webpack.DefinePlugin({
    __VUE_OPTIONS_API__: true, // 支持 vue 解析 optionsApi
    __VUE_PROD_DEVTOOLS__: false, // 禁用 vue 调试工具
    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false // 禁用生产环境显示 '水合' 信息
  }),
  // 构造最终页面渲染的页面文件
  ...htmlWebpackPluginList
 ],
}

开发环境

一般看需求而定,但是如果要实现HMR的话,就需要做一下配置:

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

生产环境

   需要确保每次打包后的文件都是新的,对一些功能模块进行额外的处理,例如提取css,加快代码构建速度,进行多线程打包等。因为插件都是返回一个方法,所以使用插件的时候,都需要 New 一下

  • clean-webpack-plugin:每次 build 前清理 public/dist 目录
  • mini-css-extract-plugin :提取 css 到单独的文件
  • happypack:webpack4 多线程打包插件
  • thread-loader:webpack5 多线程打包
6、打包优化 optimization

   optimization 是用来配置一些打包优化的配置。通常需要对代码进行分块打包,这中间涉及一些第三方库的依赖(node_modules) ,公共模块等。并且,打生产包的时候,往往还需要对代码进行一个压缩(js、css),清除一些console.log 等内容:

js 复制代码
optimization: {
    ----------------------------- 基本构建所需 -----------------------------
    /**
     * 把 js 文件 打包成 3种 类型
     * 1. vendor: 第三方 lib 库, 基本不会改动,除非依赖版本升级
     * 2. common: 业务组件代码的公共部分抽取出来,改动较少
     * 3. entry.{page}: 不同页面 entry 里的业务组件代码的差异部分,会经常改动
     * 目的:把改动和引用频率不一样的js 区分出来,以达到更好利用浏览器缓存的效果
     */
    splitChunks: {
      chunks: 'all', // 对不同和异步模块都进行分割
      maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
      maxInitialRequests: 10, // 入口点的最大并行请求数
      cacheGroups: {
        // 第三方依赖库
        vender: {
          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.js
    runtimeChunk: true,
    
    ------------------------------ 生产所需 -------------------------------
    
    // 开启压缩
    minimize: true,
    minimizer: [
      // 压缩 js 资源,使用 TerserWebpackPlugin 的并发和缓存,提升压缩阶段的性能
      new TerserWebpackPlugin({
        parallel: true, // 启用多进程 利用 cpu 的优势来加快压缩速度
        cache: true, // 启用缓存,提升压缩速度
        terserOptions: {
          compress: {
            drop_console: true, // 清除 console.log
            drop_debugger: true // 清除 debugger 语句
          }
        }
      }),

      // 优化并压缩 css 资源
      new cssMinimizerPlugin({
        parallel: true
      })
    ]
  }
7、优化类的模块解析命名 resolve

   用来配置模块解析的具体行为,方便 webpack 在打包时,如何找到并解析具体模块的路径

js 复制代码
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')
    }
  },

理解思路:通过定义好 入口 entry 和 出口 output ,使用 module 对模块依赖进行解析,经过配置一系列辅助构建打包的插件 plugins 和 配置打包输出优化的optimization,从提高整体打包构建速度和质量

四、实现开发环境 HMR

   此次学习,一个关键的点是学会了,如何在本地开发环境,构建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. 状态保留:修改代码后,页面表单输入、组件状态不会丢失

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

必须为客户端添加 hmr 热更新入口

js 复制代码
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`
    ]
  }
})

同时,还有热更新插件

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

五、总结

   通过学习 webpack 不同的配置,加深了对项目工程化的理解。此次学习的重点,不仅仅是为了学习一个工具的配置和使用,而是在于思考如何进行对打包进行打包 和 构建打包时所需要的东西,同时对需要打包的(js、css)内容做对应的处理,在这个基础上,再去想办法如何优化打包的效率。值得注意的是,需要对不同的环境做不同的配置,实现不同的环境干不同的事!

相关推荐
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·逆向分析