likes
comments
collection
share

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

作者站长头像
站长
· 阅读数 12

目录

  1. 前言
  2. webpack包分析工具
  3. 抽取css样式文件
  4. 压缩css文件
  5. 压缩js文件
  6. 合理配置打包文件hash
  7. 代码分割第三方包和公共模块
  8. tree-shaking清理未引用js
  9. tree-shaking清理未使用css
  10. 打包时生成gzip文件 11.总结
  11. 参考

前言

本文是专栏【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目系列第四篇,会详细讲解优化构建结果:构建结果分析,抽离css文件,压缩cssjs文件,hash合理配置,代码分割,tree-shaking清理cssjs,打包生成gzip等优化配置。

本系列文章将使用最新的webpack5一步一步从零搭建一个完整的vue3+ts开发和打包环境,配置完善构建速度构建结果的优化配置,以及配置完善的代码规范和git提交规范。完整代码已上传到webpack5-vue3-ts

  1. 代码格式规范:editorconfig统一编辑器配置,prettier自动格式化代码,stylelint规范样式和保存自动修复,代码提交自动格式化cssjs代码等。
  2. 代码语法规范:eslint检测js代码语法,style-lint检测样式代码语法,使用tsc检测类型报错,lint-staged按需检测代码等。
  3. git提交规范:代码提交时husky检测代码语法规范,代码提交时husky检测commit备注规范,commitizen配置commit辅助信息等。

全系列概览

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

二. 优化构建结果文件

2.1 webpack包分析工具

webpack-bundle-analyzer是分析webpack打包后文件的插件,使用交互式可缩放树形图可视化 webpack 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖:

sh
复制代码
npm install webpack-bundle-analyzer -D

修改webpack.analy.js

js
复制代码
// webpack.analy.js
const prodConfig = require('./webpack.prod.js')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const { merge } = require('webpack-merge')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件
module.exports = smp.wrap(merge(prodConfig, {
  plugins: [
    new BundleAnalyzerPlugin() // 配置分析打包结果插件
  ]
}))

配置好后,执行npm run build:analy命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小。

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

2.2 抽取css样式文件

在开发环境我们希望css嵌入在style标签里面,方便样式热替换,但打包时我们希望把css单独抽离出来,方便配置缓存策略。而插件mini-css-extract-plugin就是来帮我们做这件事的,安装依赖:

sh
复制代码
npm i mini-css-extract-plugin -D

修改webpack.base.js, 根据环境变量设置开发环境使用style-looader,打包模式抽离css

js
复制代码
// webpack.base.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
  // ...
  module: { 
    rules: [
      // ...
      {
        test: /.css$/, //匹配所有的 css 文件
        include: [path.resolve(__dirname, '../src')],
        use: [
          // 开发环境使用style-looader,打包模式抽离css
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /.less$/, //匹配所有的 less 文件
        include: [path.resolve(__dirname, '../src')],
        use: [
          // 开发环境使用style-looader,打包模式抽离css
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      },
    ]
  },
  // ...
}

再修改webpack.prod.js, 打包时添加抽离css插件

js
复制代码
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
  mode: 'production',
  plugins: [
    // ...
    // 抽离css插件
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].css' // 抽离css的输出目录和名称
    }),
  ]
})

配置完成后,在开发模式css会嵌入到style标签里面,方便样式热替换,打包时会把css抽离成单独的css文件。

2.3 压缩css文件

上面配置了打包时把css抽离为单独css文件的配置,打开打包后的文件查看,可以看到默认css是没有压缩的,需要手动配置一下压缩css的插件。

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

可以借助css-minimizer-webpack-plugin来压缩css,安装依赖

sh
复制代码
npm i css-minimizer-webpack-plugin -D

修改webpack.prod.js文件, 需要在优化项optimization下的minimizer属性中配置

js
复制代码
// webpack.prod.js
// ...
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
  // ...
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(), // 压缩css
    ],
  },
}

再次执行打包就可以看到css已经被压缩了。

2.4 压缩js文件

