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

0 阅读10分钟

基于 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 理解工程化

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

1.png

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

基础配置

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

1、模式 mode

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

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

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

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

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

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

  • 找出所有的入口文件
  • 利用 glob 遍历所有入口文件,提取文件名作为 入口名称
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
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

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

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位来区分文件命名,有利于避免缓存问题,代码没有更新的情况。

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

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

image.png

5、插件 plugins

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

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

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

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的话,就需要做一下配置:

// 开发环境 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 等内容:

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 在打包时,如何找到并解析具体模块的路径

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-middlewarewebpack-hot-middleware 中间件 实现热更新HMR

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

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

只负责**编译 + 提供文件 **,不负责热更新。

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

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

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

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

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`
    ]
  }
})

同时,还有热更新插件

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

五、总结

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