Vite 加 Tailwindcss 开发会有啥体验?

5,791 阅读11分钟

Vite2记得是在2021春节左右推出的,我是公众号里看到一篇文章才知道有这么个东西。
那时就在感叹:我在过节,别人却在推动技术!所以年后上班趁各线项目还没从节日的气氛中缓过来时,我就抓紧摸鱼研究起来。摸鱼的目的...呸!研究目的主要2个:

  1. 之前有搭过基于Vue-Cli的多页面脚手架,然后也在实战项目中用了,那热更新速度够我刷会微博了,那时为了加快热更新速度,都事先在脚手架里把未涉及到的或者比较稳定的模块入口先过滤掉,只保留当前开发所要用到模块,可想而知当时是有多慢了。
  2. React脚手架摸索不多想试试,umijs太重不太喜欢,create-react-app如需定制化需要装额外暴露接口的包,有种官方血统被污染的感觉。而其他的又比较杂乱,看到vite可以用搭建基于react技术栈的脚手架就有点跃跃欲试。

稍微体验了下后就放在一边了。

敲过 yarn or npm install 就相当于这个脚手架我会了

后来看到一篇文章,文章题目我忘了,只记得大致内容:

作者跟领导提议想用Vue,领导回复说“你想吃屎?”。

就这样一篇文章带我尝试了下react + antD + vite,随后我又捡起vite将我之前基于Vue-Cli多页面的重新搭了下,并尝试加入了tailwindcss。

基于这些尝试性工作,在最近的混合开发H5中果断上了vite + vue3 + tailwindcss 进行实战。
结果.....还行,坑踩过了,我终于懂点皮毛了,下面就是实战总结。

vite 和 tailwindcss我就默认大家都已经装好了。

whoagree.gif

Tailwindcss

Tailwindcss 基于原子化理念,将样式重复性代码降到最小,原本开发最大限度基于类名的声明块不重复,现在Tailwindcss基于单独一句声明不重复。

开发体验:

  • 不用一直想着如何取类名
  • 少写了很多样式
  • 需要额外心智去记住TW中的类名(不过装了VScode插件后有所改善)
  • HTML中的class太多导致HTML整体较为拥挤

遇到的问题:

  • 对于设计稿中如长宽在tailwindcss中没有对应值如何处理 ?
  • 设计稿中是px,如何优雅的转换成rem或vw和vh呢?
  • 颜色字体等如何扩展?
  • 如何解决HTML中class较为拥挤的感官?

解决方式:

对于设计稿中如长宽等属性在tailwindcss中没有对应值如何处理 ?

tailwindcss 2.1之后加入一个中括号的特性,允许用户可以自定义值,这个特性对于书写样式的灵活度大大提高。

/* 如高度是22px的,就可以写成,同理z-index也可以,还有各种颜色等属性也可以这样 */
.example {
  @apply h-[22px];
}
.example {
  @apply z-[-1];
}
设计稿中是px,如何优雅的转换成rem或vw和vh呢?

移动端的适配是开发过程比较要关注的点,而适配方式除了媒体查询断点之外,基本就是rem + html字体动态设置,以及通过 vw(vh)来处理。

tailwindcss默认就是以rem来处理,但是设计稿中是以px来显示,不可能每个值都通过人工算来处理转换成rem吧,用过sass或者less的同学基本想到了预处理器中的function。

虽然tailwindcss可以结合sass或者less,但是有种舍近求远的感觉,tailwindcss自身就是基于postcss,可以通过postcss的两个扩展来处理:postcss-px-to-viewportpostcss-pxtorem

