距离上次看qwik已经过去一年多的时间了,当时qwik才刚出没多久,那时候还是v0.1还是0.2的版本,还有很多bug就没兴趣研究了。如今过去一年多了,qwik版本已经到达了正式版,api也相对固定下来了,所有又有兴趣研究一下。
至于qwik是什么、对比别的框架有什么优势这里我就不重复提了,在掘金站内一搜一大堆。下面直接根据官方教程开始入门。这里推荐有一定的React基础(懂基本的jsx语法)、Vue3响应式基础(会用ref、watch等)、TS基础的小伙伴观看。
官方入门原文:https://qwik.builder.io/docs/getting-started/
前置条件 要在本地开始使用Qwik,你需要以下内容:
国内网络环境的需要先设置sharp国内代理,不然可能安装依赖失败:
1 2 npm config set sharp_binary_host "https://npmmirror.com/mirrors/sharp" npm config set sharp_libvips_binary_host "https://npmmirror.com/mirrors/sharp-libvips"
通过cli创建一个app 在你打算新建项目的路径,打开shell或者cmd,执行下面其中一个命令(按照你平时习惯选一个):
1 2 3 4 npm create qwik@latest pnpm create qwik@latest yarn create qwik bun create qwik@latest
然后就会通过交互式的对话来创建项目,这里先全面选默认选项,一直下一步直到常见项目完成,会提示你cd到qwik-app文件夹,安装依赖,比如你用了pnpm创建,那么会提示你:
1 2 3 cd qwik-app pnpm install pnpm start
执行完start之后,会启动本地开发模式,这时候也会帮你打开网页,这样整个项目就创建好并启动了。
简单的HelloWorld应用 这里先简单的在页面上显示HelloWorld,然后再从一言网址拉取一些名言或者网络流行句子进行展示。
创建一个路由 这一步要基于Qwik的元框架Qwik-city,他能根据项目的目录提供路由。
在项目的src/routes
目录下创建一个新的文件夹:sentence
,并且在里面创建一个新文件 index.tsx
.
每个路由下的index.tsx
都需要包含:export default component$(...)
,所以复制下面代码到上面新建的文件
1 2 3 4 5 6 import { component$ } from '@builder.io/qwik' ; export default component$ (() => { return <section class ="section bright" > Hello World!</section > ; });
在浏览器打开http://127.0.0.1:5173/sentence/
你的sentence路由组件现在被一个默认的布局包裹住,有关什么是布局以及如何使用布局的更多详细信息,请参阅布局
有关如何编写组件的更多细节,请参阅组件API部分 。
加载数据 我们使用一言的api,从一言拉取一些句子。我们通过 route loader 在服务器拉取数据,然后在浏览器进行渲染。
将上面的index.tsx
改成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { component$ } from '@builder.io/qwik' ;import { routeLoader$ } from '@builder.io/qwik-city' ; export const useHitokoto = routeLoader$ (async () => { const response = await fetch ('https://v1.hitokoto.cn/' , { headers : { Accept : 'application/json' }, }); return (await response.json ()) as { id : string ; hitokoto : number ; from : string ; }; }); export default component$ (() => { const sentenceSignal = useHitokoto (); return ( <section class ="section bright" > <p > {sentenceSignal.value.hitokoto} --{sentenceSignal.value.from}</p > </section > ); });
保存代码之后再去浏览器查看:http://127.0.0.1:5173/sentence/
代码解析:
通过routeLoader$
调用的函数,都会在组件渲染前调用,然后渲染成html传到浏览器进行加载渲染。
routeLoader$
会返回一个use钩子(use-hook),比如上面可以通过useHitokoto()
拿到服务器返回来的数据。
注意 :
routeLoader$
会在任何组件渲染前进行调用,也就是说,export default component$(...)
里面就算不写const sentenceSignal = useHitokoto();
,routeLoader$
里的函数也会被调用。
routeLoader$
可以根据返回类型进行推导,所以下面的sentenceSignal能得到正确的类型,这也是为什么为什么要在return进行ts的as
断言。
提交数据到服务器 在前面,我们通过routeLoader$
从服务器拉取数据,下面我们通过routeAction$
从浏览器将数据发送到服务器。
注意: routeAction$
是向服务器发送数据的首选方式,因为它使用浏览器原生表单API,即使JavaScript被禁用也能正常工作。
下面我们定义一个action,并且在组件用到这个action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { component$ } from '@builder.io/qwik' ;import { routeLoader$, Form , routeAction$ } from '@builder.io/qwik-city' ; export const useHitokoto = routeLoader$ (async () => { const response = await fetch ('https://v1.hitokoto.cn/' , { headers : { Accept : 'application/json' }, }); return (await response.json ()) as { id : string ; hitokoto : number ; from : string ; }; }); export const useSentenceVoteAction = routeAction$ ((props ) => { console .log ('投票' , props) }) export default component$ (() => { const sentenceSignal = useHitokoto (); const favoriteSentenceAction = useSentenceVoteAction (); return ( <section class ="section bright" > <p > {sentenceSignal.value.hitokoto} ——{sentenceSignal.value.from}</p > <Form action ={favoriteSentenceAction} > <input type ="hidden" name ="id" value ={sentenceSignal.value.id} /> <input type ="hidden" name ="sentence" value ={sentenceSignal.value.hitokoto} /> <button name ="vote" value ="up" > 👍</button > <button name ="vote" value ="down" > 👎</button > </Form > </section > ); });
保存代码,页面多出两个按钮,随便点一个,再查看服务端有没有打印:
代码解析:
routeAction$
接收数据.
传递给routeAction$
的函数在发送表单时就会在服务器上调用。
routeAction$
返回一个use-hook, favoriteSentenceAction,你可以在组件中使用它来发送表单数据。
Form是一个方便的组件,它封装了浏览器的原生<form>
元素
修改状态 类似Vue3的ref,Qwik提供了一个hook:useSignal
,用来保存状态,并且提供响应式。下面来使用一下:
从 qwik
导入 useSignal
:import { component$, useSignal } from "@builder.io/qwik";
在组件定义里面定义这个signal:const isFavoriteSignal = useSignal(false);
在Form的关闭标签后面添加一个按钮,用于修改状态
最终代码变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { component$, useSignal } from '@builder.io/qwik' ;import { routeLoader$, Form , routeAction$ } from '@builder.io/qwik-city' ; export const useHitokoto = routeLoader$ (async () => { const response = await fetch ('https://v1.hitokoto.cn/' , { headers : { Accept : 'application/json' }, }); return (await response.json ()) as { id : string ; hitokoto : number ; from : string ; }; }); export const useSentenceVoteAction = routeAction$ ((props ) => { console .log ('投票' , props) }) export default component$ (() => { const sentenceSignal = useHitokoto (); const favoriteSentenceAction = useSentenceVoteAction (); const isFavoriteSignal = useSignal (false ); return ( <section class ="section bright" > <p > {sentenceSignal.value.hitokoto} ——{sentenceSignal.value.from}</p > <Form action ={favoriteSentenceAction} > <input type ="hidden" name ="id" value ={sentenceSignal.value.id} /> <input type ="hidden" name ="sentence" value ={sentenceSignal.value.hitokoto} /> <button name ="vote" value ="up" > 👍</button > <button name ="vote" value ="down" > 👎</button > </Form > <button onClick $={() => (isFavoriteSignal.value = !isFavoriteSignal.value)} > {isFavoriteSignal.value ? '❤️' : '🤍'} </button > </section > ); });
监听状态变化并调用服务端函数 在Qwik中,任务(task)是在状态发生变化时需要执行的工作(这类似于其他框架中的“effect”)。在本例中,我们使用任务来调用服务端上的代码。
从 qwik
导入 useTask$
: import { component$, useSignal, useTask$ } from "@builder.io/qwik";
创建一个task来监听isFavoriteSignal的状态变化:
1 2 3 useTask$ (({ track } ) => { track (() => isFavoriteSignal.value ); });
添加要在状态更改时执行的代码:
1 2 3 4 useTask$ (({ track } ) => { track (() => isFavoriteSignal.value ); console .log ('FAVORITE (isomorphic)' , isFavoriteSignal.value ); });
如果你希望在服务器上也进行执行某些代码,那么将这些封装在server$()中。
1 2 3 4 5 useTask$ (({ track } ) => { track (() => isFavoriteSignal.value ); console .log ('FAVORITE (isomorphic)' , isFavoriteSignal.value ); server$ (() => { console .log ('FAVORITE (server)' , isFavoriteSignal.value ); })(); });
最后代码变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { component$, useSignal, useTask$ } from '@builder.io/qwik' ;import { routeLoader$, Form , routeAction$, server$ } from '@builder.io/qwik-city' ; export const useHitokoto = routeLoader$ (async () => { const response = await fetch ('https://v1.hitokoto.cn/' , { headers : { Accept : 'application/json' }, }); return (await response.json ()) as { id : string ; hitokoto : number ; from : string ; }; }); export const useSentenceVoteAction = routeAction$ ((props ) => { console .log ('投票' , props) }) export default component$ (() => { const sentenceSignal = useHitokoto (); const favoriteSentenceAction = useSentenceVoteAction (); const isFavoriteSignal = useSignal (false ); useTask$ (({ track } ) => { track (() => isFavoriteSignal.value ); console .log ('FAVORITE (isomorphic)' , isFavoriteSignal.value ); server$ (() => { console .log ('FAVORITE (server)' , isFavoriteSignal.value ); })(); }); return ( <section class ="section bright" > <p > {sentenceSignal.value.hitokoto} ——{sentenceSignal.value.from}</p > <Form action ={favoriteSentenceAction} > <input type ="hidden" name ="id" value ={sentenceSignal.value.id} /> <input type ="hidden" name ="sentence" value ={sentenceSignal.value.hitokoto} /> <button name ="vote" value ="up" > 👍</button > <button name ="vote" value ="down" > 👎</button > </Form > <button onClick $={() => (isFavoriteSignal.value = !isFavoriteSignal.value)} > {isFavoriteSignal.value ? '❤️' : '🤍'} </button > </section > ); });
注意:
组件中的useTask$
会在服务端和客户端(浏览器)中执行一次。
当用户单击按钮时,浏览器会打印:FAVORITE (isomorphic) true
,服务端打印:FAVORITE (server) true
CSS样式 Qwik提供了一种将样式与组件关联并限定其范围的方法(类似Vue的scoped)。
创建一个css文件,src/routes/sentence/index.css
:
1 2 3 4 5 6 7 p { font-weight : bold; } form { float : right; }
导入样式:import styles from "./index.css?inline";
从qwik导入useStylesScoped$
: import { component$, useSignal, useStylesScoped$, useTask$ } from "@builder.io/qwik";
告诉组件加载样式:useStylesScoped$(styles);
最后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import { component$, useSignal, useTask$, useStylesScoped$ } from '@builder.io/qwik' ;import { routeLoader$, Form , routeAction$, server$ } from '@builder.io/qwik-city' ;import styles from './index.css?inline' export const useHitokoto = routeLoader$ (async () => { const response = await fetch ('https://v1.hitokoto.cn/' , { headers : { Accept : 'application/json' }, }); return (await response.json ()) as { id : string ; hitokoto : number ; from : string ; }; }); export const useSentenceVoteAction = routeAction$ ((props ) => { console .log ('投票' , props) }) export default component$ (() => { const sentenceSignal = useHitokoto (); const favoriteSentenceAction = useSentenceVoteAction (); const isFavoriteSignal = useSignal (false ); useTask$ (({ track } ) => { track (() => isFavoriteSignal.value ); console .log ('FAVORITE (isomorphic)' , isFavoriteSignal.value ); server$ (() => { console .log ('FAVORITE (server)' , isFavoriteSignal.value ); })(); }); useStylesScoped$ (styles) return ( <section class ="section bright" > <p > {sentenceSignal.value.hitokoto} ——{sentenceSignal.value.from}</p > <Form action ={favoriteSentenceAction} > <input type ="hidden" name ="id" value ={sentenceSignal.value.id} /> <input type ="hidden" name ="sentence" value ={sentenceSignal.value.hitokoto} /> <button name ="vote" value ="up" > 👍</button > <button name ="vote" value ="down" > 👎</button > </Form > <button onClick $={() => (isFavoriteSignal.value = !isFavoriteSignal.value)} > {isFavoriteSignal.value ? '❤️' : '🤍'} </button > </section > ); });
效果:
上面就是Qwik官方文档的入门教程,有兴趣赶紧去试试吧