阅读 3147

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 则是让我们可以在运行时根据不同的状态而修改给用户展示的资源。 image.png

如果你之前深入用过webpack,或许知道webpack自身为我们提供了变量__webpack_public_path__来实现运行时获取publicPath(也就是 vite 的base)的能力,也就是说通过在程序入口赋值为诸如window.publicPath的变量,就可以动态地在运行时改变publicPath了。

而就目前而言,vite官方并没有为我们提供类似的方法。但好在vite同时兼容rollup的所有接口,所以我们还可以在插件层面上使用打包拦截 + 变量替换的方法实现该功能。

实现 runtimePublicPath 插件

下面的代码参考自 vite-plugin-dynamic-publicpath

vite 中有两个部分都需要有 runtimePublicPath 的功能,一个是资源预加载时的路径,一个是资源import时的路径。

我们可以使用 rollup 提供了两个 Api:renderDynamicImportgenerateBundlerenderDynamicImport用于拦截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' },   }, } 复制代码

下面是整体的处理思路: image.png

添加持续集成服务

这小节的内容可以直接参考这里

持续集成服务可以帮我们方便地进行项目的部署等操作,相比于手动打包来说还是非常节省时间的,我这里使用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 上部署项目,所以要修改一下vitebase配置用于路由匹配:

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 了: image.png

总结

本文从基本的 vite 打包开始,到介绍打包中遇到的一些坑和具体的实践解决方式,最后又简单介绍了一下目前比较通用的自动化部署服务。总的来说 vite 确实给了我们极致的开发体验,但在生产环境中或许还是有着一些小缺陷,期待后续官方能够提供更加优雅的方式来解决它们。


作者:Col0ring
链接:https://juejin.cn/post/7028773763827105829


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