Vite对React项目的第三方依赖做打包优化配置之手动分包和CDN引用

1,417 阅读10分钟

前言

使用Vite(版本:"5.4.9")搭建React(版本:"^18.3.1")项目,针对大体积的三方依赖做打包优化~

打包优化

使用插件rollup-plugin-visualizer

  • 安装插件rollup-plugin-visualizer(版本: "^5.12.0"):
# 安装插件:对打包体积分析
pnpm i rollup-plugin-visualizer -D

该插件的功能:对打包体积输出分析报告,生产一个stats.html

  • 配置vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
    plugins: [
        // ==打包后的文件体积分析==
        visualizer({
            open:true, // 打包完成后自动打开浏览器
        })
    ]
})

在终端执行打包命令pnpm build

vite v5.4.9 building for production...
✓ 34 modules transformed.
dist/index.html                   0.87 kB │ gzip:  0.43 kB
dist/assets/index-BHdWsPZQ.css    0.08 kB │ gzip:  0.09 kB
dist/assets/index-J-a4xQHu.js   203.16 kB │ gzip: 66.59 kB
✓ built in 2.31s

可以看到,所有的js都被打包进一个文件index-J-a4xQHu.js,该文件大小203.16 kB

浏览器自动打开stats.html,分析结果:

1732285993766.png

体积大的就是react-domreactreact-router-dom,下面对此做优化~。

配置第三方依赖资源单独打包

build.rollupOptions.external

该字段作用:指定依赖不被打包

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==外部化依赖:指定依赖不打进入口的index包==
      external: ["react", "react-dom", "react-router-dom"],
    }
  }
})

再次输入pnpm build打包,结果如下,发现index.js文件明显小了很多,由原来的203.16KB到现在的3.56KB

vite v5.4.9 building for production...
✓ 17 modules transformed.
dist/index.html                  0.87 kB │ gzip: 0.43 kB
dist/assets/bg-CyIxRLAg.png     28.28 kB
dist/assets/index-BHdWsPZQ.css   0.08 kB │ gzip: 0.09 kB
dist/assets/index-C9xEeSm6.js    2.24 kB │ gzip: 1.08 kB
✓ built in 743ms

但是打包目录只有一个js文件,设置外部化的依赖文件到哪里去了?

再次看刚配置的vite.config.js,发现,我只是设置了external字段,该字段是指定哪些依赖不打进bundule即最后的入口文件index-hash.js,这样指定的依赖reactreact-domreact-react-dom确实没有被打进包,所以这个输出是正常的~

默认情况下:vite将样式和图片抽离出来了(dist/assets/index-BHdWsPZQ.cssdist/assets/bg-CyIxRLAg.png)~

build.rollupOptions.output.manualChunks

该字段作用:配置手动分包策略

修改配置文件:

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==外部化依赖:指定依赖不打进入口的index包==
      external: ["react", "react-dom", "react-router-dom"],
      // ==新增:输出配置==
      output: {
        // ==新增:配置手动分包==
        manualChunks: {
          react: ["react", "react-dom", "react-router-dom"],
        }
      }
    }
  }
})

执行打包命令pnpm build,报错了:

vite v5.4.9 building for production...
✓ 12 modules transformed.
x Build failed in 715ms
error during build:
"react" cannot be included in manualChunks because it is resolved as an external module by the "external" option or plugins.
    at getRollupError (file:///F:/test/monorepo-test/node_modules/.pnpm/rollup@4.24.3/node_modules/rollup/dist/es/shared/parseAst.js:395:41)
    at error (file:///F:/test/monorepo-test/node_modules/.pnpm/rollup@4.24.3/node_modules/rollup/dist/es/shared/parseAst.js:391:42)
    at ModuleLoader.loadEntryModule (file:///F:/test/monorepo-test/node_modules/.pnpm/rollup@4.24.3/node_modules/rollup/dist/es/shared/node-entry.js:20063:20)
    at async Promise.all (index 0)
    at async Promise.all (index 0)
 ELIFECYCLE  Command failed with exit code 1.

看报错信息:react在external被配置了不打包,而manualChunks又是配置单独打包。react一边被设置了不打包,一边设置了打包,两者冲突。将external字段去掉,再次打包即可。

修改配置文件:

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==新增:注释external==
      // ==外部化依赖:指定依赖不打进入口的index包==
      // external: ["react", "react-dom", "react-router-dom"],
      // ==输出配置==
      output: {
        // ==配置手动分包==
        manualChunks: {
          react: ["react", "react-dom", "react-router-dom"], // 被打包进react开头的js文件,vite默认的chunk文件名是outDir/[name]-[hash:8].js
        }
      }
    }
  }
})
vite v5.4.9 building for production...
✓ 37 modules transformed.
dist/index.html                   0.94 kB │ gzip:  0.46 kB
dist/assets/bg-CyIxRLAg.png      28.28 kB
dist/assets/index-BHdWsPZQ.css    0.08 kB │ gzip:  0.09 kB
dist/assets/index-BkrjLTIM.js     2.21 kB │ gzip:  1.08 kB
dist/assets/react-CNZNb05N.js   140.87 kB │ gzip: 45.25 kB
✓ built in 1.38s

这次成功了,并且将reactreact-domreact-react-dom三个依赖单独打进react-hash.js里了。

build.rollupOptions.output.chunkFileNames

该字段作用:配置chunk的打包输出路径+名称。

Vite默认的chunk打包输出是outDir/[name]-[hash].js,若是不想这样的输出路径或文件名称,则可使用字段chunkFileNames进行个性化配置。

但是我想让js在outDir/js/[name]-[hash].js,就需要配置chunkFileNames字段:

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==输出配置==
      output: {
        // ==配置手动分包==
        manualChunks: {
          react: ["react", "react-dom", "react-router-dom"], // 被打包进react开头的js文件,vite默认的chunk文件名是outDir/[name]-[hash:8].js
        },
        // ==新增:代码分割后的文件输出配置(即manualChunks配置了手动分包,这些包的输出路径+名称)==
        chunkFileNames: `${assetsDir}/js/[name]-[hash].js`,
      }
    }
  }
})

再次执行打包命令pnpm build

vite v5.4.9 building for production...
✓ 37 modules transformed.
dist/index.html                     0.94 kB │ gzip:  0.46 kB
dist/assets/bg-CyIxRLAg.png        28.28 kB
dist/assets/index-BHdWsPZQ.css      0.08 kB │ gzip:  0.09 kB
dist/assets/index-t51NycYU.js       2.22 kB │ gzip:  1.08 kB
dist/assets/js/react-CNZNb05N.js  140.87 kB │ gzip: 45.25 kB
✓ built in 1.63s

这样就完成了react依赖的单独打包配置~

build.rollupOptions.output.entryFileNames

该字段作用:配置入口js文件的输出名称,类似chunkFileNames字段。

Vite默认的入口js打包输出是outDir/index-[hash].js,若是不想这样的输出路径或文件名称,则可使用字段entryFileNames进行个性化配置。例如,我想要让他被打包到outDir/js目录下。

修改配置文件:

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==输出配置==
      output: {
        // ==配置手动分包==
        manualChunks: {
          react: ["react", "react-dom", "react-router-dom"], // 被打包进react开头的js文件,vite默认的chunk文件名是outDir/[name]-[hash:8].js
        },
        // ==代码分割后的文件输出配置(即manualChunks配置了手动分包,这些包的输出路径+名称)==
        chunkFileNames: `${assetsDir}/js/[name]-[hash].js`,
        // ==新增:配置入口js文件的名称==
        entryFileNames: `${assetsDir}/js/[name]-[hash].js`,
      }
    }
  }
})

重新打包pnpm build

