>  기사  >  웹 프론트엔드  >  vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다.

vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다.

青灯夜游
青灯夜游앞으로
2022-10-09 19:19:242328검색

스켈레톤 화면은 금상첨화입니다. 이상적으로는 개발자가 너무 많은 주의를 기울일 필요가 없습니다. 따라서 개발 경험의 관점에서 스켈레톤 화면을 수동으로 작성하는 것은 좋은 해결책이 아닙니다. 따라서 이 기사에서는 스켈레톤 화면을 자동으로 생성하는 또 다른 방식, 즉 vite 플러그인을 통해 스켈레톤 화면을 자동으로 삽입하는 방식을 주로 연구합니다.

vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다.

【관련 추천: vuejs 비디오 튜토리얼

스켈레톤 화면에는 SPA 애플리케이션의 사용자 경험을 크게 향상시키는 두 가지 기능이 있습니다.

  • 페이지 초기 로딩 중 공백을 피하고, 경험을 SSR과 완료 사이입니다. 페이지 초기화가 완료되기를 기다리고 있습니다.
  • 일부 라우팅 구성 요소가 데이터를 로드한 후 렌더링해야 하는 공백을 피하세요

스켈레톤 화면은 사용자에게 콘텐츠가 반환된 것 같은 환상을 줄 것입니다. 잠시만 기다리면 완전한 콘텐츠를 볼 수 있으므로, 실제 콘텐츠가 준비될 때까지 화면은 스탠드인으로 배치됩니다.

저는 이전에 스켈레톤 화면을 빠르게 생성하는 아이디어를 연구했습니다. Chrome 확장 프로그램을 사용하여 웹 페이지 스켈레톤 화면 생성 일반적인 원칙은 Chrome 확장 프로그램을 통해 content.js를 삽입하는 것입니다. 페이지 DOM 인터페이스를 수정하고 마지막으로 뼈대 화면 스타일로 HTML 코드를 내보냅니다. content.js修改页面DOM接口,最终导出带有骨架屏样式的HTML代码。

当时的这个想法并没有在生产中落地,最近在折腾用户体验相关的功能,发现还是有必要继续完善一下骨架屏相关的东西。

业界对于骨架屏的应用,也有好几种方案

  • 直接让设计师提供页面对应的骨架屏设计图
    • 导出svgbase64图片嵌入代码中,比较影响项目体积
    • 开发手动编写样式,工作量较大
  • 通过组件编写骨架屏
    • 诸如vue-content-loaderreact-content-loader等组件,可以通过svg快速编写骨架屏内容,但输出的产物与真实页面有一定差距,不容易实现定制化骨架屏需求。
    • 一些组件库,如vantvarlet也提供了skeleton组件,通过配置参数的形式控制生成骨架屏内容,其缺点也是定制化程度较差
  • 自动生成骨架屏
    • page-skeleton-webpack-plugin等比较成熟的自动骨架屏方案,甚至有专门的UI界面来控制生成不同一面的骨架屏,缺点是生成的骨架屏代码较大,影响项目体积
    • 借助puppeteer
    • 당시 아이디어는 아직 프로덕션에 구현되지 않았습니다. 최근 사용자 경험 관련 기능을 손보면서 스켈레톤 화면과 관련된 부분을 계속해서 개선할 필요가 있다는 것을 알게 되었습니다.
    • 업계에는 스켈레톤 스크린 적용을 위한 여러 솔루션도 있습니다
디자이너에게 페이지에 해당하는 스켈레톤 스크린 디자인 도면을 직접 요청하세요

svg 또는 base64 내보내기 이미지 코드에 포함되어 있으면 프로젝트 볼륨에 더 많은 영향을 미칩니다. 수동으로 작성된 스타일을 개발하려면 더 큰 작업 부하가 필요합니다. react-content-loader

및 기타 구성요소를 사용하면 빠르게 작성할 수 있습니다. svg를 통해 스켈레톤 화면 콘텐츠를 제작하고 있지만, 출력된 결과물과 실제 페이지 사이에 일정한 간격이 있는 맞춤형 스켈레톤 화면에 대한 수요를 실현하기는 쉽지 않습니다.

vant

, varlet

skeleton 구성요소도 제공합니다. 구성 매개변수의 형태는 스켈레톤 화면 콘텐츠의 생성을 제어합니다. vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다.

스켈레톤 화면 자동 생성

vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다. page-skeleton-webpack-plugin 및 기타 상대적으로 성숙한 자동 스켈레톤 화면 솔루션에는 다양한 측면의 스켈레톤 화면 생성을 제어하는 ​​특수 UI 인터페이스도 있습니다. 단점은 생성된 스켈레톤 화면 코드가 더 커서 프로젝트 크기에 영향을 미친다는 것입니다

puppeteer 페이지의 해당 뼈대를 렌더링하는 헤드리스 브라우저 화면 콘텐츠는 매우 의존적입니다. 뼈대 화면 콘텐츠를 생성하려면 Chrome 확장 프로그램을 사용하세요. 이는 헤드리스 브라우저의 원리와 본질적으로 유사합니다. 이상적으로는 개발자가 이에 너무 많은 주의를 기울일 필요가 없으므로 개발 경험 관점에서 스켈레톤 화면을 수동으로 작성하는 것은 좋은 해결책이 아닙니다. 따라서 이 기사에서는 스켈레톤 화면을 자동으로 생성하는 또 다른 방식, 즉 vite 플러그인을 통해 스켈레톤 화면을 자동으로 삽입하는 방식을 주로 연구합니다.

효과 미리보기

🎜프론트엔드 지능형 탐색, 스켈레톤 화면 낮음 -코드 자동 생성 솔루션 실습 🎜🎜🎜🎜vite-plugin-vue-inspector🎜이 플러그인을 구현하면 소스 코드의 일부 정보가 페이지에 주입됩니다🎜🎜🎜Skeleton 화면 - WeChat Mini 프로그램 개발 문서🎜, 미니 프로그램 유사한 빠른 생성을 제공하는 개발자 도구 현재의 페이지 스켈레톤 화면 솔루션🎜🎜🎜우선 디자인 도면이나 실제 페이지를 스켈레톤 화면으로 자동 변환할 수 있는 솔루션을 모색해야 합니다. 아마도 다음과 같은 아이디어가 있을 것입니다🎜
  • 컴파일 툴을 사용해 코드에 작성된 HTML 템플릿을 파싱하고 스켈레톤 화면을 생성합니다.
  • 스케치, figma 등 디자인 소스에서 시작하여 플러그인을 통해 스켈레톤 화면 내용을 내보냅니다.
  • 실제 페이지의 DOM을 직접 운영하고 Skeleton 화면 콘텐츠 생성

기존 스타일 활용

세 번째 아이디어가 구현 비용이 가장 저렴하고 가장 친숙한 것 같습니다. 이는 Chrome 확장 프로그램을 사용하여 웹 페이지 스켈레톤 화면 생성에서도 사용되는 솔루션이므로 여기서는 구체적인 구현 세부 사항을 반복하지 않습니다. 간단히 요약하겠습니다

  • 개발 환경에서는 특정 스위치를 수동으로 트리거하여 생성을 시작합니다. 각 페이지에 해당하는 스켈레톤 화면 콘텐츠
  • 노드 유형에 따라 페이지를 다른 블록으로 분할
  • 사용자 정의 노드 유형 지원, 노드 무시 또는 숨기기
  • 최종 내보내기는 원본의 구조와 CSS를 재사용하는 HTML 코드 조각입니다. 페이지 레이아웃 코드

에 대한 핵심 API는 단 하나입니다. 해당 항목 노드를 전달하고 변환된 스켈레톤 화면 코드

const {name, content} = renderSkeleton(sel, defaultConfig)

예를 들어 다음 구조

<div>
		<div>卡片标题</div>
	  <div>卡片内容卡片内容</div>
</div>

로 생성된 스켈레톤 화면 코드는

<div>
		<div>卡片标题</div>
	  <div>卡片内容卡片内容</div>
</div>

여기에 있습니다. 블록 및 sk-text와 같은 sk- 스타일 클래스는 생성 중에 추가되어 원래 스타일을 덮어쓰고 원래 레이아웃 스타일을 유지하면서 스켈레톤 화면의 회색 배경을 표시합니다. . <code>sk-blocksk-text等样式类都是在生成时追加上去的,用于覆盖原本的样式,从而展示骨架屏的灰色背景,但同时保留原本的布局样式。

renderSkeleton的调用时机由开发者自己控制,我们可以向页面注入一个按钮,点击时调用

function createTrigger() {
  const div: HTMLDivElement = document.createElement('div')
  div.setAttribute('style', 'position:fixed;right:0;bottom:20px;width:50px;height:50px;background:red;')
  div.addEventListener('click', function () {
    renderSkeleton('[data-skeleton-root]')
  })
  document.body.appendChild(div)
}

