工程化 | webpack5从0到1搭建项目环境

967 阅读13分钟

webpack5.png

注: 完整版参考:github.com/xuxinhanan/…

一、基础配置

entry 入口

webpack 资源入口,使用相对路径表示。可以是字符串、数组、对象等形式。

如果选择数组形式,那么数组的最后一个文件是资源的入口文件,数组的其余文件会被预先构建到入口文件中。

module.exports = {
  entry: ['./src/index.ts'],
}

output 出口

webpack 打包资源的出口。

module.exports = { 
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'js/[name].[fullhash].js'
  }
}

该配置项有几个重要的属性:

1. path

代表资源打包后输出的位置。该位置地址需要绝对路径。不设置则默认为 dist 目录。

2. filename

打包后生成的资源名称。并且除了是文件名称之外,也可以是相对地址。最终打包输出的文件地址是 path 绝对路径与 filename 拼接后的地址。

filename 支持类似变量的方式生成动态文件名,如[fullhash]-bundle.js

这个功能用作文件指纹策略并且在 webpack5 已经不赞成使用 [hash] 了,现在变成了 [fullhash] 或者是 [chunkhash] 或 [contenthash]。

  • [chunkhash]:是根据当前打包的 chunk 计算出来的
  • [contenthash]:主要用于计算 CSS 文件的 hash 值

除此之外,还有 [name] 等特定的动态值。 [name] 表示的是 chunk 的名字。

注:在打包过程中,一个资源入口依赖的模块集合代表一个 chunk,一个异步模块依赖的模块集合也代表一个 chunk,另外代码拆分也有单独的 chunk 生成。

补充:文件指纹策略

简单说来说文件指纹策略就是:通过给资源名称增加 hash 值来控制浏览器是否继续使用本地资源中的文件。

浏览器获得了强缓存的资源后,就会把该资源一直缓存在本地磁盘中。在下一次访问该页面的时候,对于同名资源,不会再去请求网络服务器的资源,而是直接使用本地磁盘中的。

但是,如果资源内容变化了,不想使用本地缓存了,该怎么办呢?

一个办法就是为缓存文件起一个独特的名字,只要文件内容不变,那么就一直使用该文件;而如果文件内容改变了,那么我们就用一个新的文件名,这时浏览器发现本地没有缓存该名字的文件,那么就重新向服务器请求。通过这种方式提高了强缓存的灵活性。

为了实现这个方案,我们使用 webpack 的哈希算法。也就是上面提到的 filename 动态文件名。

webpack 会根据所有的文件内容计算出一个特殊的字符串,只要文件的内容有变化,webpack 就会计算出一个新的特殊字符串。

mode 打包模式

webpack 的打包模式共有三种:production、development 和 none。

设置 mode 可以使⽤ webpack 内置的优化,默认值为 production。

image.png

resolve.alias 路径别名

通过创建 importrequire 的别名,来确保模块引入变得更简单。使用如下:

import App from "@/App.vue";

配置如下:

module.exports = {
  // ...
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "../src"),
    },
  },
  // ...
};

loader

loader,也叫做预处理器,它本质上是一个函数,它接收一个资源模块,然后将其处理成 webpack 能使用的形式。

预处理器是在配置项 module 下配置的。它有几个配置子项:

1. rules

定义了预处理器的处理规则。它是一个数组,数组的每一项都是一个 JS 对象。这个对象有两个关键属性 test 和 use。

test 是一个正则表达式,用来匹配模块文件名,匹配成功则会被 use 属性里的预处理器处理。

可以使用多个预处理器进行链式处理,处理的顺序是从后向前。

  module: {
    rules: [
      {
        test: /.(sa|sc|c)ss$/,
        use: [
          "style-loader",
          "css-loader",
          // 'postcss-loader',
          "sass-loader",
        ],
      },
    ],
  },

2. exclude 和 include

如果有一些文件不想被正则表达式匹配到的预处理器处理,那么我们可以配置 exclude 属性。

include 则相反,只对给定目录下的文件模块进行匹配。

  // 转译 ts、js
  {
    test: /.(t|j)s$/,
    exclude: /node_modules/,
    use: [
      {
        loader: "babel-loader",
      },
    ],
  },

