Qwik展示具有可扩展和高速浏览页面的令人着迷的内容最终形式

追求爆炸式速度的可伸缩网页。

以下是对原文的中文译文:

NextJS等框架通过Hydration机制采用了流式下载内容的方法。
而Qwik则采用了一种全然不同的Resumable方法来优化速度。
Qwik是由Misko Hevery(Builder.io的CTO和AngularJS的开发者)创造的新框架。

可恢复性 vs. 水合

这是一个极端的图像,但比较从首次呈现渲染完成(也就是Core Web Vitals的度量目标)开始的过程,可以通过以下形象来解释。(实际上,Qwik方面也会涉及大约1毫秒的最低程度的JS代码下载。)

assets_YJIGb4i01jvw0SRdL5Bt_04681212764f4025b2b5f5c6a258ad6e.png

当SSR/SSG应用程序主要在客户端上启动,以ReactJS等为代表时,客户端框架需要恢复以下三个信息。

    1. 找到事件监听器并将其安装在DOM节点上,使应用程序变成交互式。

 

    1. 构建表示应用程序组件树的内部数据结构(也称为虚拟DOM)。

 

    从服务器上的存储中获取或保存数据来恢复应用程序状态。

由于JS框架的缘故,重新注水是一个非常昂贵的过程,因为这些操作会发生。尤其是在第一次加载页面时,下载不可见的捆绑JS(块)以及构建事件侦听器和DOM树的过程都是很浪费的。随着页面变得越来越庞大,我们意识到重新注水机制本身就是性能瓶颈。这些问题是因为在客户端上试图重建ReactJS(以及VueJS等)这样的JS框架导致的,这就意味着我们必须下载所有JS实现。(可重现的)

举例来说,NextJS通过以下的Stream形式来进行hydration。
我认为NextJS本身是一个托管服务,在基础设施层面上提供了Vercel的ISR机制和出色的缓存策略,这是非常优秀的,但是随着页面内容变得越来越庞大,页面内的内容变多会导致加载速度变慢,这是一个局限性。

FRxl9YcVkAAE-4G.png

顺便提一下,从理论上讲,它在内容可扩展性方面是最强大的,但是由于需要学习Qwik独特的写作方式,它的掌握难度稍高一些。是否有可能将「从理论上讲是最强大的」Qwik/QwikCity 作为前端的共同基础呢?

为什么速度如此重要?

核心网络性能指标是Google提供的指标。
它几乎是网页性能测量的事实标准指标。

Web Vitals 网络关键指标

作为主要指标

    • LCP(Largest Content Paintful):ページ内の最大のコンテンツのレンダリングにかかる時間です。短いほど良く、 2.5秒以内にする必要があります。

 

    • FID(First Input Delay):ユーザーの操作(インタラクション)が開始できるまでの時間です。将来的にはINPに重要指標が代わる予定です。短いほど良く、100ミリ秒以下にする必要があります。

 

    CLS(Cumulative Layout Shift):ページ表示時のレイアウトの位置ズレがどれくらい発生しているかの指標です。値が少ないほど良く、0.1以下に維持する必要があります。(理想は0=位置ズレなし)

为了确保几乎所有用户都能达到满意的推荐目标值,我们将总页面加载数的75百分位数作为阈值,分别针对移动设备和台式机设备进行设定。

只有满足上述三个指标中的75%推荐阈值,评估工具才将页面判定为通过,并符合核心网页指标的要求。页面速度洞察。

这个指标之所以重要,是因为它是用户体验的指标,而且 Google 已经明确表示它也考虑在搜索排名中。例如,据说雷鬼天24通过改进核心Web指标,改善了以下数值。

    • 訪問者の利益率が53.37%増加

 

    • コンバージョン率(購買率)が33.13%増加

 

    • 平均購買額が15.20%増加

 

    • 平均滞在時間が9.99%増加

 

    離脱率が35.12%低下

只从这个例子中,就可以明显看出速度就是正义。
关于React的加速方法,我已经写过以下的文章了,请参考一下。

你们的React很慢
你们的React太慢了(SSG编辑)
推荐超快速的Headless NextJS

这篇文章是前一篇文章的延续。

Qwik是如何实现可恢复下载的?

HTML优先、JavaScript后,这是提升网页速度的秘诀!