if(process.end.NODE_ENV ==='development'){
 createTrigger() 
}

在得到骨架屏代码之后,在业务代码中通过一个loading标志位控制展示的是骨架屏还是真实内容

<script>
import {ref, onMounted} from "vue";

const loading = ref(true);
const list = ref<number>([]);

async function fetchList() {
  await sleep(1000)
  list.value = [1, 2, 3, 4, 5]
  loading.value = false
}

onMounted(() => {
  fetchList()
})

</script>

<template>
  <div>
    <div>
      <!--这里的都是骨架屏代码-->
      <div>
          <div>卡片标题</div>
          <div>卡片内容卡片内容</div>
      </div>
   </div>
    <div>
     <div>
          <div>卡片标题</div>
          <div>卡片内容卡片内容</div>
      </div>
    </div>
  </div>

</template>

<style>
// 相关的样式
</style>

可以看到,v-if="loading"标签内部的代码,就是生成的骨架屏内容。需要注意的是,既然骨架屏与业务代码在一起,也会参与Vue的SFC编译,因此骨架屏标签上面的一些动态属性如scopeid等,需要移除。关于scopeid带来的其他问题,后面的篇幅会提到,这也会影响整个renderSkeleton的实现。

如果每次在调用renderSkeleton拿到骨架屏代码之后,手动修改业务代码替换loading展示的内容,无疑非常麻烦,现在来研究一下如何自动化解决这个问题。

前面提到,骨架屏主要应用在首屏渲染需要和路由页面切换时

  • SPA首屏渲染优化
  • 路由组件切换时的占位内容

接下来看看这两种场景下如何自动注入骨架屏代码

组件内渲染骨架屏

我们可以通过占位符来声明当前组件对应骨架屏代码的地方,比如

<div>__SKELETON_APP_CONTENT__</div>
<div>真实业务代码</div>

在获得骨架屏代码之后,将__SKELETON_APP_CONTENT__这里的内容替换成真实的骨架屏代码即可。

如何替换呢?vite插件提供了一个transform的钩子

const filename = './src/skeleton/content.json'

function SkeletonPlaceholderPlugin() {
  return {
    name: 'skeleton-placeholder-plugin',
    enforce: 'pre',
    transform(src, id) {
      if (/\.vue$/.test(id)) {
        const {content} = fs.readJsonSync(filename)
        // 约定对应的骨架屏占位符
        let code = src.replace(/__SKELETON_(.*?)_CONTENT__/igm, function (match) {
          return content
        })

        return {
          code,
        }
      }
      return src
    },
  } as Plugin
}

其中./skeleton.txt中的内容,就是在调用renderSkeleton后生成的骨架屏代码,通过transformpre,我们就可以在vue插件解析SFC之前,先将骨架屏占位符替换成真正的代码,再参与后续的编译流程。

这里还需要解决一个问题:renderSkeleton是在客户端触发的,而skeleton.txt是在开发服务器环境下的,需要有一个通信的机制将客户端生成的骨架屏代码发送到项目目录下面。

vite插件提供了一个configureServer钩子,用来配置vite开发服务器,我们可以加一个中间件,用来提供一个保存骨架屏代码的接口

function SkeletonApiPlugin() {
  async function saveSkeletonContent(name, content) {
    await fs.ensureFile(filename)
    const file = await fs.readJson(filename)
    file[name] = {
      content,
    }
    await fs.writeJson(filename, file)
  }

  return {
    name: 'skeleton-api-plugin',
    configureServer(server) {
      server.middlewares.use(bodyParser())
      server.middlewares.use('/update_skeleton', async (req, res, next) => {
        const {name, content, pathname} = req.body
        await saveSkeletonContent(name, content, pathname)
        // 骨架屏代码更新之后,重启服务
        server.restart()
        res.end('success')
      })
    },
  }
}

然后在renderSkeleton之后,调用这个接口上传生成的骨架屏代码即可

async function sendContent(body: any) {
  const response = await fetch('/update_skeleton', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  })
  const data = await response.text()
}

const __renderSkeleton = async function () {
  const {name, content} = renderSkeleton(".card-list", {})

  await sendContent({
    name,
    content
  })
}