vite v5.4.9 building for production...
✓ 37 modules transformed.
dist/index.html                     0.95 kB │ gzip:  0.46 kB
dist/assets/bg-CyIxRLAg.png        28.28 kB
dist/assets/index-BHdWsPZQ.css      0.08 kB │ gzip:  0.09 kB
dist/assets/js/index-BkrjLTIM.js    2.21 kB │ gzip:  1.08 kB
dist/assets/js/react-CNZNb05N.js  140.87 kB │ gzip: 45.25 kB
✓ built in 1.43s

js文件都被打包进dist/assets/js目录下了,但是样式文件css是vite默认抽离出来的,我想让它在styles文件夹下。

build.rollupOptions.output.assetFileNames

该字段作用:静态资源输出配置,除了入口文件(entryFileNames)和设置的chunk(chunkFileNames)的静态资源,如字体、样式、图片等。

修改配置文件,使样式文件被打包到styles目录下:

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==输出配置==
      output: {
        // ==配置手动分包==
        manualChunks: {
          react: ["react", "react-dom", "react-router-dom"], // 被打包进react开头的js文件,vite默认的chunk文件名是outDir/[name]-[hash:8].js
        },
        // ==代码分割后的文件输出配置(即manualChunks配置了手动分包,这些包的输出路径+名称)==
        chunkFileNames: `${assetsDir}/js/[name]-[hash].js`,
        // ==配置入口js文件的名称==
        entryFileNames: `${assetsDir}/js/[name]-[hash].js`,
        // ==新增:配置静态资源的输出(静态资源按类型输出)==
        assetFileNames: (assetInfo)=> {
          const { names } = assetInfo
          const name = names && names[0] 
          const suffix = "[name]-[hash].[ext]" // 文件名称格式
          if (!name) {
            console.log(1, assetInfo.name)
            return `${assetsDir}/${suffix}`
          } else if (name.endsWith(".css")) {
            console.log(2, name)
            return `${assetsDir}/${styleDir}/${suffix}` // 样式输出到styles
          } else if (checkFileExtension(name, imageExtensions)) {
            console.log(3, name)
            return `${assetsDir}/${imagesDir}/${suffix}` // 图片输出到images
          } else {
            console.log(4, name)
            return `${assetsDir}/${suffix}`
          }
        }
      }
      }
    }
  }
})

重新打包pnpm build

vite v5.4.9 building for production...
✓ 37 modules transformed.
3 bg.png
1 index.css
2 index.css
dist/index.html                          0.95 kB │ gzip:  0.46 kB
dist/assets/images/bg-CyIxRLAg.png      28.28 kB
dist/assets/styles/index-BHdWsPZQ.css    0.08 kB │ gzip:  0.09 kB
dist/assets/js/index-CgbsK4No.js         2.22 kB │ gzip:  1.08 kB
dist/assets/js/react-CNZNb05N.js       140.87 kB │ gzip: 45.25 kB
✓ built in 1.53s

图片被打包到dist/assets/images、样式在dist/assets/styles,脚本在dist/assets/js,这样就做到了资源的分门别类~

配置不打包依赖, 使用CDN链接

由上面的打包结果可知,可以将第三方依赖单独打包,节省页面的二次加载时间。

但是,也可以在生产环境使用CDN来优化~

配置生产环境下的CDN引用,有两种方式:

  • 方式一:在index.html中增加script标签对依赖库的CDN链接。需要配合external字段设置依赖外化和插件vite-plugin-externals作变量转换。
  • 方式二:修改配置文件,使用插件vite-plugin-cdn-import动态生成对依赖库的CDN链接的引入,同时使用插件vite-plugin-externals作变量转换。

两者区别:一个是使用插件vite-plugin-cdn-import动态生成的script标签到打包后的index.html,一个是手动在index.html中配置的script标签。

build.rollupOptions.external