设置modeproduction时,webpack会使用内置插件terser-webpack-plugin压缩js文件,该插件默认支持多线程压缩,但是上面配置optimization.minimizer压缩css后,js压缩就失效了,需要手动再添加一下,webpack内部安装了该插件,由于pnpm解决了幽灵依赖问题,如果用的pnpm的话,需要手动再安装一下依赖。

sh
复制代码
npm i terser-webpack-plugin -D

修改webpack.prod.js文件

js
复制代码
// ...
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
  // ...
  optimization: {
    minimizer: [
      // ...
      new TerserPlugin({ // 压缩js
        parallel: true, // 开启多线程压缩
        terserOptions: {
          compress: {
            pure_funcs: ["console.log"] // 删除console.log
          }
        }
      }),
    ],
  },
}

配置完成后再打包,cssjs就都可以被压缩了。

2.5 合理配置打包文件hash

项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而hash就是浏览器缓存策略很重要的一部分。webpack打包的hash分三种:

  • hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash
  • chunkhash:不同的入口文件进行依赖文件解析、构建对应的chunk,生成对应的哈希值,文件本身修改或者依赖文件修改,chunkhash值会变化
  • contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash

hash是在输出文件时配置的,格式是filename: "[name].[chunkhash:8][ext]" , [xx] 格式是webpack提供的占位符, :8是生成hash的长度。

占位符解释
ext文件后缀名
name文件名
path文件相对路径
folder文件所在文件夹
hash每次构建生成的唯一 hash 值
chunkhash根据 chunk 生成 hash 值
contenthash根据文件内容生成hash 值

因为js我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以js适合使用chunkhash

css和图片资源媒体资源一般都是单独存在的,可以采用contenthash,只有文件本身变化后会生成新hash值。

修改webpack.base.js,把js输出的文件名称格式加上chunkhash,把css和图片媒体资源输出格式加上contenthash

js
复制代码
// webpack.base.js
// ...
module.exports = {
  // 打包文件出口
  output: {
    filename: 'static/js/[name].[chunkhash:8].js', // // 加上[chunkhash:8]
    // ...
  },
  module: {
    rules: [
      {
        test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
        // ...
        generator:{ 
          filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
        },
      },
      {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体文件
        // ...
        generator:{ 
          filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
      {
        test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
        // ...
        generator:{ 
          filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8]
        },
      },
    ]
  },
  // ...
}

再修改webpack.prod.js,修改抽离css文件名称格式

js
复制代码
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
  mode: 'production',
  plugins: [
    // 抽离css插件
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css' // 加上[contenthash:8]
    }),
    // ...
  ],
  // ...
})

再次打包就可以看到文件后面的hash

2.6 代码分割第三方包和公共模块

一般第三方包的代码变化频率比较小,可以单独把node_modules中的代码单独打包, 当第三包代码没变化时,对应chunkhash值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积, webpack提供了代码分隔功能, 需要我们手动在优化项optimization中手动配置下代码分隔splitChunks规则。

修改webpack.prod.js

js
复制代码
module.exports = {
  // ...
  optimization: {
    // ...
    splitChunks: { // 分隔代码
      cacheGroups: {
        vendors: { // 提取node_modules代码
          test: /node_modules/, // 只匹配node_modules里面的模块
          name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
          minChunks: 1, // 只要使用一次就提取出来
          chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0, // 提取代码体积大于0就提取出来
          priority: 1, // 提取优先级为1
        },
        commons: { // 提取页面公共代码
          name: 'commons', // 提取文件命名为commons
          minChunks: 2, // 只要使用两次就提取出来
          chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0, // 提取代码体积大于0就提取出来
        }
      }
    }
  }
}

配置完成后执行打包,可以看到node_modules里面的模块被抽离到vendors.6aeb3c4c.js中,业务代码在main.f70933df.js中。

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

测试一下,此时verdors.jschunkhash6aeb3c4c.js,main.js文件的chunkhash是f70933df,改动一下App.vue,再次打包,可以看到下图main.js的chunkhash值变化了,但是vendors.js的chunkhash还是原先的,这样发版后,浏览器就可以继续使用缓存中的verdors.ec725ef1.js,只需要重新请求main.js就可以了。

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

2.7 tree-shaking清理未引用js

