Vite 开发实践 - 打包部署(vite和webpack的区别)
前言
在前面我们讲了vite
的环境配置与插件开发实践,下面再来讲下vite
在打包以及部署中的相关实践。
打包生产环境
简单的应用直接npm run build
就可以得到生产环境的代码,但是如果需要对打包做更精确的操作,还需要单独做一些配置,比如对多页应用和代码库的打包。
当然,这一点官方已经说的很清楚了,这里建议直接看 vite 官网就行了
多页应用的打包
vite 中多页应用打包很简单,因为是使用的rollup
,所以改变一下rollup
相关选项就行了:
import { defineConfig } from 'vite' import path from 'path' module.exports = defineConfig({ build: { rollupOptions: { input: { main: path.resolve(__dirname, 'index.html'), nested: path.resolve(__dirname, 'index2.html') } } } }) 复制代码
其余更细节的操作可以参考rollup
的打包流程。
库模式的打包
vite 专门为我们提供了库模式打包的配置,一般来说只需要在build.lib
的配置项中进行相应声明就可以了。
import { defineConfig } from 'vite' import path from 'path' export default defineConfig({ build: { lib: { entry: path.resolve(__dirname, 'lib/main.js'), // umd 形式的命名空间 name: 'MyLib', fileName: (format) => `my-lib.${format}.js` }, rollupOptions: { // 确保外部化处理那些你不想打包进库的依赖 external: ['vue'], output: { // 在 umd 构建模式下为这些外部化的依赖提供一个全局变量 globals: { vue: 'Vue' } } } } }) 复制代码
官方还建议我们在package.json
中进行相关定义,当我们引入包时会匹配到下面的定义的内容:
{ // 包名 "name": "my-lib", // 代表只上传 dist 目录和 package.json 文件,如果不写则默认上传所有非 node_modules 文件 "files": ["dist"], // 声明包内导出的内容,该字段是一个较新的语法,建议能写就写,但是相应的兼容写法都需要写上。 "exports": { // . 代表该包只导出在了第一级,只能使用 import xx from 'my-lib'的方式引入包内容,不能使用 import xxx from 'my-lib/dist/my-lib.es.js'等方式引入 ".": { // 通过 import 引入时会匹配到这里 "import": "./dist/my-lib.es.js", // 通过 require 引入时会匹配到这里 "require": "./dist/my-lib.umd.js", // ts 定义文件 "types": "./dist/my-lib.d.ts" }, // 代表导出了'my-lib/dist'目录下所有内容,可以 import xx from 'my-lib/dist/my-lib.es.js' 引入包内容 "./dist/*": { "import": "./dist/my-lib.es.js", "require": "./dist/my-lib.umd.js", "types": "./dist/my-lib.d.ts" } }, // 通过 require 引入时会匹配到这里,exports 的兼容写法 "main": "./dist/my-lib.umd.js", // 通过 import 引入时会匹配到这里,exports 的兼容写法 "module": "./dist/my-lib.es.js", // ts 定义文件,exports 的兼容写法 "types": "./dist/my-lib.d.ts" } 复制代码
公共基础路径
vite 允许我们添加base
配置项来管理资源的公共路径,由 JS 引入的资源 URL,CSS 中的 url()
引用以及 .html
文件中引用的资源在构建过程中都会自动调整,以适配此选项。
正常情况下在引入资源不是很多的情况下都不需要做其余的配置。但是当我们需要在 JS 中引入大量诸如图片等资源时,通过import
一个个引入图片就显得过于繁琐了,所以往往都是通过引入url
路径的形式直接引入的,比如下面这样:
// vite 通过 import 引入 import img1 from 'xxx1' import img2 from 'xxx2' import img3 from 'xxx3' const config = { img1, img2, img3, } // 通过把图片放到 public 目录中直接引入 https://cn.vitejs.dev/guide/assets.html#the-public-directory const config = { img1: 'xx1', img2: 'xx2', img3: 'xx3', } 复制代码
但由于只能够通过import
的形式引入时vite
才会自动适配base
选项,所以我们还需要在代码中对添加的base
进行动态的适配。值得庆幸的是,vite 为我们提供了在运行时获取base
字段的能力,所以我们只需要封装一下就行了:
const isHttp = (url: string) => /^https?:/.test(url) function addBase(url: string) { // import.meta.env.BASE_URL 是 vite 提供的注入变量 return isHttp(url) ? url : `/${`${import.meta.env.BASE_URL}/${url}`.split('/').filter(Boolean).join('/')}` } const config = { img1: addBase('xx1'), img2: addBase('xx2'), img3: addBase('xx3'), } 复制代码
支持 runtimePublicPath 功能
什么是 runtimePublicPath
简单来说,publicPath(也就是上面的base
配置)功能可以帮助我们根据自己的选择进行资源部署,配置了此选项后代码中的所有的资源引用都会被分发到该路径下(比如配置 CDN),而 runtimePublicPath 则是让我们可以在运行时根据不同的状态而修改给用户展示的资源。
如果你之前深入用过webpack
,或许知道webpack
自身为我们提供了变量__webpack_public_path__
来实现运行时获取publicPath
(也就是 vite 的base
)的能力,也就是说通过在程序入口赋值为诸如window.publicPath
的变量,就可以动态地在运行时改变publicPath
了。
而就目前而言,vite
官方并没有为我们提供类似的方法。但好在vite
同时兼容rollup
的所有接口,所以我们还可以在插件层面上使用打包拦截 + 变量替换的方法实现该功能。
实现 runtimePublicPath 插件
下面的代码参考自 vite-plugin-dynamic-publicpath
vite 中有两个部分都需要有 runtimePublicPath 的功能,一个是资源预加载时的路径,一个是资源import
时的路径。
我们可以使用 rollup 提供了两个 Api:renderDynamicImport
和generateBundle
。renderDynamicImport
用于拦截import
语句,添加动态引入的路径变量,generateBundle
则用于生成新的资源映射关系并替换掉 vite 原有的预加载路径,生成新的预加载路径变量。
import path from 'path' import { parse as parseImports, ImportSpecifier } from 'es-module-lexer' import { normalizePath, Plugin } from 'vite' interface Options { /** * @default: window.__dynamicImportHandler__ */ // 动态引入的变量 dynamicImportHandler?: string /** * @default: window.__dynamicImportPreload__ */ // 动态预加载的变量 dynamicImportPreload?: string /** * @description 该值和打包后的生成文件夹对应,请同步修改 * @default assets */ assetsBase?: string } export function viteDynamicPublicPathPlugin(options?: Options): Plugin { const defaultOptions: Options = { dynamicImportHandler: 'window.__dynamicImportHandler__', dynamicImportPreload: 'window.__dynamicImportPreload__', assetsBase: 'assets', } // eslint-disable-next-line no-param-reassign options = { ...defaultOptions, ...options } const { dynamicImportHandler, dynamicImportPreload, assetsBase } = options return { name: 'vite-dynamic-public-path-plugin', enforce: 'post', apply: 'build', // 拦截 import renderDynamicImport({ format }) { // 看 es 就行了 if (format === 'es') { // 在 import 时添加动态变量 return { left: `import("__PUBLIC_PATH_MARKER__" + (${dynamicImportHandler} || function(importer) { return importer; })(`, right: ') + "__PUBLIC_PATH_MARKER__" )', } } else if (format === 'system') { return { left: `module.import((${dynamicImportHandler} || function(importer) { return importer; })(`, right: '))', } } return null }, // 生成打包后的代码 generateBundle({ format }, bundle) { if (format !== 'es') { return } // vite 中预加载的标记,这个是 vite 内部的,固定值 const preloadMarker = '__VITE_PRELOAD__' const preloadMarkerRE = new RegExp(`"${preloadMarker}"`, 'g') // eslint-disable-next-line guard-for-in for (const file in bundle) { const chunk = bundle[file] if (chunk.type === 'chunk' && chunk.code.indexOf(preloadMarker) > -1) { const code = chunk.code.replace(/"__PUBLIC_PATH_MARKER__"/g, '""') let imports: ImportSpecifier[] try { // 拿到解析所有 imports,过滤拿到所有动态导入的 import imports = parseImports(code)[0].filter((i) => i.d > -1) } catch (e: any) { this.error(e, e.idx) } if (imports?.length) { // 所有动态导入 for (let index = 0; index < imports.length; index++) { const { s: start, e: end } = imports[index] // 路径 const url = code.slice(start, end) // 加上资源目录前缀 const normalizedFile = path.posix.join( path.posix.dirname(chunk.fileName), url.slice(1, -1) ) const importerResult = url.match(/\(['"](.+)['"]\)/) if (Array.isArray(importerResult) && importerResult.length > 1) { // 与当前某个 bundle 对应的 assetKey 值,因为我们实际上只是改变了一下映射,资源内容还是一样的 const assetKey = normalizePath( path.join(`${assetsBase}`, importerResult[1]) ) // 多生成一份相对应 bundle,否则生成文件时找不到文件映射关系会少生成文件 // eslint-disable-next-line no-param-reassign bundle[normalizedFile] = bundle[assetKey] } } } chunk.code = code // 替换 vite 中预加载的静态标记,改为我们的动态函数值 .replace( preloadMarkerRE, `(${dynamicImportPreload} || function(importer) { return importer; })((${preloadMarker}))` ) } } }, } } export default viteDynamicPublicPathPlugin 复制代码
然后,只需要我们在项目入口中加入下面几行代码就可以实现 runtimePublicPath 功能了:
// main.ts // Your dynamic cdn const dynamicCdn = 'cdn.xxx.com' // import 路径 window.__dynamicImportHandler__ = function(importer) { return dynamicCdn + importer; } // 预加载路径 window.__dynamicImportPreload__ = function(preloads) { return preloads.map(preload => dynamicCdn + preload); } 复制代码
资源文件的处理
在上面我们实际只能解决js
文件的引入问题,但通过import
引入的相关图片等资源文件却还是无法正常获取,因为 vite 中图片等资源文件的引入是通过在解析完成后返回 url 路径来拿的,像下面这样:
// import img from './xxx.jpg' 时返回如下 export default '/xxx.jpg' 复制代码
我们在之前实际只解决了import img from './xxx.jpg'
的路径问题,但最终资源的 url 还是错的(应该是和window.publicPath
绑定),这时候就需要我们对项目中的资源文件拦截后再做单独处理了。当然,这同样需要使用 vite 提供的插件功能:
// 下面是对 svg 格式资源的处理 import { Plugin } from 'vite' const svgRegex = /\.svg$/ function svgPublicPathPlugin(): Plugin { return { name: 'vite-svg-plugin-path-plugin', enforce: 'pre', apply: 'build', transform(code, id) { // 拿到文件源码,再进行字符串替换 if (svgRegex.test(id)) { // 引入 svg 的 url const url = code.match(/".*"/gi)?.[0] || '' return `const importer = ${url} // 手动添加前缀 const prefix = window.publicPath || '/' const url = prefix + importer.slice(1) export default url` } }, } } export default svgPublicPathPlugin 复制代码
这样,我们就能正常引入资源文件了。
至于 CSS 中的资源文件,由于无法处理 js 中的变量,我对其只是简单地进行了资源内联,通过postcss-url
插件资源全部变为 base64 编码打入资源包中,或许也可以考虑使用 CSS 变量来解决,但由于我之前并没有在 CSS 中引入过多资源,所以这里就没有往这条路走了,有兴趣的同学可以试一试。
下面是postcss.config.js
文件的配置:
module.exports = { plugins: { 'postcss-url': { filter: 'node_modules/**/*', url: 'inline' }, }, } 复制代码
下面是整体的处理思路:
添加持续集成服务
这小节的内容可以直接参考这里
持续集成服务可以帮我们方便地进行项目的部署等操作,相比于手动打包来说还是非常节省时间的,我这里使用Github Actions
部署Github Pages
来简单做一个演示(当然工作中也可以自行选择 ci、cd 工具链)。
Github Actions
服务相比其他 ci、cd 工具来说简单很多,我们在项目中创建.github/workflows/deploy.yaml
文件,写入下面的代码:
name: Build and Deploy # 监听 main 分支的推送,我这边是把 master 分支修改为了 main 分支 on: push: branches: - main pull_request: branches: - main jobs: # job 名 build-and-deploy: # 运行环境 runs-on: ubuntu-latest # 运行步骤 steps: # 获取源码 - name: Checkout uses: actions/checkout@v2.3.1 # 下载依赖 - name: Install dependencies and Build run: yarn && yarn build # 发布 - name: Deploy uses: JamesIves/github-pages-deploy-action@4.1.4 with: # 发布在 gh-pages 分支,会自动创建 branch: gh-pages # 将打包后的 dist 目录放到 gh-pages 分支 folder: dist 复制代码
具体的
Github Actions
的配置见文档
除此之外,由于我们要在 github 上部署项目,所以要修改一下vite
的base
配置用于路由匹配:
import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ // 部署的前缀,这里的匹配方式是 username.github.io/repository 的形式 base: 'https://col0ring.github.io/vite-react-start-template/', // ... }) 复制代码
然后就可以把项目推送到github
了,github
会找到我们的配置文件,然后开启 ci、cd 流程。
流程跑完后,进行下面的选择,就可以访问到我们部署的 github pages 了:
总结
本文从基本的 vite 打包开始,到介绍打包中遇到的一些坑和具体的实践解决方式,最后又简单介绍了一下目前比较通用的自动化部署服务。总的来说 vite 确实给了我们极致的开发体验,但在生产环境中或许还是有着一些小缺陷,期待后续官方能够提供更加优雅的方式来解决它们。
作者:Col0ring
链接:https://juejin.cn/post/7028773763827105829