修改配置文件,将reactreact-domreact-router-dom依赖外化,设置不打包:

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==新增:外部化依赖:指定依赖不打进入口包==
      external: ["react", "react-dom", "react-router-dom"],
      // ==输出配置==
      output: {
        // ==新增:注释手动分包==
        // ==配置手动分包==
        // manualChunks: {
        //  react: ["react", "react-dom", "react-router-dom"], // 被打包进react开头的js文件,vite默认的chunk文件名是outDir/[name]-[hash:8].js
        // },
        // ==代码分割后的文件输出配置(即manualChunks配置了手动分包,这些包的输出路径+名称)==
        // chunkFileNames: `${assetsDir}/js/[name]-[hash].js`,
        // ==配置入口js文件的名称==
        entryFileNames: `${assetsDir}/js/[name]-[hash].js`,
        // ==配置静态资源的输出(静态资源按类型输出)==
        assetFileNames: (assetInfo)=> {
          const { names } = assetInfo
          const name = names && names[0] 
          const suffix = "[name]-[hash].[ext]" // 文件名称格式
          if (!name) {
            console.log(1, assetInfo.name)
            return `${assetsDir}/${suffix}`
          } else if (name.endsWith(".css")) {
            console.log(2, name)
            return `${assetsDir}/${styleDir}/${suffix}` // 样式输出到styles
          } else if (checkFileExtension(name, imageExtensions)) {
            console.log(3, name)
            return `${assetsDir}/${imagesDir}/${suffix}` // 图片输出到images
          } else {
            console.log(4, name)
            return `${assetsDir}/${suffix}`
          }
        }
      }
      }
    }
  }
})

执行打包命令pnpm build

vite v5.4.9 building for production...
✓ 17 modules transformed.
3 bg.png
1 index.css
2 index.css
dist/index.html                         0.88 kB │ gzip: 0.43 kB
dist/assets/images/bg-CyIxRLAg.png     28.28 kB
dist/assets/styles/index-BHdWsPZQ.css   0.08 kB │ gzip: 0.09 kB
dist/assets/js/index-BMimJrdv.js        2.24 kB │ gzip: 1.08 kB
✓ built in 587ms

发现没有了三方依赖文件的打包,因为将三方依赖的库设置在external字段里即不被打包~

但是上面的配置也有问题:将react-router-dom也设置在external字段里,打包后(使用live-server在本地运行打包后的项目)运行报错:

Uncaught TypeError: Cannot read properties of undefined (reading 'Routes')
    at Object.get [as Routes] (index.tsx:2062:1)
    at index-hYraL5ch.js:1:3353

寻找报错原因:

点开index-hYraL5ch.js(打包后的入口脚本)看报错的这一行window.ReactRouterDOM.Routes,继续看react-router-dom源码,找Routes,发现它的生成需要依赖第三方库@remix-run/router

上面我没有配置@remix-run/router的打包,所以便报了上面的错误。

为了避免这个问题,将reactreact-dom排除在打包之外,且将react-router-dom配置在手动分包。

手动配置CDN引用

  • 第一步:去第三方服务网站找到相应的链接并复制(锁定版本),这里笔者用的是jsdelivr

1732350770727.png

  • 第二步:修改项目根目录下的index.html,将第一步复制的结果粘贴到head标签里:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
    <!-- 新增:react和react-dom的引用 -->
    <!-- 引入CDN,加载react -->
    <script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
    <!-- 引入CDN,加载react-dom -->
    <script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="index.js"></script>
    <script>
      console.log(this)
    </script>
  </body>
</html

注意:因为使用CDN链接便不再是通过ESM来导入使用,而是通过全局变量来使用。

为了知道引入的CDN文件的全局变量是谁,注释掉<script type="module" src="index.js"></script>,打印当前作用域console.log(this),浏览器打开该页面看打印信息:

1732350445869.png

发现,引入的CDN在window上绑定了相应的库,react被绑定到了window.Reactreact-dom被绑定到了window.ReactDOM

现在已经知道了全局变量名称,下面就需要做全局变量的转换~

  • 第三步:设置reactreact-dom外部化和react-router-dom手动分包:

修改配置文件:

export default defineConfig({
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
      // ==新增:配置外化依赖==
      // ==输出配置==
      output: {
        // ==修改:配置手动分包==
        external: ["react", "react-dom"],
        // ==修改:配置手动分包==
        manualChunks: {
          "react-router": ["react-router-dom"], // 被打包进react-router开头的js文件,vite默认的chunk文件名是outDir/[name]-[hash:8].js
        },
        ...
      }
    }
  }
})
  • 第四步:全局变量的转换,使用build.rollupOption.output.globals字段

继续修改配置文件:

globals: {
  "react": "React", // 映射react的全局变量为React
  "react-dom": "ReactDOM",
  "react-dom/client": "ReactDOM", // 因为项目入口脚本引用了react-dom/client:import { createRoot } from "react-dom/client"
}

执行打包命令pnpm build

vite v5.4.9 building for production...
✓ 20 modules transformed.
3 bg.png
1 index.css
2 index.css
dist/index.html                           0.87 kB │ gzip: 0.46 kB
dist/assets/images/bg-CyIxRLAg.png       28.28 kB
dist/assets/styles/index-BHdWsPZQ.css     0.08 kB │ gzip: 0.09 kB
dist/assets/js/index-CYRc7TcT.js          3.65 kB │ gzip: 1.66 kB
dist/assets/js/react-router-MroGPJZ-.js  16.43 kB │ gzip: 6.34 kB
✓ built in 981ms

在打包目录的终端输入命令live-server,运行本地项目:

1231.webp

报错了,从报错信息看加载react失败,再去看打包后的代码index-CYRc7TcT.js

1732353079703.png

说明使用build.rollupOption.output.globals无效~

百度了下,使用插件vite-plugin-externals吧:

  • 安装:pnpm i vite-plugin-externals -D
  • 使用:修改配置文件如下,
// ==新增:导入插件==
import { viteExternalsPlugin } from "vite-plugin-externals"
export default defineConfig({
  plugins: [
    ...,
    // ==新增:外部插件的命名映射配置==
    viteExternalsPlugin({
        "react": "React", // 映射react的全局变量为React
        "react-dom": "ReactDOM",
        "react-dom/client": "ReactDOM",
      },{
        // =官方文档:https://github.com/crcong/vite-plugin-externals/blob/HEAD/README.zh-CN.md==
        // ==警告: 如果你在开发环境中,引入了生产环境的库, 可能会使得 HMR 失败。==
        // ==在server模式下禁止转换external里的代码==
        disableInServe: true,
    })
  ]
  // ==打包配置==
  build: {
    // ==修改:删除或注释build.rollupOption.output.globals字段内容==
   ...
  }
})

重新打包pnpm build

vite v5.4.9 building for production...
✓ 16 modules transformed.
3 bg.png
1 index.css
2 index.css
dist/index.html                           0.87 kB │ gzip: 0.46 kB
dist/assets/images/bg-CyIxRLAg.png       28.28 kB
dist/assets/styles/index-BHdWsPZQ.css     0.08 kB │ gzip: 0.09 kB
dist/assets/js/index-D_0ulMjz.js          3.82 kB │ gzip: 1.71 kB
dist/assets/js/react-router-BenATCSB.js  17.82 kB │ gzip: 6.70 kB
✓ built in 1.06s

运行打包项目,页面正常显示~

123.webp

自动配置CDN引用

  • 第一步:安装插件vite-plugin-cdn-import(终端执行pnpm i vite-plugin-cdn-import -D, 版本:1.0.1);
  • 第二步:设置相应的外部化依赖并且配置自动导入CDN插件,修改配置文件如下:
// ==新增:安装插件(pnpm i vite-plugin-cdn-import -D),配置CDN==
import { Plugin as ImportToCDN } from 'vite-plugin-cdn-import'
export default defineConfig({
  plugins: [
    ...
    // ==新增:配置CDN==
    ImportToCDN({
      // ==指定CDN源==
      prodUrl: 'https://cdn.jsdelivr.net/npm/{name}@{version}/{path}',
      // ==配置第三方依赖使动态生成相应的CDN引用==
      modules: [
        {
          name: "react",
          version: "18.3.1",
          path: "umd/react.production.min.js",
          var: "React", // 这个var是固定的,与在html中通过script标签引入的库会在window对象生成一个对象且绑定到window上,这两个要一致
        },
        {
          name: "react-dom",
          version: "18.3.1",
          path: "umd/react-dom.production.min.js",
          var: "ReactDOM",
        },
      ]
    }),
  ],
  // ==打包配置==
  build: {
    outDir: `${buildDir}`, // 打包输出目录,默认是dist
    rollupOptions: {
        // ==新增:外部化依赖:指定依赖不打进入口包==
      external: ["react", "react-dom"],
      // ==输出配置==
      output: {
        ...
      }
    }
  }
})

执行打包命令pnpm build

vite v5.4.9 building for production...
✓ 20 modules transformed.
3 bg.png
1 index.css
2 index.css
dist/index.html                           0.86 kB │ gzip: 0.43 kB
dist/assets/images/bg-CyIxRLAg.png       28.28 kB
dist/assets/styles/index-BHdWsPZQ.css     0.08 kB │ gzip: 0.09 kB
dist/assets/js/index-C-59qxsq.js          3.76 kB │ gzip: 1.65 kB
dist/assets/js/react-router-BYLXKHLw.js  17.95 kB │ gzip: 6.69 kB
✓ built in 1.11s

再看dist/index.html,在head标签里增加了这三项依赖的script标签引入:

<!doctype html>
<html lang="en">
  <head>
    <script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js" crossorigin="anonymous"></script>

    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
    <script type="module" crossorigin src="/assets/js/index-C-59qxsq.js"></script>
    <link rel="modulepreload" crossorigin href="/assets/js/react-router-BYLXKHLw.js">
    <link rel="stylesheet" crossorigin href="/assets/styles/index-BHdWsPZQ.css">
  </head>
  <body>
    <div id="root"></div>
    <script>
      console.log(this)
    </script>
  </body>
</html>

注意:第二步使用的是动态导入+external,还有另外一种方式是动态导入+viteExternalsPlugin

// ==动态导入CDN==
import { Plugin as ImportToCDN } from "vite-plugin-cdn-import"
// ==依赖外部化配置==
import { viteExternalsPlugin } from "vite-plugin-externals"
export default defineConfig(()=> {
  return {
    plugins: [
      ...,
      // ==配置CDN==
      ImportToCDN({
        prodUrl: "https://cdn.jsdelivr.net/npm/{name}@{version}/{path}",
        modules: [
          {
            name: "react",
            version: "18.3.1",
            path: "umd/react.production.min.js",
            var: "React", // 这个var是固定的,与在html中通过script标签引入的库会在window对象生成一个对象且绑定到window上,这两个要一致
          },
          {
            name: "react-dom",
            version: "18.3.1",
            path: "umd/react-dom.production.min.js",
            var: "ReactDOM", 
          },
        ]
      }),
      // ==外部模块的全局变量==
      viteExternalsPlugin({
        "react": "React", // 映射react的全局变量为React
        "react-dom": "ReactDOM",
        "react-dom/client": "ReactDOM",
      },{
        // =官方文档:https://github.com/crcong/vite-plugin-externals/blob/HEAD/README.zh-CN.md==
        // ==警告: 如果你在开发环境中,引入了生产环境的库, 可能会使得 HMR 失败。==
        // ==在server模式下禁止转换external里的代码==
        disableInServe: true,
      })
    ],
    // ==打包配置==
    build: {
      // ==打包输出目录,vite默认是dist==
      outDir: buildDir,
      // ==打包输出配置(vite打包使用的是rollup)==
      rollupOptions: {
        // ==修改:注释外部依赖配置==
        // ==配置某个依赖不打包==
        // external: ["react", "react-dom"],
        output: {
          ...
        }
      }
    }
  }
})