Tree Shaking的意思就是摇树,伴随着摇树这个动作,树上的枯叶都会被摇晃下来,这里的tree-shaking在代码中摇掉的是未使用到的代码,也就是未引用的代码,最早是在rollup库中出现的,webpack2版本之后也开始支持。模式modeproduction时就会默认开启tree-shaking功能以此来标记未引入代码然后移除掉,测试一下。

src/components目录下新增Demo1,Demo2两个组件

vue
复制代码
// src/components/Demo1.vue
<template>
  我是Demo1组件
</template>

// src/components/Demo2.vue
<template>
  我是Demo2组件
</template>

再在src/components目录下新增index.ts, 把Demo1Demo2组件引入进来再暴露出去

ts
复制代码
// src/components/index.ts
export { default as Demo1 } from './Demo1'
export { default as Demo2 } from './Demo2'

App.vue中引入两个组件,但只使用Demo1组件

vue
复制代码
<template>
  <img :src="smallImg" alt="小于10kb的图片" />
  <img :src="bigImg" alt="大于于10kb的图片" />
  <!-- 小图片背景容器 -->
  <div className='smallImg'></div>
  <!-- 大图片背景容器 -->
  <div className='bigImg'></div>
  <div> 修改App.vue</div>
  <!-- 使用Demo1组件 -->
  <Demo1 />
</template>

<script setup lang="ts">
  import smallImg from './assets/imgs/5kb.png'
  import bigImg from './assets/imgs/22kb.png'
  import './app.css'
  import './app.less'
  import { Demo1, Demo2 } from '@/components' //引入Demo1和Demo2组件
</script>

<style scoped>
</style>

再次执行npm run build:dev打包,可以看到在main.js中搜索Demo,只搜索到了Demo1, 代表Demo2组件被tree-shaking移除掉了。

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

2.8 tree-shaking清理未使用css

js中会有未使用到的代码,css中也会有未被页面使用到的样式,可以通过purgecss-webpack-plugin插件打包的时候移除未使用到的css样式,这个插件是和mini-css-extract-plugin插件配合使用的,在上面已经安装过,还需要glob-all来选择要检测哪些文件里面的类名和id还有标签名称, 安装依赖:

sh
复制代码
npm i purgecss-webpack-plugin glob-all -D

修改webpack.prod.js

js
复制代码
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const globAll = require('glob-all')
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
module.exports = {
  // ...
  plugins: [
    // 抽离css插件
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:8].css'
    }),
    // 清理无用css
    new PurgeCSSPlugin({
      // 检测src下所有vue文件和public下index.html中使用的类名和id和标签名称
      // 只打包这些文件中用到的样式
      paths: globAll.sync([
        `${path.join(__dirname, '../src')}/**/*.vue`,
        path.join(__dirname, '../public/index.html')
      ]),
    }),
  ]
}

测试一下, 现在App.vue中有两个div,类名分别是smallImgbigImg,当前app.less代码为

less
复制代码
#root {
  .smallImg {
    width: 69px;
    height: 75px;
    background: url('./assets/imgs/5kb.png') no-repeat;
  }
  .bigImg {
    width: 232px;
    height: 154px;
    background: url('./assets/imgs/22kb.png') no-repeat;
  }
}

此时先执行一下打包,查看main.css

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

因为页面中有 smallImgbigImg类名,所以打包后的css也有,此时修改一下app.less中的 .smallImg.smallImg1,后面加一个1,这样 .smallImg1就是无用样式了,因为没有页面没有类名为 .smallImg1的节点,再打包后查看 main.css

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

可以看到main.css已经没有 .smallImg1类名的样式了,做到了删除无用css的功能。

但是purgecss-webpack-plugin插件不是全能的,由于项目业务代码的复杂,插件不能百分百识别哪些样式用到了,哪些没用到,所以请不要寄希望于它能够百分百完美解决你的问题,这个是不现实的。

插件本身也提供了一些白名单safelist属性,符合配置规则选择器都不会被删除掉,比如使用了组件库element-plus, purgecss-webpack-plugin插件检测src文件下vue文件中使用的类名和id时,是检测不到在src中使用element-plus组件的类名的,打包的时候就会把element-plus的类名都给过滤掉,可以配置一下安全选择列表,避免删除element-plus组件库的前缀el-