bingo!大功告成。梳理一下流程

  • 开发者在某个时候手动调用__renderSkeleton,就会自动生成当前页面的骨架屏

  • 将骨架屏代码发送给vite接口,更新本地skeleton/content.json中的骨架屏代码,

  • vite重启服务后,重新触发pre队列中的skeleton-content-component

    renderSkeleton 호출 타이밍은 개발자가 제어합니다. 페이지에 버튼을 삽입하고 클릭 시
function SkeletonApiPlugin() {
  return {
    name: 'skeleton-aip-plugin',
    transformIndexHtml(html) {
      let {content} = fs.readJsonSync(filename)
      const code = `
<script>
var map = ${JSON.stringify(content)}
var pathname = window.location.pathname
var target = Object.values(map).find(function (row){
  return row.pathname === pathname
})
var content = target && target.content || &#39;&#39;
document.write(content)
</script>
      `
      return html.replace(/__SKELETON_CONTENT__/, code)
    }

  }
}
를 호출할 수 있습니다. 비즈니스 코드플래그는 스켈레톤 화면을 표시할지 실제 콘텐츠를 표시할지를 제어합니다

<div>__SKELETON_CONTENT__</div>
v-if="loading" 태그 내부의 코드가 생성된 스켈레톤 화면임을 알 수 있습니다 콘텐츠. 스켈레톤 화면은 비즈니스 코드와 함께 있기 때문에 Vue의 SFC 컴파일에도 참여하므로 scopeid와 같은 스켈레톤 화면 라벨의 일부 동적 속성을 제거해야 합니다. . 범위 ID로 인해 발생하는 다른 문제에 대해서는 나중에 언급할 것이며 이는 전체 renderSkeleton 구현에도 영향을 미칠 것입니다.
  • 스켈레톤 화면 코드를 얻기 위해 renderSkeleton을 호출할 때마다 로드되는 표시 내용을 대체하기 위해 비즈니스 코드를 수동으로 수정하는 것은 의심할 여지 없이 매우 번거로울 것입니다. 이제 이 문제를 자동으로 해결하는 방법을 연구해 보겠습니다. .

    앞서 언급했듯이 스켈레톤 화면은 주로 첫 번째 화면 렌더링이 필요하고 페이지 전환이 라우팅될 때 사용됩니다.
  • SPA 첫 번째 화면 렌더링 최적화

  • 라우팅 구성 요소 전환 시 자리 표시자 콘텐츠

이 두 가지 시나리오를 살펴보겠습니다. 자동으로 스켈레톤 화면 코드 삽입🎜

구성 요소 내에서 스켈레톤 화면 렌더링🎜🎜자리 표시자를 사용하여 현재 구성 요소가 스켈레톤 화면에 해당하는 위치를 선언할 수 있습니다. 예를 들어 🎜
  const {name, content} = renderSkeleton(sel, defaultConfig)
  // 如果是hash路由,就替换成fragment
  const {pathname} = window.location
  await sendContent({
    name,
    content,
    pathname // 保存骨架屏代码的时候顺道把pathname也保存了
  })
🎜스켈레톤 화면 코드를 얻은 후 여기 __SKELETON_APP_CONTENT__의 콘텐츠를 실제 스켈레톤 화면 코드로 바꾸세요. 🎜🎜교체하는 방법은 무엇인가요? vite 플러그인은 transform 후크를 제공합니다🎜
<div></div>
🎜./skeleton.txt의 내용은 renderSkeleton 화면 코드를 호출한 후 생성된 스켈레톤입니다. , transformpre를 통해 vue 플러그인이 SFC를 구문 분석하기 전에 스켈레톤 화면 자리 표시자를 실제 코드로 대체한 다음 후속 컴파일 프로세스에 참여할 수 있습니다. 🎜🎜여기서 해결해야 할 또 다른 문제가 있습니다. renderSkeleton이 클라이언트에서 트리거되고 skeleton.txt가 개발 서버 환경에 있어야 합니다. 통신 메커니즘 클라이언트가 생성한 스켈레톤 화면 코드가 프로젝트 디렉토리로 전송됩니다. 🎜🎜vite 플러그인은 vite 개발 서버를 구성하기 위한 configureServer 후크를 제공하여 스켈레톤 화면 코드🎜
<div></div>
🎜를 저장한 다음 renderSkeleton 그런 다음 이 인터페이스를 호출하여 생성된 스켈레톤 화면 코드🎜<pre class="brush:php;toolbar:false">.card[data-v-xxx] {}</pre>🎜bingo를 업로드하면 완료됩니다. 프로세스를 정리해보자🎜🎜🎜🎜개발자가 어떤 시점에서 수동으로 <code>__renderSkeleton을 호출하면 현재 페이지의 스켈레톤 화면이 자동으로 생성됩니다🎜🎜🎜🎜스켈레톤 화면 코드를 vite 인터페이스로 보냅니다. 🎜🎜🎜🎜vite가 서비스를 다시 시작한 후 로컬 skeleton/content.json의 스켈레톤 화면 코드를 업데이트하고 skeleton-content-comComponent를 다시 트리거합니다. >pre queue 플러그인은 스켈레톤 화면 자리 표시자를 대체하고 스켈레톤 화면 코드를 삽입하며 전체 스켈레톤 화면 삽입 프로세스를 완료합니다. 🎜🎜🎜🎜전체 프로세스에서 개발자는 다음 두 단계만 완료하면 됩니다.🎜🎜🎜🎜비즈니스 코드에서 스켈레톤 화면 자리 표시자를 선언하세요🎜🎜🎜🎜스켈레톤 화면 코드 생성을 트리거하려면 버튼을 클릭하세요🎜🎜🎜🎜 라우팅 전환을 위한 스켈레톤 화면은 주로 라우팅 구성요소에 사용됩니다. 자세한 내용은 여기에서 설명하고 하나씩 확장하지는 않겠습니다. 🎜