所以,插件vite-plugin-externals中的viteExternalsPlugin可以替代build.rollupOptions.external字段来使用。方式一也可以直接分两步:手动导入CDN引用viteExternalsPlugin配置便可实现,无需再配置build.rollupOptions.external

注意:在本地开发过程中,使用的还是本地的依赖包(还是要安装reactreact-dom);但是在生产环境下,使用的reactreact-dom是通过CDN获取到的。

好了,完成【针对大体积的三方依赖打包优化】任务~

总结

主要介绍了针对【大体积的三方依赖】的两种处理方式:

  • 配置手动分包(单独打包):降低页面二次加载时间(第一次加载后会存入缓存,再次请求从缓存中拿)。
  • 外部化依赖(不打包):在开发环境使用本地包,在生产环境使用CDN链接(极大减少请求时间和提升用户体验度)。

字段

  • build.rollupOptions.output.manualChunks:配置手动分包策略。
  • build.rollupOptions.output.chunkFileNames:配置chunk的输出路径和文件名称。
  • build.rollupOptions.output.entryFileNames:配置入口脚本的输出路径和文件名称。
  • build.rollupOptions.output.assetFileNames:配置静态资源的输出路径和文件名称。
  • build.rollupOptions.external:配置三方依赖不被打包。

插件

  • rollup-plugin-visualizer:打包体积分析。
  • vite-plugin-cdn-import:自动导入CDN链接到index.html。
  • vite-plugin-externals:配置外部化依赖。

CDN

CDN英文名称为Content Delivery Network,内容分发网络,其节点遍布各地,用户的访问到的CDN链接对应的内容会从最近节点拿,从而加快了加载速度,提高用户的体验。

其第三方服务网站主要有两个:例如 jsDelivr 或者 cdnjs

两种实现方式:

  • 手动配置CDN引用
    • 手动在index.html页面配置对应CDN链接的script标签的引用;
    • 使用插件viteExternalsPlugin配置外部化依赖,定义全局变量的映射。
  • 自动配置CDN引用
    • 使用插件vite-plugin-cdn-import配置动态导入;
    • 配置外部化依赖:build.rollupOptions.external或者使用插件vite-plugin-externals

live-server

使用live-server在本地运行打包后的项目:

  • 先全局安装live-servernpm install live-server -g
  • 再在打包目录的终端输入命令live-server:默认启动http://127.0.0.1:8080/

源代码

完整配置

// vite.conifg.js
import { defineConfig } from "vite"
import React from "@vitejs/plugin-react"
import { visualizer } from "rollup-plugin-visualizer"
import { Plugin as ImportToCDN } from "vite-plugin-cdn-import"
import { viteExternalsPlugin } from "vite-plugin-externals"
import path from "path"

// ==路径合成,返回绝对路径==
const resolve = function () {
  return arguments.length ? path.resolve(...arguments) : ""
}

// ==路径配置==
// ==项目根目录==
const projectDir = process.cwd()
// ==源代码目录==
const srcDir = "src"
// ==指定打包目录,默认dist==
const buildDir = "dist"
// ==指定静态资源存放路径(相对于build.outDir)==
const assetsDir = "assets"
// ==指定字体的输出目录==
const fontDir = "font"
// ==指定图片的输出目录==
const imagesDir = "images"
// ==指定js的输出目录==
const jsDir = "js"
// ==指定样式文件的输出目录==
const styleDir = "styles"
// ==指定静态资源的后缀名称==
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]
const fontExtensions = [".ttf", ".otf", ".woff", ".woff2"]

// ==校验文件名称是否是指定类型的==
function checkFileExtension(filename, extensionsArray) {
  // ==校验是否是以指定名称的后缀==
  const regex = new RegExp(`\(${extensionsArray.join('|')})$`, 'i')
  return regex.test(filename)
}