js
复制代码
new PurgeCSSPlugin({
  // ...
  safelist: {
    standard: [/^el-/], // 过滤以el-开头的类名,哪怕没用到也不删除
  }
})

2.9 打包时生成gzip文件

前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用gzip压缩,现在大部分浏览器和服务器都支持gzip,可以有效减少静态资源文件大小,压缩率在 70% 左右。

nginx可以配置gzip: on来开启压缩,但是只在nginx层面开启,会在每次请求资源时都对资源进行压缩,压缩文件会需要时间和占用服务器cpu资源,更好的方式是前端在打包的时候直接生成gzip资源,服务器接收到请求,可以直接把对应压缩好的gzip文件返回给浏览器,节省时间和cpu

webpack可以借助compression-webpack-plugin 插件在打包时生成 gzip 文章,安装依赖

sh
复制代码
npm i compression-webpack-plugin -D

添加配置,修改webpack.prod.js

js
复制代码
// ...
const CompressionPlugin  = require('compression-webpack-plugin')
module.exports = {
  // ...
  plugins: [
     // ...
     new CompressionPlugin({
      test: /.(js|css)$/, // 只生成css,js压缩文件
      filename: '[path][base].gz', // 文件命名
      algorithm: 'gzip', // 压缩格式,默认是gzip
      test: /.(js|css)$/, // 只生成css,js压缩文件
      threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
      minRatio: 0.8 // 压缩率,默认值是 0.8
    })
  ]
}

配置完成后再打包,可以看到打包后js的目录下多了一个 .gz 结尾的文件

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

因为只有verdors.js的大小超过了10k, 所以只有它生成了gzip压缩文件,借助serve -s dist启动dist,查看verdors.js加载情况

【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目(四)

可以看到verdors.js的原始大小是61kb, 使用gzip压缩后的文件只剩下了23kb,减少了**70%**左右 的大小,可以极大提升页面初始加载速度,也能减轻服务器压力。

总结

到目前为止已经使用webpack5vue3+ts的基本构建环境配置完成,并且配置比较常见的优化构建速度构建结果的配置,完整代码已上传到webpack5-vue3-ts 。还有细节需要优化,比如把容易改变的配置单独写个config.js来配置,输出文件路径封装。这篇文章只是配置,如果想学好webpack,还需要学习webpack的构建原理以及loaderplugin的实现机制。

后续会继续更新代码格式规范, 代码语法规范,git提交规范,集成vue-router,pinia,element-ui等基础项目等配置。

附上上面安装依赖的版本

json
复制代码
"dependencies": {
    "vue": "^3.3.4"
  },
  "devDependencies": {
    "@babel/core": "^7.22.1",
    "@babel/preset-typescript": "^7.21.5",
    "autoprefixer": "^10.4.14",
    "babel-loader": "^9.1.2",
    "compression-webpack-plugin": "^10.0.0",
    "copy-webpack-plugin": "^11.0.0",
    "cross-env": "^7.0.3",
    "css-loader": "^6.8.1",
    "css-minimizer-webpack-plugin": "^5.0.1",
    "glob-all": "^3.3.1",
    "html-webpack-plugin": "^5.5.1",
    "less": "^4.1.3",
    "less-loader": "^11.1.2",
    "mini-css-extract-plugin": "^2.7.6",
    "postcss-loader": "^7.3.2",
    "purgecss-webpack-plugin": "^5.0.0",
    "speed-measure-webpack-plugin": "^1.5.0",
    "style-loader": "^3.3.3",
    "terser-webpack-plugin": "^5.3.9",
    "thread-loader": "^4.0.2",
    "vue-loader": "^17.2.2",
    "webpack": "^5.85.1",
    "webpack-bundle-analyzer": "^4.9.0",
    "webpack-cli": "^5.1.3",
    "webpack-dev-server": "^4.15.0",
    "webpack-merge": "^5.9.0"
  }

参考

  1. webpack官网
  2. babel官网
  3. 【万字】透过分析 webpack 面试题,构建 webpack5.x 知识体系
  4. Babel 那些事儿
  5. 阔别两年,webpack 5 正式发布了!