'postcss-px-to-viewport': {
   unitToConvert: 'px', // 要转化的单位
   viewportWidth: 375, // UI设计稿的宽度
   unitPrecision: 6, // 转换后的精度,即小数点位数
   propList: ['width, height'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
   viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
   fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
   selectorBlackList: ['wrap'], // 指定不转换为视窗单位的类名,
   minPixelValue: 4, // 默认值1,小于或等于4px则不进行转换
   mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
   replace: true, // 是否转换后直接更换属性值
   exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
   landscape: false // 是否处理横屏情况
 },
'postcss-pxtorem': {
   rootValue: 16,
   unitPrecision: 6,
   propList: ['font', 'font-size', 'line-height', 'letter-spacing'],
   selectorBlackList: [],
   replace: true,
   mediaQuery: true,
   minPixelValue: 4,
   exclude: /node_modules/i
 }

我这边rem主要用在字体上,vw用在width, height, padding, margin, border上,再加上媒体查询断点处理,移动端的自适应完全满足了。

颜色字体等如何扩展?

这个tailwindcss官方也有说明,我这边只说一点,要覆盖的直接写在theme下,要扩展的直接写theme.extend下。
直接看代码,注意下面2个fontFamily位置,sans字体是覆盖tailwindcss的默认值,bebasNeue是扩展字体(当然也别忘了在总入口处引入字体文件)

theme: {
  fontFamily: {
    sans: ['PingFang-SC-Bold', 'PingFang-SC', 'Microsoft YaHei', 'Microsoft Jhenghei', 'sans-serif']
  },
  extend: {
    fontFamily: {
      bebasNeue: ['BebasNeue Bold']
    },
    colors: {
      'custom-blue': '#0088FF',
      'custom-black': '#333333',
      'custom-gray1': '#666666',
      'custom-gray2': '#999999',
      'custom-gray3': '#B2B2B2'
    },
    screens: {
      '2xsm': '250px',
      xsm: '320px'
    }
  }
}
如何解决HTML中class较为拥挤的感官?

开发中你可能看到是这样的:

 <span class="text-[30px] leading-[37px] text-white align-middle ml-[12px] mr-[4px] font-bebasNeue"></span>

特别是遇到设计稿修改的,在调整样式的过程中可能又写了些类名,由于视觉上比较拥挤就有可能出现重复或者覆盖的情况。我这边后是这么处理的:

  • 对于较长的,比如超过5个class的就设定个自定义class, 然后用这个class apply你要写的tailwindcss类名。
  • 对于有hover,media-query的,可以多写几行区分,如:
<span class="custom"></span>
<style lang="postcss">
.custom {
 @apply text-[30px] leading-[37px] text-white align-middle ml-[12px] mr-[4px] font-bebasNeue;
 @apply md:text-[40px] md:leading-[50px];
 @apply hover:text-red;
}
</style>

对于简单的项目可以这样,对于复杂点的项目(如果设计稿是组件化标准设计的话)个人建议直接加一层全局自定义class(计算机相关问题中,万事不决加一层),每个class中apply tailwindcss的class。换个角度理解就是自己写UI框架,然后框架里每个类的声明块都替换为tailwindcss的类名。

Vite

遇到的问题

  • 如何像Webpack那样拆包?
  • 如何优雅增加环境变量?
  • 打包时如何替换本地静态资源为CDN(OSS)资源?
  • 对于有二级目录的线上环境,如何设置?

解决方式:

如何像Webpack那样拆包?

这个直观体验,rollup配置比webpack清晰太多了

// rollup的拆包配置
 build: {
   rollupOptions: {
     output: {
       manualChunks(id) {
         if (id.includes('html2canvas')) {
           return 'html2canvas'
         } else if (id.includes('AMapLoader')) {
           return 'AMapLoader'
         } else if (id.includes('echarts')) {
            return 'echarts'
         } else if (id.includes('node_modules')) {
            return 'vendor'
         }
       }
     }
   }
}
如何优雅增加环境变量?

这个接触过vue-cli的同学可能会说,这不是很简单么,直接在vite.config.js里写process.env.your_variable不就行了?

模式和环境变量 | Vue CLI

但是如果前端页面想用这些变量是无法通过process访问到的,因为前端的模块是基于ESModule的规范,process是基于commonJS的规范。BTW,process可以在vite.config.js和postcss.config.js中能被访问到。

这是为什么呢?查看了下Vite依赖构建相关描述:

CommonJS 和 UMD 兼容性:开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM

所以我大胆猜测了下,vite.config.js和postcss.config.js的处理应该是在“依赖预构建”前,那时还处在nodejs环境中,所以可以访问到process(求大佬拍醒)。所以应该怎么处理呢?

自己查百度.png

百度不行谷歌.png

还好还好,文档里都有。
Env Variables and Modes | Vite
import.meta - JavaScript | MDN

嗯,问题不大。import.meta + .env,稍微配置下就能解决页面中需要用到环境变量的问题(记得变量前缀加上VITE_)。
但是我还想在vite.config.js中使用变量怎么办呢,直接写process么?虽然可行,但是不优雅,因为已经用了.env,就不要把变量散落在项目各个文件中,那还是在.env文件里写吧。啥?.env的变量在vite.config.js里读不到!咋解决?

百度查不到.png

谷歌不行砸电脑.png

还好还好,谷歌查到了,不用砸电脑了。

env variables not accessible in vite.config.js · Issue #1930 · vitejs/vite

祖师爷说无法用,因为这是个

先有鸡还是先有蛋的问题

graph TD
vite.config.js --> merge配置 --> 获取.env里变量
  1. vite.config.js可以配置项目根路径, .env在项目根路径
  2. 要merge vite.config.js的options后,才能根据配置的根路径去读取.env里变量
  3. vite.config.js在merge options之前如何让它吐变量给你? 还好这个问题后面有个小哥的回复让我虎躯一震, I get it!

loadEnv.png

祖师爷在这给你关上门,却在那给你打开了一扇窗。

loadEnv-code.png

  • mode:当前配置环境 development or production
  • envDir: .env文件所在目录
  • prefix: 这个我没用过,结合vite里对 .env 的说明应该是变量的前缀,因为对于VITE_前缀的可以暴露到前端页面中,其他的只在构建里能访问到。

至此,环境变量问题优雅的解决了,上代码!

// vite.config.js
 const { VITE_OSS_URL, VITE_API_PREFIX, VITE_API_DEV_TARGET } = loadEnv(mode, process.cwd())
打包时如何替换本地静态资源为CDN(OSS)资源?

这次的项目中,我主要是用来替换图片。我想做到的是效果是:

  • 开发过程中不做任何一丝改动,当然也不把图片放到静态目录中(/public),因为放到静态目录中,打包时是不会处理静态目录下的资源的,万一后续要对图片做个统一处理就不好办了,所以仍旧像往常一样引入图片。 如:
<!-- In template -->
<img src="@/assets/images/xxx.png" />
/* In styles */
.example {
  background:url(@/assets/images/xxx.png);
}
// js引入方式
import exampleImg from '@/assets/images/xxx.png'
  • 打包的时候,直接替换,不用对开发代码做任何修改,保证部署的时候不用记录额外配置,减少心智负担。 朝着这2个目标,我开始慢慢探索,凭着以前对vue-cli配置的了解,首先去翻阅了vite的assets相关的配置,也确实找到了线索:

assetsDir 指定生成静态资源的存放路径(相对于build.outDir)

然而这个对所有的css和js也会造成影响,不行。
既然build的时候使用rollup的,那么就在Awesome Rollup里查找看看,经过几次尝试,找到了smartAsset,再经过几番折腾,终于把部分图片替换为OSS上的链接了,配置如下

// vite.config.js
build: {
  rollupOptions: {
   plugins: [
     smartAsset({
       extensions: ['.png', '.jpg'],
       url: 'copy',
       publicPath: VITE_OSS_URL
     })
   ]
  }
}

sormy/rollup-plugin-smart-asset

为什么是部分呢?
因为这个只能处理两种引入方式的图片:

  1. import形式的图片(用于第三方插件要引入项目里自定义的图片,如高德地图的站点marker,第三方可不会识别你alias的标识,如 imageUrl: '@/assets/images/location.png')
  2. img标签里的图片

background里如何处理?
backgroundg跟tailwindcss相关,tailwindcss跟postcss相关,那只能查下postcss相关插件了,这个也折腾了几次找到postcss-url。

postcss/postcss-url

配置方式和smartAsset有点相似,但主要还是这句话:

postcss-url.png

也就是在copy类型下,自定义url的return函数可以做到CDN的效果

nice.gif

我把配置写在了vite.config.js里的css中

 css:{
   postcss:{
      plugins:[]
    }
 }

一打包测试,提示我无法识别tailwindcss的语句(@tailwind),咋回事?还是先看vite文档吧

css.postcss
内联的 PostCSS 配置(格式同 postcss.config.js),或者一个(默认基于项目根目录的)自定义的 PostCSS 配置路径。其路径搜索是通过 postcss-load-config 实现的。
注意:如果提供了该内联配置,Vite 将不会搜索其他 PostCSS 配置源。

原来配置在vite.config.js会忽略postcss.config.js,而tailwindcss的相关配置我是放在postcss.config.js里。考虑了保持文件整洁,还是把配置移动到了postcss.config.js里,如下:

require('postcss-url')({
  filter: '**/assets/images/*.png',
  url(assets, dir, options, decl, warn, result) {
    const pathStrArray = assets.pathname.split('/')
    const fileName = pathStrArray[pathStrArray.length - 1]
    // 这边保证在开发环境下继续使用本地资源,线上用CDN
    return ctx.env === 'production' ? `${VITE_OSS_URL}${fileName}` : assets.url
  },
  maxSize: 10, // maximum file size to inline (in kilobytes)
  fallback: 'copy' // fallback method to use if max size is exceeded
})

对于有二级目录的线上环境,如何设置?

首先要说下为什么要设置二级目录,因为都是一些偏H5的活动页面,页面数量较少,线上都要使用https,分开建站https证书申请过于麻烦,所以用二级目录区分不同站点。 这个相对简单,只要注意2个点: 在createRoute的时候加上根路径就行

const router = createRouter({
 history: createWebHistory(import.meta.env.BASE_URL),
 routes: []
})

这边的BASE_URL可以在vite.config.js里配置,但是也会影响development环境

{
  base : 'yourbaseurl'
}

所以我直接放在了我打包+docker镜像远程推送的脚本里
(感觉有点打脸,说好了不要将配置散落在各处)
yarn build --base=/yourbaseurl/

  1. 在nginx反向代理中加上
  • 本地的server proxy可能是 '/yourprefix/api/xxxx/'
  • 线上nginx里就要 '^~/baseurl/yourprefix/api/xxxx/'

至此各个坑终于填平,如果大家看到我的解决方式有问题或者有更好的方式欢迎大家留言!