常见 loader

  • file-loader:处理文件导入语句并替换成它的访问地址,同时把文件输出到相应位置。
  • url-loader:除支持 file-loader 的所有功能外,还增加了 Base64 编码的能力。对于文件体积小于指定值的时候,可以返回一个 Base64 编码的 data URL 来代替访问地址。好处是减少一次网络请求。
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性 (先用 css-loader 编译 css 代码,再用 style-loader 放入到网页的 style 标签里面去)
  • sass-loader:将 SCSS/SASS 代码转换成 CSS
  • vue-loader:加载 Vue.js 单文件组件

注:file-loader 、url-loader、raw-loader 在 webpack5 中已被 asset module 取代了

plugins

插件用来在 webpack 编译的某些阶段,通过调用 webpack 对外暴露除的 API 来扩展 webpack 的能力。

通常 plugins 数组的每一个元素都是插件构造函数创建出来的一个实例,根据每一个插件的特点,可能会需要向其参数里传递各种配置参数,但一般插件都哟哟默认的参数,可以免去配置工作。

clean-webpack-plugin

它是一个清除文件的插件。在每次打包后,磁盘空间都会存有打包后的资源,在再次打包的时候,将这些资源清空,来减少对磁盘空间的占用。

copy-webpack-plugin

对于一些本地资源,如图片和音视频,在打包过程中没有任何模块使用他们,但我们又想要把他们存放到打包后的资源输出目录下。这时候就可以用这个插件了。

  plugins: [
    // 处理静态文件夹 static 复制到打包的 static 文件夹
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../static"),
          to: "static",
        },
      ],
    }),
  ],

参数对象的属性 patterns 属性中的 from 用来设置从哪个文件夹赋值内容,to 属性用于设置复制到哪个文件夹去。

html-webpack-plugin

打包后的资源名称通常是由 hash 值组成,因此我们无法使用 HTML 文件来引入固定的 JS 和 CSS 等文件。该插件为我们做这个事,它自动创建 HTML 文件,并引入 JS、CSS 等文件。

同时该插件可以通过参数 template 来设置模板,以此模板来生成最终的 HTML 文件。

  plugins: [
    // 添加 html 模板
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../index.html"),
    }),
  ],

二、开发环境配置

开发环境的配置主要有:webpack-dev-server,模块热替换,source map,asset modules。

webpack-dev-server

它通过开启一个本地服务器来加载构建完成的资源文件,并且具有文件监听、代理请求等功能。其配置和说明如下:

  devServer: {
    host: "localhost", // 指定host,,改为0.0.0.0可以被外部访问
    port: 8081, // 指定 web 服务运行的端口号
    open: false, // webpack-dev-server 开启本地 web 服务后是否自动打开浏览器
    historyApiFallback: true, 
    hot: true, // 启用模块热替换HMR
    compress: true, // 设定是否为静态资源开启 Gzip 压缩
    https: false, 
    proxy: {
      // 需要代理到的真实目标服务器,可以解决前端跨域请求的问题
      "/api": "www.baidu.com",
    },
  },
  • historyApiFallback:在进行本地开发的时候,用来配置开启的本地 DevServer 服务器是否支持 HTML5 History 模式。在 HTML5 History 模式下,所有的 404 响应都会返回 index.html 的内容

source-map

source map 最初会生成一个单独的后缀名是 .map 的文件,浏览器可以通过它还原打包构建前的代码,方便调试。

通过 devtool 配置项来配置生成哪种形式的 source map。

官方文档中 devtool 的取值有二十多种,我们采用"eval-cheap-module-source-map",该配置能保留预处理器处理前的原始代码信息,并且打包速度也不慢,是一个较佳的选择。

devtool: "eval-cheap-module-source-map" 

Asset Modules

它是 webpack5 新增功能,用来取代 file-loader 等预处理器。Asset Modules 的几个主要配置项都存放在 module.rules 中:

  module: {
    rules: [
      // 处理图片资源
      {
        test: /.(png|svg|jpg|gif|cur)$/,
        type: "asset/resource",
      },
    ],
  },

关键的配置项是 type,它的值有以下四种:

  • asset/resource:与 file-loader 很像,它处理文件导入地址并将其替换成访问地址,同时把文件输出到相应位置。
  • asset/inline:与 url-loader 很像,它处理文件导入地址并将其替换为 data URL,默认是 Base64 格式编码的 url。
  • asset/source:与 raw-loader 很像,以字符串形式导出文件资源。
  • asset:在导出单独文件和 data URL 间自动选择。