比如说,它可以以QRL格式在HTML中展开。

<div ::app-state="./AppState" 
     app-state:1234="{count: 321}">
  <div decl:template="./Counter_template"
       on:q-render="./Counter_template"
       ::.="{countStep: 5}"
       bind:app-state="state:1234">
    <button on:click="./MyComponent_increment">+5</button>
    321.
    <button on:click="./MyComponent_decrrement">-5</button>
  </div>
</div>

::app-state (application state code): アプリケーション状態変更コードをダウンロードできる URL を指します。状態更新コードは、状態を変更する必要がある場合にのみダウンロードされます。

app-state:1234 (application state instance):特定のアプリケーション インスタンスへのポインタ。状態をシリアル化することにより、アプリケーションは状態の再構築を再実行するのではなく、中断したところから再開できます。

decl:template (declare template): コンポーネント テンプレートをダウンロードできる URL を指します。コンポーネント テンプレートは、コンポーネントの状態が変化し、再レンダリングする必要があると Qwik が判断するまでダウンロードされません。

on:q-render (component is scheduled for rendering): フレームワークは、どのコンポーネントを再レンダリングする必要があるかを追跡する必要があります。これは通常、無効化されたコンポーネントの内部リストを保存することによって行われます。Qwik では、無効化されたコンポーネントのリストが属性の形式で DOM に保存されます。その後、コンポーネントは q-render イベントがブロードキャストされるのを待ちます。

::.=”{countStep: 5}” (Internal state of component instance):コンポーネントは、再ハイドレーション後に内部状態を維持する必要がある場合があります。状態を DOM に保持できます。コンポーネントが再ハイドレーションされると、継続するために必要なすべての状態が得られます。状態を再構築する必要はありません。

bind:app-state=”state:1234″ (a reference to shared application state):これにより、複数のコンポーネントが同じ共有アプリケーション状態を参照できるようになります。

Qwik与Next.js的速度比较

当然,首先需要下载解析属性值的核心JS文件,例如querySelectorAll等等,但是该JS文件的大小很小,不依赖于页面内容的大小。

XyqHIwK0xDMOGdIf5iTNW4CrLjb2-zzg33l7.png
XyqHIwK0xDMOGdIf5iTNW4CrLjb2-7pe33gz.jpeg

对于NextJS而言,由于采用了SSR Streaming渲染的方式,页面内的内容会随着内容增多而变得越来越慢。

XyqHIwK0xDMOGdIf5iTNW4CrLjb2-lmk33h8.png
XyqHIwK0xDMOGdIf5iTNW4CrLjb2-jhf33g7.jpeg

快速的生命周期

スクリーンショット 2023-10-08 18.10.39.png
スクリーンショット 2023-10-08 18.10.57.png
    • useSignal: primitiveなデータをstate管理するのに使う(React.useStateに近い)、React.useRefに該当するDOM参照もこちらで行う

 

    • useStore: オブジェクトや配列はuseSignalの代わりに使う。

useComputed$: 計算結果をメモライズすることで高速化が図れます。主に算出にコストが掛かるような処理結果を保存したい場合にwrapします。(ReactのuseMemoやuseCallbackと等価)

useResource$: useComputed$のasync版、関数内でAPIコールなどしたい場合に使う。Resourceとセットで使う。

useTask$: SSR、CSR両方で実行される。onClickやonChangeなどのブラウザのイベントハンドリングをbindすることもできる

useVisibleTask$: 該当コンポーネントのレンダリング完了後(表示時)に一度実行される。

import { component$, useSignal, useStore, useComputed$, useTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const text = useSignal('qwik');
  const debounceText = useSignal('');
  const state = useStore({ count: 0 });

  const capitalizedText = useComputed$(() => {
    // it will automatically reexecute when text.value changes
    return text.value.toUpperCase();
  });
 
  useTask$(({ track, cleanup }) => {
    const value = track(() => text.value);
    const id = setTimeout(() => (debounceText.value = value), 500);
    cleanup(() => clearTimeout(id));
  });

  const time = useSignal('paused');
  useVisibleTask$(
    ({ cleanup }) => {
      isRunning.value = true;
      const update = () => (time.value = new Date().toLocaleTimeString());
      const id = setInterval(update, 1000);
      cleanup(() => clearInterval(id));
    }
  );

  return (
    <section>
      <label>
        Enter text: <input bind:value={text} />
      </label>
      <p>Capitalized text: {capitalizedText.value}</p>
      <p>Debounced text: {debounceText}</p>
      <button onClick$={() => state.count++}>Increment</button>
      <p>Count: {state.count}</p>
      <div>{time}</div>
    </section>
  );
});