首屏渲染骨架屏

骨架屏对于SPA首屏渲染优化,需要在应用初始化之前就开始渲染,即需要在id="app"的组件内植入初始化的骨架屏代码

如果是服务端预渲染,可以直接返回填充后的代码;如果是客户端处理,可以通过document.write处理,我们这里只考虑纯SPA引用,由前端处理骨架屏的插入。

我们可以通过vite插件提供的transformIndexHtml钩子注入这段逻辑

function SkeletonApiPlugin() {
  return {
    name: 'skeleton-aip-plugin',
    transformIndexHtml(html) {
      let {content} = fs.readJsonSync(filename)
      const code = `
<script>
var map = ${JSON.stringify(content)}
var pathname = window.location.pathname
var target = Object.values(map).find(function (row){
  return row.pathname === pathname
})
var content = target && target.content || &#39;&#39;
document.write(content)
</script>
      `
      return html.replace(/__SKELETON_CONTENT__/, code)
    }

  }
}

对应的index.html代码为

<div>__SKELETON_CONTENT__</div>

根据用户当前访问的url,读取该url对应的骨架屏代码,然后通过document.write写入骨架屏代码。这里可以看出,在生成骨架屏代码时,我们还需要保留对应页面url的映射,甚至需要考虑动态化路由的匹配问题。这个也比较简单,在提交到服务端保存时,加个当前页面的路径参数就行了

  const {name, content} = renderSkeleton(sel, defaultConfig)
  // 如果是hash路由,就替换成fragment
  const {pathname} = window.location
  await sendContent({
    name,
    content,
    pathname // 保存骨架屏代码的时候顺道把pathname也保存了
  })

整理一下流程

  • 用户访问url
  • 根据页面url,加载对应的骨架屏代码,填充在根节点下
    • 如果是服务端预渲染,可以直接返回填充后的代码
    • 如果是客户端处理,可以通过document.write处理
  • 用户看见渲染的骨架屏内容
  • 初始化应用,加载页面数据,渲染出真实页面

开发者在点击生成当前页面的骨架屏时,保存的骨架屏代码,既可以用在路由组件切换时的骨架屏,也可以用在首屏渲染时的骨架屏,Nice~

存在的一些问题

利用vite插件注入骨架屏的代码,看起来是可行的,但在方案落地时,发现了一些需要解决的问题。

存在原始样式不生效的场景

由于生成的骨架屏代码是依赖原始样式的,

<div></div>

对应的骨架屏代码

<div></div>

其中的sk-block只会添加一些灰色背景和动画,至于整体的尺寸和布局,还是card这个类来控制的。

这么设计的主要原因是:即使card的尺寸布局发生了变化,对应的骨架屏样式也会一同更新。

但在某些场景下,原始样式类无法生效,最具有代表性的问题就Vue项目的的scoped css

我们知道,vue-loader@vitejs/plugin-vue等工具解析SFC文件时,会为对应组件生成scopeId(参考之前的源码分析:从vue-loader源码分析CSS-Scoped的实现),然后通过postcss插件,通过组合选择器实现了类似于css作用域的样式表

