likes
comments
collection
share

webpack5学习 --- 环境分离和代码分割

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

目前我们所有的webpack配置信息都是放到一个配置文件中的:webpack.config.js

当配置越来越多时,这个文件会变得越来越不容易维护;

并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环 境都会使用的

所以,我们最好对配置进行划分,方便我们维护和管理;

区分开发环境

环境: 在项目根目录下存在一个文件夹为config,用于存放webpack的配置文件

方式1: 编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可

package.json

"scripts": {
  "dev": "webpack --config ./config/config.dev.js",
  "prod": "webpack --config ./config/config.prod.js"
}

方式2:使用相同的一个入口配置文件,通过设置参数来区分它们

package.json

"scripts": {
  "dev": "webpack --config ./config/config.common.js --env development",
  "prod": "webpack --config ./config/config.common.js --env production"
}
// package.json
// env选项的值如果只设置了key, 默认的value值为true
"start": "webpack serve --env dev", // => { dev: true }
// 如果设置了value值,在解析的时候,会根据=进行划分 [=前后不要加上空格]
"start": "webpack serve --env env=dev", // => {env: 'dev'}

config.common.js

const path = require('path')

module.exports = (env, argv) =>  {
  // development ---> { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, development: true }
  // production ---> { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, production: true }
  console.log(env) 
  
  // argv是一个对象,用于存储所有传入的键值对
  // 当执行的指令为 webpack serve --mode development --env env=dev
  // argv -> { mode: 'development', env: { WEBPACK_SERVE: true, env: 'dev' } }
  console.log(argv)

  return {
    // 这里必须写成 ./ 不能写 ../ 不然无法正确找到入口文件
    entry: './src/index.js',

    output: {
      // 因为当前目录在config下边,所以构建目录需要存放在上一级的目录下
      path: path.resolve(__dirname, '../dist'),
      filename: 'bundle.js'
    }
  }
}

context

我们之前编写入口文件的规则是这样的:./src/index.js,但是如果我们的配置文件所在的位置变成了 config 目录, 我们是否应该变成 ../src/index.js呢? 显然是不可以的

这是因为入口文件其实是和另一个属性时有关的 context

context的作用是用于解析入口(entry point)和加载器(loader)

const path = require('path')

module.exports = env =>  {
  return {
    // context的值 被要求是一个绝对路径
    // 实际入口文件路径为 path.resolve(context, entry)
    // 即entry的相对路径其实是相对于context所设置的那个路径的
    context: path.resolve(__dirname, './'),
    // 因为此时context被设置成了当前路径,也就是config目录
    // 所以此时的路径是上一层的src下的index.js
    entry: '../src/index.js',

    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    }
  }
}

默认的context的值为 webpack对应命令行执行时所处的路径

# 所以 context的默认路径是项目的根目录
webpack --config ./config/config.common.js --env development

因此,默认情况下,entry的路径应该被设置为./src/index.js,而不是../src/index.js

配置文件分离

当我们设置了mode的时候,webpack会自动将我们设置的mode值,设置到process.env.NODE_ENV

例如mode: 'development ===> process.env.NODE_ENV的值就是development

path.js

const path = require('path')

// node中 process.cwd() 方法可以输出当前node进程的执行目录
// 这里的node进程开启是在项目根目录下开启的(pacakge.json中启动)
// 所以process.cwd() 的结果就是项目的根目录
module.exports = (relativePath) => path.resolve(process.cwd(), relativePath)

config.common.js

const path = require('path')

// webpack-merge这个库可以帮助我们进行配置文件的相互合并
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const prodConfig = require('./config.prod')
const devConfig = require('./config.dev')

const resolvePath = require('../utils/path')

module.exports = env =>  {
  // 这里设置process.env.NODE_ENV的作用是 提前设置全局环境值
  // 因为此时还没有设置mode的具体值,所以需要手动进行设置

  // tips:如果设置process.env.NODE_ENV的某个属性值为undefined的时候
  // 再取出process.env.NODE_ENV中对应的属性值的时候,结果为undefined,但是类型是字符串类型,不是undefined类型
  process.env.NODE_ENV = env.production ? 'production' : 'development'

  const commonConfig = {
    entry: './src/index.js',

    output: {
      path: path.resolve(__dirname, '../dist'),
      filename: 'bundle.js'
    },

    devServer: {
      hot: true
    },

    plugins: [
      new HtmlWebpackPlugin({
        template: resolvePath('./public/index.html')
      })
    ]
  }

  return env === 'production' ? merge(commonConfig, prodConfig) : merge(commonConfig, devConfig)
}

config.dev.js

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
  mode: 'development',

  plugins: [
    new ReactRefreshWebpackPlugin()
  ]
}

config.prod.js

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',

  plugins: [
    new CleanWebpackPlugin()
  ]
}

babel.config.js

const presets = [
  '@babel/preset-env'
]

const plugins = []

// process.env.NODE_ENV 这里可以取到值是因为在common.config.js中进行了环境变量的设置, 而不是因为设置了mode的值后webpack帮助我们进行设置的
// 因为babel设置是位于config.common.js,此时还没有设置具体的mode的值
if (process.env.NODE_ENV === 'development') {
  plugins.push('react-refresh/babel')
}

module.exports = {
  presets,
  plugins
}

package.json

"scripts": {
  "dev": "webpack serve --config ./config/config.common.js --env development",
  "prod": "webpack --config ./config/config.common.js --env production"
}

代码分离

代码分离(Code Splitting)是webpack一个非常重要的特性:

默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载, 就会影响首页的加载速度;

主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件

代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能

Webpack中常用的代码分离有三种:

  1. 入口起点:使用entry配置手动分离代码

  2. 抽离重复模块:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码

  3. 动态导入:通过模块的内联函数调用来分离代码;