在一个家庭聚会上,亲戚们欢聚一堂,分享着快乐和笑声。他们围绕餐桌上丰盛的食物相互交谈,分享彼此的生活和经历。大家互相祝福和鼓励,气氛十分温馨和融洽。

您可以像React的Context一样,使用useContextProvider和useContext来将状态传递给子组件,而无需使用props。还可以使用context识别的createContextId来引用它们。

import { type Signal, component$, useSignal } from '@builder.io/qwik';
import {
  useContext,
  useContextProvider,
  createContextId,
} from '@builder.io/qwik';
 
export const ThemeContext = createContextId<Signal<string>>(
  'docs.theme-context'
);
 
export default component$(() => {
  const theme = useSignal('dark');
  useContextProvider(ThemeContext, theme);
  return (
    <>
      <button
        onClick$={() =>
          (theme.value = theme.value == 'dark' ? 'light' : 'dark')
        }
      >
        Flip
      </button>
      <Child />
    </>
  );
});
 
const Child = component$(() => {
  const theme = useContext(ThemeContext);
  return <div>Theme is {theme.value}</div>;
});

风格

与NextJS类似,您可以在global CSS 或者 CSS Modules中使用。
也可以使用styled-vanilla-extract库来以CSS in JS的方式编写样式。

import { style } from 'styled-vanilla-extract/qwik';
 
export const blueClass = style({
  display: 'block',
  width: '100%',
  height: '500px',
  background: 'blue',
});

使用方将导入CSS文件。

import { component$ } from '@builder.io/qwik';
import { blueClass } from './styles.css';
 
export const Cmp = component$(() => {
  return <div class={blueClass} />;
});

快城市

这是一个与NextJS在React中具有类似定位的全栈框架。

routing: ディレクトリベースのルーティングを使用してアプリケーションのルートを定義します。 (MPAとSPAの両方のルーティングモデルをサポートします。)

pages: アプリケーションページのレンダリング。アプリケーションの主な機能です。

layouts: ページ間で再利用される共通の共有ページレイアウトを定義します。

loaders: コンポーネントが使用するデータをサーバー上で取得します。

actions: コンポーネントがサーバーにアクションの実行を要求する方法を提供します。

validators: アクションとローダーをバリデーションする方法を提供します。

endpoints: RESTful API、GraphQL API、JSON、XML、またはリバースプロキシのデータエンドポイントを定義する方法。

middleware: 認証、セキュリティ、キャッシュ、リダイレクト、ロギングなどの横断的な処理を一元的に実行する方法。

server$: サーバー上でロジックを実行する簡単な方法。

cache: コンテンツのキャッシュを制御します。

env variables: 一般的にキーに使用される環境変数の読み取りをプラットフォームに依存しない方法で管理するためのAPI。

路由,页面,布局

在路由目录下创建要托管的页面文件。在这个例子中,/product/[id]将成为终端节点(类似于NextJS)。[id]将匹配任意的字符串部分。

スクリーンショット 2023-10-08 16.57.20.png
    • layouts.tsxは共通のレイアウトを定義します。なお、共通レイアウトはnestすることも可能です。

 

    product/[id]/index.tsxはホスティングするページの実装を書きます。

此外,还可以通过使用[…catchall]来定义无法匹配任何路由的404页面。

src/
└── routes/
    ├── contact/
    │   └── index.mdx         # https://example.com/contact
    ├── about/
    │   └── index.md          # https://example.com/about
    ├── docs/
    │   └── [id]/
    │       └── index.ts      # https://example.com/docs/1234
    │                         # https://example.com/docs/anything
    ├── [...catchall]/
    │   └── index.tsx         # https://example.com/anything/else/that/didnt/match
    │
    └── layout.tsx            # This layout is used for all pages