.card[data-v-xxx] {}

我们的生成骨架屏的时机是在开发环境下进行的,这就导致在生产环境下,看到的骨架屏并没有原始样式类对应的尺寸和布局。

下面是vite vue插件的源码

export function createDescriptor(
  filename: string,
  source: string,
  { root, isProduction, sourceMap, compiler }: ResolvedOptions
): SFCParseResult {
  const { descriptor, errors } = compiler.parse(source, {
    filename,
    sourceMap
  })

  const normalizedPath = slash(path.normalize(path.relative(root, filename)))
  descriptor.id = hash(normalizedPath + (isProduction ? source : ''))

  cache.set(filename, descriptor)
  return { descriptor, errors }
}

vue-loader中生成scopeid的方法类似,看了一下貌似并没有提供自定义scopeid的API。

因此对于同一个文件而言,生产环境和非生产环境参与生产hash的参数是不一样的,导致最后得到的scopeid 也不一样。

对于组件内渲染骨架屏这种场景,我们也许可以不考虑scopeid,因为在SFC编译之前,我们就已经通过transform钩子注入了对应的骨架屏模板,对于SFC编译器而言,骨架屏代码和业务代码都在同一个组件内,也就是说他们最后都会获得相同的scopeid,这也是为什么生成的骨架屏代码,要擦除HTML标签上面的scopeid的原因。

但如果骨架屏依赖的外部样式并不在同一个SFC文件内,也会导致原始的骨架屏样式不生效。

<template>
  <div>
    <div>
      <div>
          <div>卡片标题</div>
          <div>卡片内容卡片内容</div>
      </div>
   </div>
    <div>
     <card></card>
    </div>
  </div>

</template>

<style>
// card 样式不在这个SFC下面,但是插入的骨架屏代码却在这个SFC文件下
</style>

此外,对于首屏渲染骨架屏这种场景,就不得不考虑scopeid了。如果骨架屏依赖的原始样式是携带作用域的,那就必须要保证骨架屏代码与生产环境的样式表一致

.card[data-v-xxx] {}
<div></div>

这样,首屏渲染依赖的骨架屏和组件内渲染的骨架屏就产生了冲突,前者需要携带scopeid,而后者又需要擦除scopeid。

为了解决这个冲突,有两种办法

  • 在保存骨架屏代码时,同时保存对应的scopeid,并在首屏渲染时,为每个标签上手动添加scopeid。
  • 原始骨架屏代码就携带scopeid,而在替换组件内的骨架屏占位符时再擦除scopeid

但不论通过何种方式保证两个环境下生成的scopeid 一致(甚至是通过修改插件源码的方式),可能也会存在旧版本的骨架屏携带的scopeid和新版本对应的scopeid 不一致的问题,即旧版本的class和新版本的class不一致。

要解决这个问题,除非在每次修改源码之后,都更新一下骨架屏,由于生成骨架屏这一步是手动的,这与自动化的目的背道而驰了。

因此,看起来利用原始类同步真实DOM的布局和尺寸,在scoped css中并不是一个十分完善的设计。

骨架屏代码质量

第二个不是那么重要的问题是生成的骨架屏代码,相较于手动编写,不够精简。

虽然在源代码中,骨架屏代码被占位符替代,但在编译阶段,骨架屏会编译到render函数中,可能造成代码体积较大,甚至影响页面性能的问题。

这个问题并不是一个阻塞性问题,可以后面考虑如何优化,比如骨架屏仍旧保留v-for等指令,组件可以正常编译,而首屏渲染的骨架屏需要通过自己解析生成完整的HTML代码。

解决方案

上面这两个问题的本质都是因为骨架屏生成方案导致的,跟后续保存骨架屏代码并自动替换并没有多大关系,因此我们只需要优化骨架屏生成方案即可。

既然依赖于原始样式生成的骨架屏代码存在这些缺点,有没有什么解决办法呢?

事实上,我们对于骨架屏是否更真实内容结构的还原程度并没有那么高的要求,也并没有要求骨架屏要跟业务代码一直保持一致,既然导出HTML骨架屏代码比较冗余和繁琐,我们可以换一换思路。

不使用scoped css

其他比较常用的CSS方案如css moudlecss-in-js或者是全局原子类css如tailwindwindicss等,如果输出的是纯粹的CSS代码,且生产环境和线上保持一致,理论上是不会出现scopeid这个问题的。