入口起点

入口起点的含义非常简单,就是配置多入口, 从而得到多个输出的bundle文件

module.exports = {
  entry: {
    main: './src/main.js',
    index: './src/index.js'
  },

  output: {
    path: path.resolve(__dirname, '../dist'),
    // 此时name的取值就是entry的key值
    // 打包后会形成 main.bundle.js 和 index.bundle.js
    filename: '[name].bundle.js'
  }
}

抽离重复模块

Entry Dependencies(入口依赖)

假如我们的index.js和main.js都依赖两个库:lodash、dayjs

如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs

事实上我们可以对他们共同的依赖进行抽取

entry: {
  main: {
    import: './src/main.js',
    // main.js 需要提取出来的依赖,值为字符串(值只能是字符串)
    dependOn: 'lodash'
  },
  index: {
    import: './src/index.js',
    dependOn: 'lodash'
  },
  // 抽离出来的公共模块
  lodash: 'lodash',
  dayjs: 'dayjs'
}

如果有多个需要共享的模块存在,可以将多个模块抽离为一个数组后子再一并进行使用

entry: {
 main: {
   import: './src/main.js',
   dependOn: 'shared'
 },
 index: {
   import: './src/index.js',
   dependOn: 'shared'
 },
 // 多个需要被公用的模块组成的数组
 shared: ["lodash", "dayjs"]
}

SplitChunks

另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的:

因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件,只需要提供SplitChunksPlugin相关的配置信息即可

optimization: {
  splitChunks: {
    // chunks 可以设置的值有三个
    // 1. async 对异步(async)引入的包进行分包操作 默认值
    // 2. inital 对同步引入的包进行分包操作
    // 3. all 对所有引入的包全部进行分包操作
    chunks: 'all'
    // 默认情况下,所有被抽取出去的包都会合并到[id].bundle.js中
  }
}

splitChunks还可以有更多的配资选项

   optimization: {
      splitChunks: {
        chunks: 'all',
        // 默认值是20000Byte,表示大于这个大小的引入文件都需要抽离出来
        minSize: 20000,
        // 表示的是大于多少字节的包需要进行二次拆分,拆分为不小于minSize的包
        // 多数情况下,如果设置maxSize的值的时候,minSize和maxSize的值一般是一致的
        maxSize: 20000,
        // 某一个包引入了多少次就需要被抽离出来
        minChunks: 1,

        // cacheGroups的含义是 所有的模块输出,会存放在缓存中,最后一起执行对应的操作
        // 在这个属性里面可以自己自定义的代码分割配置
        // cacheGroups的优先级小于minSize和maxSize,所以当两种冲突的时候,cacheGroup中的配置会默认失效
        cacheGroups: {
          // key可以任意取,在这边只是一个占位符
          // value是一个配置对象
          vendor: {
            // 正则,用以匹配对应的模块路径
            test: /[\\/]node_modules[\\/]/,
            // 输出文件名 输出文件会以 输出文件名-hash值.js的形式输出
            // name: "vender",
            
            // filename 输出文件名,和name不同的是,filename中可以使用placeholder
            filename: 'vendor_[id].js',
            // 优先级 在这个配置中约定俗称,一般设置为负数
            priority: -10
          },
          default: {
            minChunks: 2,
            filename: "common_[id].js",
            priority: -20
          }
        }
      }
    }

多数情况下,同步的代码一般会被拆分成4个文件

文件名说明
main.bundle.js主代码文件
runtime.bundle.jswebpack的辅助函数抽取文件
vendor.bundle.js所有第三方包所在的文件
common.bundle.js如果存在多入口的时候,所有重复引入的模块,当引用次数超过一定次数的时候时候,会抽取到的一个文件

动态导入

当某一个模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载)

我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件

这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码

这个时候我们就可以使用动态导入

只要是异步导入的代码, webpack都会进行代码分离, 无论这个文件的大小

即使webpack在设置的时候,将chunks设置为了inital,webpack的其它默认配置项也会将动态导入的模块抽离出来,形成一个独立的js文件

import('./main').then(res => console.log(res))

动态导入的文件命名

因为动态导入通常是一定会打包成独立的文件的,默认情况下这些异步模块的命名规则和output.filename的命令规则是一致的

但是我们通常希望异步的模块取名叫做xxx.chunk.js,此时,我们可以通过 output.chunkFilename 属性来命名

output: {
  path: path.resolve(__dirname, '../dist'),
  // 如果是同步模块的时候,一般命名为bundle
  filename: '[name].bundle.js',
  // 如果是动态引入,一般命名为chunk
  chunkFilename: '[name].chunk.js'
}

你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的

如果我们希望修改name的值,可以通过magic comments(魔法注释)的方式

import(/* webpackChunkName: 'foo' */'./foo').then(res => console.log(res))

此时,打包出来的模块名就会是foo.chunk.js

去除comments的抽离

在production模式下,打包后的代码,默认会将一些comments信息提取出来,形成对应的txt文件

const TerserPlugin = require('terser-webpack-plugin')

optimization: {
  // 压缩配置
  minimizer: [
    new TerserPlugin({
      // 关闭comments的抽离
      extractComments: false,
    })
  ]
}

chunkIds

optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成

说明
natural按照数字的顺序使用id如2.bundle.js, 3.bundle.js不推荐,因为如果移除某一个模块的时候,每个模块对应的自然数会发生改变不利于浏览器的缓存操作
nameddevelopment下的默认值,一个可读的名称的id;(开发过程中推荐使用)
deterministicproduction下的默认值,表示确定性的,根据内部算法生成在不同的编译中不变的短数字id(打包过程中推荐使用)
转载自:https://juejin.cn/post/6994251135540264996
评论
请登录