Page的定义是显示由component$定义的组件。

    • onRequest, onGet, onPost, onPut, onDelete: それぞれのリクエストメソッドに応じて最初に呼ばれる関数でミドルウェアです。例えば、キャッシュコントロールや認証に使えます。不要時は省略可能です。

routeLoader$: 外部APIコールなどでデータを取得することができます。component$やheadで呼び出しします。不要時は省略可能です。

routeAction$: Formからの送信時にサーバーサイドでハンドリングすることができるhookを作成できます。validateに関してはzod validatorとほぼ同じzod$を使います。もしくは代わりにvalidator$では定義することができます。

server$: Form以外からのcomponent$からの通信処理先を定義することができます。通信はRPC(Remote Procedure Call)により実装されています。

component$: ページに表示する要素を返却します。Slotは子要素となります(ReactJSのchildrenに近い)。layout.tsxで使う際に埋め込む中のコンポーネントとなります。ページ内遷移はLinkもしくはuseNavigateで遷移することができます。
head:ページのheadタグ部分に相当する部分を返却します。ページ毎に動的なメタタグを埋め込みたいケースもよくあるのでその場合はrouteLoader$でデータを受け渡しします。

import { component$, useSignal, Slot } from '@builder.io/qwik';
import { Link, useNavigate, routeLoader$, routeAction$, zod$, z, Form } from "@builder.io/qwik-city";
import type { DocumentHead, RequestHandler } from '@builder.io/qwik-city';
import { PrismaClient } from '@prisma/client'

export const onGet: RequestHandler = async ({ cacheControl }) => {
  // Control caching for this request for best performance and to reduce hosting costs:
  // https://qwik.builder.io/docs/caching/
  cacheControl({
    // Always serve a cached response by default, up to a week stale
    staleWhileRevalidate: 60 * 60 * 24 * 7,
    // Max once every 5 seconds, revalidate on the server to get a fresh version of this page
    maxAge: 5,
  });
};

export const useProductDetails = routeLoader$(async (requestEvent) => {
  // This code runs only on the server, after every navigation
  const res = await fetch(`https://.../products/${requestEvent.params.productId}`);
  const product = await res.json();
  return product as Product;
});

export const useAddUser = routeAction$(
  async (data, requestEvent) => {
    // This will only run on the server when the user submits the form (or when the action is called programmatically)
    const prisma = new PrismaClient();
    const user = await prisma.user.create({
     data,
    });
    return {
      success: true,
      user,
    };
  },
  // Zod schema is used to validate that the FormData includes "firstName" and "lastName"
  zod$({
    firstName: z.string(),
    lastName: z.string(),
  })
);

// By wrapping a function with `server$()` we mark it to always
// execute on the server. This is a form of RPC mechanism.
const serverGreeter = server$((name: string) => {
  const greeting = `Hello ${name}`;
  console.log('Prints in the server', greeting);
  return greeting;
});
 
export default component$(() => {
  const name = useSignal('');
  const nav = useNavigate();
  const action = useAddUser();
  // In order to access the `routeLoader$` data within a Qwik Component, you need to call the hook.
  const signal = useProductDetails(); // Readonly<Signal<Product>>

  return <>
   <Form action={action}>
     <input name="firstName" />
     <input name="lastName" />
     {action.value?.failed && <p>{action.value.fieldErrors?.firstName}</p>}
     {action.value?.failed && <p>{action.value.fieldErrors?.lastName}</p>}
     <button type="submit">Add user</button>
   </Form>
   {action.value?.success && <p>User added successfully</p>}
   <p>Product name: {signal.value.product.name}</p>
   <Link reload>Refresh (better accessibility)</Link>
   <button onClick$={() => nav()}>Refresh</button>
   <section>
      <label>First name: <input bind:value={name} /></labe
      <button
        onClick$={async () => {
          const greeting = await serverGreeter(name.value);
          alert(greeting);
        }}
      >
        greet
      </button>
    </section>
   <Slot />
  </>;
});


export const useJoke = routeLoader$(async (requestEvent) => {
  // Fetch a joke from a public API
  const jokeId = requestEvent.params.jokeId;
  const response = await fetch(`https://api.chucknorris.io/jokes/${jokeId}`);
  const joke = await response.json();
  return joke;
});
 