三、生产环境配置

生产环境是指代码会被用户直接使用的线上正式环境。

生产环境与开发环境不同的一点就是对样式的处理。

Sass 处理

使用 sass-loader 预处理器即可。

PostCSS

一个 CSS 转换工具。用来提供 CSS 样式浏览器厂商私有前缀。

样式文件的提取

在打包构建阶段,我们使用了 style-loader 和 css-loader 来处理样式。经过 style-loader 和 css-loader 处理后的样式代码是通过 JS 逻辑动态插入到页面中的。

而在线上的生产环境中,我们需要把样式代码提取到单独的 CSS 文件里,这时需要使用 mini-css-extract-plugin 插件。

  new MiniCssExtractPlugin({
    filename: 'css/[name].[contenthash].css',
    chunkFilename: 'css/[name].[contenthash].css',
  }),
  • filename 表示同步代码里提取的 CSS 文件名称
  • chunkFilename 表示异步代码里提取的 CSS 文件名称

使用 mini-css-extract-plugin 插件需要注意,它自身带有一个预处理器,在用 css-loader 处理完 css 模块后,需要紧接着使用 MiniCssExtractPlugin.loader 这个预处理器。

  module: {
    rules: [
      {
        test: /.(sa|sc|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      },
    ],
  },

四、合并配置

开发环境和生产环境的配置有很多是相同的。针对这个问题,解决办法就是把开发环境与生产环境公共配置提取到一个单独的文件里,然后分别维护一份开发环境的配置文件和一份生产环境的配置环境,并将公共配置的 JS 代码合并到这两个文件中。

而 webpack-merge 这个工具可以用来文件合并。

五、性能优化

webpack 性能优化包括两部分,分别是开发环境的优化和生产环境的优化。

它们之间的共同目标是减少打包时间。

对于开发环境,需要针对开发者的使用体验做一些优化;而对于生产环境,还需要提升 Web 页面的加载性能。

总结:

  • 代码压缩 —— 减小文件体积,以提升页面加载速度和降低带宽消耗

  • 缩小查找范围 —— 减少不需要 webpack 处理的模块来优化打包时间

  • 代码分割 —— 因为第三方库不会经常变动,因此把它们提取出来放在一个入口里,然后单独生成一个打包后的 JS 文件,这有利于使用浏览器缓存

  • tree shaking —— 帮助我们检测模块中没有用到的代码块并移除,减少打包后的资源体积

  • 文件系统缓存 —— 极大地减少了再次编译的时间

监控构建性能工具

首先介绍两个监控构建性能的工具,分别用来监控打包体积大小和打包时间。

  • 打包体积分析工具 webpack-bundle-analyzer

    通过对打包资源文件的组成和大小进行分析,可以指导我们选择合适的优化方案进行 webpack 打包分析,例如合理分割体积过大的文件。

  • 打包速度分析工具 speed-measure-webpack-plugin

    一般来说,预处理器和插件往往占据了时间花费的主要部分,我们可以通过该工具的时间分析展示,对 webpack进行针对性优化。

压缩JS、CSS文件

压缩文件的主要目的是减小文件体积,以提升页面加载速度和降低带宽消耗等。资源压缩通常发生在生产环境打包的最后一个环节,开发环境是不需要进行压缩处理的。

在安装 webpack5 时,会自动安装 terser-webpack-plugin 插件。 然后我们只需通过 optimization 配置项来配置该插件作为压缩器进行压缩。

压缩 css 文件需在 optimization 配置项里配置 terser-webpack-plugin 插件,首先要开启 optimization.minimize。如下:

module.exports = {
  //...  
  optimization: {
    minimize: true, // 是否压缩
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin(),
    ],
  },
}

optimization.minimizer 是一个数组,用来存放压缩器。因此除了配置 JS 文件的压缩器外,还可以配置压缩 CSS 文件的压缩器 —— css-minimizer-webpack-plugin。

多进程并行压缩文件

上面两个插件还支持非常多的个性化参数,其中支持配置使用的 CPU 进程数。如:

new TerserPlugin({ parallel: 4 })

缩小查找范围

优化 webpack 打包时间的一个很直接的措施就是减少不需要 webpack 处理的模块。以下是常见的缩小查找范围的方法。

1.配置预处理器的 exclude 与 include