但Vue项目中,scoped css方案应该占据了半壁江山,加上我自己也比较喜欢scoped css,因此这是一个绕不过去的问题。

将骨架屏页面自动转成图片

第一种思路将骨架屏页面保存为图片,这样就不用再依赖原始样式了。

大概思路就是:在解析当前页面获得骨架屏代码之后,再通过html2canvas等工具,将已经渲染的HTML内容转成canvas,再导出base64图片。

import html2canvas from 'html2canvas'

const __renderSkeleton = async function (sel = 'body') { 
  const {name, content} = renderSkeleton(sel, defaultConfig)
  const canvas = await html2canvas(document.querySelector(sel)!)
  document.body.appendChild(canvas);
  
  const imgData = canvas.toDataURL()
  // 保存<img  alt="vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다." >作为骨架屏代码
}

这种通过图片替代HTML骨架屏代码的优点在于兼容性好(对应的页面骨架屏甚至可以用在App或小程序中),容易迁移,不需要依赖项目代码中的样式类。

但是html2canvas生成的图片也不是百分百还原UI,需要足够纯净的代码原始结构才能生成符合要求的图片。此外图片也存在分辨率和清晰度等问题。

也许又要回到最初的起点,让设计大佬直接导出一张SVG?(开个玩笑,我们还是要走自动化的道路

复制一份独立的样式表

如果能够找到骨架屏代码中每个标签对应的class在样式表中定义的样式,类似于Chrome dev tools中的Elements Styles面板,我们就可以将这些样式复制一份,然后将scopeid替换成其他的选择器

vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다.

开发环境下的样式都是通过style标签引入,因此可以拿到页面上所有的样式表对象,提取符合对应选择器的样式,包括.className.className[scopeId]这两类

写一个Demo

const { getClassStyle } = (() => {
    const styleNodes = document.querySelectorAll("style");
    const allRules = Array.from(styleNodes).reduce(
        (acc, styleNode) => {
            const rules = styleNode.sheet.cssRules;
            acc = acc.concat(Array.from(rules));
            return acc;
        },
        []
    );
    const getClassStyle = (selectorText) => {
        return allRules.filter(
            (row) => row.selectorText === selectorText
        );
    };

    return {
        getClassStyle,
    };
})();

const getNodeAttrByRegex = (node, re) => {
    const attr = Array.from(node.attributes).find((row) => {
        return re.test(row.name);
    });
    return attr && attr.name;
};

const parseNodeStyle = (node) => {
    const scopeId = getNodeAttrByRegex(node, /^data-v-/);
    return Array.from(myBox.classList).reduce((acc, row) => {
        const rules = getClassStyle(`.${row}`);

      	// 这里没有再考虑两个类.A.B之类的组合样式了,排列组合比较多
        return acc
            .concat(getClassStyle(`.${row}`))
            .concat(getClassStyle(`.${row}[${scopeId}]`));
    }, []);
};
const rules = parseNodeStyle(myBox);
console.log(rules);

这样就可以得到每个节点在scoped css的样式,然后替换成骨架屏依赖的样式。

但现在要保存的骨架屏代码的HTML结构之外,还需要保存对应的那份CSS代码,十分繁琐

提取必要的布局信息生成骨架屏

能否像html2canvas的思路一样,重新绘制一份骨架屏页面出来呢

通过getComputedStyle可以获取骨架屏每个节点的计算样式

const width = getComputedStyle(myBox,null).getPropertyValue('width');

复用页面结构,把所有布局和尺寸相关的属性都枚举出来,一一获取然后转成行内样式,看起来也是可行的。

但这个方案需要逐步尝试完善对应的属性列表,相当于复刻一下浏览器的布局规则,工作量较大,此外还需要考虑rem、postcss等问题,看起来也不是一个明智的选择。

postcss插件

既然scopeid是通过postcss插入的,能不能在对应的样式规则里面加一个分组选择器,额外支持一下骨架屏的呢

比如

.card[data-v-xxx] {}

修改为

.card[data-v-xxx], .sk-wrap .card {}

这样,只要解决生产环境和开发环境scopeid不一致的问题就可以了。

编写postcss插件可以参考官方文档:编写一个postcss 插件

vue/compuler-sfc源码中发现,scopedPlugin插件位于传入的postcssPlugins之后,而我们编写的插件需要位于scopedPlugin之后才行,

vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다.

如果不能修改源码,只有继续从vite 插件的transform钩子入手了,在transform中手动执行postcss进行编译

继续编写一个SkeletonStylePlugin插件

const wrapSelector = '.sk-wrap'
export function SkeletonStylePlugin() {
  return {
    name: 'skeleton-style-plugin',
    transform(src: string, id: string) {
      const {query} = parseVueRequest(id)

      if (query.type === 'style') {
        const result = postcss([cssSkeletonGroupPlugin({wrapSelector})]).process(src)
        return result.css
      }
      return src
    }
  }
}

注意该插件要放在vue插件后面执行,因为此时得到的内容才是经过vue-compiler编译后的携带有scopeid 的样式。

其中cssSkeletonGroupPlugin是一个postcss插件

import {Rule} from 'postcss'

const processedRules = new WeakSet<rule>()

type PluginOptions = {
  wrapSelector: string
}
const plugin = (opts: PluginOptions) => {
  const {wrapSelector} = opts

  function processRule(rule: Rule) {
    if (processedRules.has(rule)) {
      return
    }
    processedRules.add(rule)
    rule.selector = rewriteSelector(rule)
  }

  function rewriteSelector(rule: Rule): string {
    const selector = rule.selector || ''

    const group: string[] = []
    selector.split(',').forEach(sel => {
      // todo 这里需要排除不在骨架屏中使用的样式
      const re = /\[data-v-.*?\]/igm
      if (re.test(sel)) {
        group.push(wrapSelector + ' ' + sel.replace(re, ''))
      }
    })

    if(!group.length) return selector
    return selector + ', ' + group.join(',')
  }

  return {
    postcssPlugin: 'skeleton-group-selector-plugin',
    Rule(rule: Rule) {
      processRule(rule)
    },
  }
}
plugin.postcss = true

export default plugin</rule>

这个插件写的比较粗糙,只考虑了常规的选择器,并依次追加分组选择器。测试一下

.test1[data-v-xxx] {}

成功编译成了

.test1[data-v-xxx], .sk-wrap .test1 {}

这样,只需要将骨架屏代码外边包一层sk-wrap,骨架屏中的样式就可以正常生效了!

content && document.write('<div>' +content+'</div>')

看起来解决了一个困扰我很久的问题。

小结

至此,一个借助于Vite插件实现自动骨架屏的方案就实现了,总结一下整体流程

首先初始化插件

import {SkeletonPlaceholderPlugin, SkeletonApiPlugin} from '../src/plugins/vitePlugin'

export default defineConfig({
  plugins: [
    SkeletonPlaceholderPlugin(),
    vue(),
    SkeletonApiPlugin(),
  ],
  build: {
    cssCodeSplit: false
  }
})

然后填写占位符,对于首屏渲染的骨架屏

<div>__SKELETON_CONTENT__</div>

对于组件内的骨架屏


__SKELETON_APP_CONTENT__
<div></div>

接着初始化客户端触发器,同时向页面插入一个可以点击生成骨架屏的按钮

import '../../src/style/skeleton.scss'
import {initInject} from '../../src/inject'

createApp(App).use(router).mount('#app')

// 开发环境下才注入
if (import.meta.env.DEV) {
  setTimeout(initInject)
}

点击触发器,自动将当前页面转换成骨架屏

通过HTTP将骨架屏代码发送到插件接口,通过fs写入本地文件./src/skeleton/content.json中,然后自动重启vite server

页面内组件的占位符会通过SkeletonPlaceholderPlugin替换对应占位符的骨架屏代码,loading生效时展示骨架屏

首屏渲染页面时,通过location.pathname插入当前路径对应的骨架屏代码,直接看见骨架屏代码

所有骨架屏依赖的当前样式通过cssSkeletonGroupPlugin解析,通过分组选择器输出在css文件,不再依赖scopeid。

这样,一个基本自动的骨架屏工具就集成到项目中,需要进行的手动工作包括

  • 配置插件
  • 定义组件的骨架屏占位符,以及骨架屏入口data-skeleton-root="APP"
  • 必要时在标签上声明data-skeleton-type,定制骨架屏节点

整个项目比较依赖vite插件开发知识,也参考了vite@vitejs/plugin-vue@vue/compile-sfc等源码的实现细节。

所有Demo已经放在github上面了,剩下要解决的就是优化生成骨架屏的效果和质量了,期待后续吧

(学习视频分享:web前端开发编程基础视频

위 내용은 vite 플러그인을 사용하여 스켈레톤 화면을 자동화하는 방법에 대해 이야기해 보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제