// Now we can export a function that returns a DocumentHead object
export const head: DocumentHead = ({resolveValue, params}) => {
  const joke = resolveValue(useJoke);
  return {
    title: `Joke "${joke.title}"`,
    meta: [
      {
        name: 'description',
        content: joke.text,
      },
      {
        name: 'id',
        content: params.jokeId,
      },
    ],
  };
};

环境变量

由于Qwik是使用vite构建的,因此可以通过将环境变量添加到vite的内置.env文件中来在Qwik上进行引用。

无论在何处,可以引用PUBLIC_前缀的环境变量。(请注意不要注册访问令牌等不应公开的内容)

PUBLIC_API_URL=https://api.example.com

假设服务器URL是通过环境变量进行设置的,您可以按照以下方式进行引用。

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  // `({}).PUBLIC_*` variables can be read anywhere, including browser
  return <div>PUBLIC_API_URL: {import.meta.env.PUBLIC_API_URL}</div>
})

如果在服务器端引用,需要定义非PUBLIC_的环境变量。

DB_PRIVATE_KEY=123456789

通过 middleware、routeLoader$、routeAction$、server$ 等可参考如下。

import {
  routeLoader$,
  routeAction$,
  server$,
  type RequestEvent,
} from '@builder.io/qwik-city';
 
export const onGet = (requestEvent: RequestEvent) => {
  console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
};
 
export const onPost = (requestEvent: RequestEvent) => {
  console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
};
 
export const useAction = routeAction$(async (_, requestEvent) => {
  console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
});
 
export const useLoader = routeLoader$(async (requestEvent) => {
  console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
});
 
export const serverFunction = server$(function () {
  // `this` is the `RequestEvent` object
  console.log(this.env.get('DB_PRIVATE_KEY'));
});

重写线路

有时候我们希望给相同路径设置不同名称。比如,当我们希望从同一个页面组件渲染/docs和/documents两个路径时,或者希望在/it/products的情况下将其重定向到/it/prodotti等。

如果想要进行反向代理,请在vite.config.ts中添加rewriteRoutes的定义。

import { defineConfig } from 'vite';
import { qwikCity } from '@builder.io/qwik-city/vite';
 
export default defineConfig(async () => {
  return {
    plugins: [
      qwikCity({
        rewriteRoutes: [
            {
              paths: {
                  'docs': 'documentation'
              },
            },
            {
              prefix: 'it',
              paths: {
                'docs': 'documentazione',
                'getting-started': 'per-iniziare',
                'products': 'prodotti',
              },
            },
          ],
      }),
    ],
  };
});

使用React和MUI进行引入

快速反应 ⚛️

在package.json文件中添加与React相关的库。

{
  "dependencies": {
    "@builder.io/qwik-react": "0.5.0",
    "@types/react": "18.0.28",
    "@types/react-dom": "18.0.11",
    "react": "18.2.0",
    "react-dom": "18.2.0",
  }
}

在 vite.config.ts 中添加插件。

import { qwikReact } from '@builder.io/qwik-react/vite';
 
export default defineConfig(() => {
   return {
     plugins: [
       // The important part
       qwikReact()
     ],
   };
});

在使用React时,需要在文件的开头加上/** @jsxImportSource react */这个魔术注释。
qwikify$很重要,它在鼠标悬停时执行React的hydrate,并使onClick事件处理生效。

/** @jsxImportSource react */
import React from 'react';
import { qwikify$ } from '@builder.io/qwik-react';
import Button from '@mui/material/Button';

// Create React component standard way
function Greetings() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>Hello from React</p>
      <Button variant="contained" onClick={() => setCount(count + 1)}>Count {count}</Button>
    </div>
  );
}
 
// Specify eagerness to hydrate component on hover event.
export const QGreetings = qwikify$(Greetings, { eagerness: 'hover' });

限制Qwik化的React组件。

每个被Qwik化的React组件实例都成为一个独立的React应用程序。
它们完全被隔离了。

export const MUISlider = qwikify$(Slider);
 
<MUISlider></MUISlider>
<MUISlider></MUISlider>
    • 各MUISliderは完全に分離されたReactアプリケーションであり、独自の状態、ライフサイクルなどを持ちます。

stateは独立です。
styleは重複します。
Qwikのcontextは使えません。
それぞれのインスタンスごとにhydrateされます。

广告
将在 10 秒后关闭
bannerAds