在使用预处理器解析模块时,通过配置项 exclude 排除不需要该预处理器解析的文件目录,include 设置该预处理器只对哪些目录生效,这样可以减少不需要被预处理器处理的文件模块,从而提升构建速度。

2.module.noParse

有些不需要被任何预处理器解析的模块,例如 jQuery 和 Lodash,可以通过配置 module.noParse 告诉 webpack 这些模块不需要被解析处理。

注意,被忽略的模块中不应有 Import 和 require 等任何模块导入语法。

module.exports = {
  //...
  module: {
    noParse: /jQuery | lodash/,
  }
}

3.resolve.modules

resolve.modules 用于配置 webpack 如何搜寻第三方模块的路径,默认是相对路径 ['node_modules'],我们使⽤绝对路径指明第三⽅模块存放的位置,以减少搜索步骤。

module.exports = {
  //...
  resolve: { 
    modules: [path.resolve(__dirname,'node_modules')] 
  }
}

4.resolve.extensions

resolve.extensions 用于 webpack 匹配文件后缀名。配置 resolve.extensions 有以下两个关键点:

  • 出现频率最⾼的后缀要放在最前⾯,以便尽快结束匹配过程。
  • 缩小数组长度,用不到的后缀名不要放到数组里。

另外在导⼊语句中,应尽可能带上后缀名,从⽽避免匹配过程。例如在确定的情况下将 require(ʼ. /data ʼ)写成 require(ʼ. /data.json ʼ)。

代码分割 SplitChunks

对于前端开发而言,很多库不会经常变动,因此完全可以把它们提取出来放在一个入口里,这样这些不经常变动的库会单独生成一个打包后的 JS 文件,这有利于使用浏览器缓存。

使用在 SplitChunks 只需在配置项 optimization.splitChunks 里进行配置。

module.exports = {
  //... 
  optimization: {
    splitChunks: {
      chunks: 'all', // 所有的 chunks 代码公共的部分分离出来成为一个单独的文件
      cacheGroups: {
        vendor: {
          name: 'vendor',
          test: /[\/]node_modules[\/]/, // 匹配模块资源路径或 chunk 名称
          priority: 10, // 缓存组的优先级
          chunks: 'initial', // 只打包初始时依赖的第三方
        },
      },
    },
  },
}
  • chunks:表示从什么类型的 chunks 里面提取代码。
  • cacheGroups:缓存组。缓存组可以继承或覆盖来自 splitChunks.* 的任何配置。但它特有的参数只能在缓存组里进行配置。

tree shaking

tree shaking 可以帮助我们检测模块中没有用到的代码块,并在 webpack 打包时将没有使用到的代码块移除,减少打包后的资源体积。

使用 tree shaking 一共分为两个步骤:

  1. 开启 usedExports 标注未使用的代码
  2. 通过 TerserPlugin 对未使用的代码进行删除
module.exports = {
  //...
  optimization: {
    usedExports: true,
    minimize: true,
    minimizer: [new TerserPlugin()],
  }
}

注意:

  1. tree shaking 只在生产环境中使用

  2. 有一些代码没有被其他模块导入使用,如 polyfill.js,它主要用来拓展全局变量,需要告诉 webpack 这类有副作用的代码不能删除。可以在 package.json 文件里使用 sideEffects 配置:

    // package.json
    {
      "sideEffects": [
        "./polyfill.js"
      ]
    }
    

使用缓存

webpack 5 提供了持久化缓存,它通过使用文件系统缓存,极大地减少了再次编译的时间。

使用文件系统缓存非常简单,只需增加如下配置:

module.exports = {
  //...
  cache: {
    type: 'filesystem',
  }
}

六、代码规范

这部分参考另外一篇文章# Eslint + Prettier + Husky + Commitlint+ Lint-staged 规范前端工程代码规范

1.ESLint

ESLint, 可以使开发者在执行前就发现代码错误或不合理的写法。

2.Prettier

prettier 这个工具能够将原始代码风格移除, 替换为团队统一配置的代码风格。

3.hushy

husky 通过 Git 命令的钩子,在 Git 命令进行到某一时段时,可以被交给开发者完成某些特定的操作。

4.lint-staged

在整个项目上运行 lint 会很慢,我们一般只会对更改的文件进行检查,这时就需要用到 lint-staged。

5.commitlint

commit 提交信息的校验工具。

6.commitizen

用来辅助 commit 的工具,通过给定的提交选项,规范提交信息。