export default defineConfig(({
  command,
  // mode,
  ...args
})=> {
  // console.log("command", command, args, process.env.NODE_ENV)
  return {
    plugins: [
      React({
        babel: {
          plugins: ["@babel/plugin-transform-react-jsx"], // 转换jsx,使可以在js文件里写jsx
        }
      }),
      // ==对打包体积分析==
      visualizer({
        open: true,
      }),
      // ==配置CDN:动态导入==
      ImportToCDN({
        prodUrl: "https://cdn.jsdelivr.net/npm/{name}@{version}/{path}",
        modules: [
          {
            name: "react",
            version: "18.3.1",
            path: "umd/react.production.min.js",
            var: "React", // 这个var是固定的,与在html中通过script标签引入的库会在window对象生成一个对象且绑定到window上,这两个要一致
          },
          {
            name: "react-dom",
            version: "18.3.1",
            path: "umd/react-dom.production.min.js",
            var: "ReactDOM",
          },
        ]
      }),
      // ==外部模块的全局变量==
      viteExternalsPlugin({
        "react": "React", // 映射react的全局变量为React
        "react-dom": "ReactDOM",
        "react-dom/client": "ReactDOM",
      },{
        // =官方文档:https://github.com/crcong/vite-plugin-externals/blob/HEAD/README.zh-CN.md==
        // ==警告: 如果你在开发环境中,引入了生产环境的库, 可能会使得 HMR 失败。==
        // ==在server模式下禁止转换external里的代码==
        disableInServe: true,
      })
    ],
    // ==解析配置==
    resolve: {
      // ==配置别名==
      alias: {
        "@": resolve(projectDir, srcDir),
        "@components": resolve(projectDir, `${srcDir}/components`),
        "@utils": resolve(projectDir, `${srcDir}/utils`),
        "@assets": resolve(projectDir, `${srcDir}/assets`),
      },
      // ==配置扩展名==
      extensions: [".jsx", ".tsx", ".js", ".ts"],
    },
    // ==打包配置==
    build: {
      // ==打包输出目录,vite默认是dist==
      outDir: buildDir,
      // ==打包输出配置(vite打包使用的是rollup)==
      rollupOptions: {
        // ==配置某个依赖不打包==
        // external: ["react", "react-dom"],
        output: {
          // ==配置手动分包策略==
          manualChunks: {
            "react-router": ["react-router-dom"],
          },
          // ==代码分割后的文件输出配置(即manualChunks配置了手动分包,这些包的输出名称)==
          chunkFileNames: `${assetsDir}/${jsDir}/[name]-[hash].js`,
          // ==配置入口js文件的名称==
          entryFileNames: `${assetsDir}/${jsDir}/[name]-[hash].js`,
          // ==配置静态资源输出(指的是非chunk[chunkFileNames]、非入口文件[entryFileNames])==
          assetFileNames: (assetInfo) => {
            const { names } = assetInfo
            const name = names && names[0]
            const suffix = "[name]-[hash].[ext]"
            if (!name) {
              console.log(1, assetInfo.name)
              return `${assetsDir}/${suffix}`
            } else if (name.endsWith(".css")) {
              console.log(2, name)
              return `${assetsDir}/${styleDir}/${suffix}`
            } else if (checkFileExtension(name, imageExtensions)) {
              console.log(3, name)
              return `${assetsDir}/${imagesDir}/${suffix}`
            } else {
              console.log(4, name)
              return `${assetsDir}/${suffix}`
            }
          }
        }
      }
    }
  }
})

package.json

{
  "name": "@monorepo-test/react-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host --mode development",
    "build": "vite build --mode production",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "live-server": "^1.2.2",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.28.0"
  },
  "devDependencies": {
    "@babel/plugin-transform-react-jsx": "^7.25.9",
    "@eslint/js": "^9.13.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.3",
    "eslint": "^9.13.0",
    "eslint-plugin-react": "^7.37.2",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "rollup-plugin-visualizer": "^5.12.0",
    "vite-plugin-cdn-import": "^1.0.1",
    "vite-plugin-externals": "^0.6.2"
  }
}