THE
BLOG
nextjs 加载 iconify 如何 SSR 化

> 由于开发项目页面多且复杂,所以选择使用到 iconify 图标库。使用到的是 nextjs,由于 iconify 在ssr中的一些缺陷,只能通过 json 去将图标 ssr 化,导致搭配 vscode 插件 Iconify IntelliSense 预览图标和书写体验不佳,所以进行封装改善开发体验。 ## 使用: ```typescript <AxIcon icon="line-md:close-circle-twotone" className="mr-2" size={48} /> ``` ## 完整代码 ```typescript "use client"; import { cn } from "@/lib/utils"; import { icons as entypoIcons, IconifyJSON } from "@iconify-json/entypo"; import { icons as lineMDIcons } from "@iconify-json/line-md"; import { icons as phIcons } from "@iconify-json/ph"; import { icons as tablerIcons } from "@iconify-json/tabler"; import { replaceIDs } from "@iconify/react"; import { getIconData, iconToHTML, iconToSVG } from "@iconify/utils"; interface AxIconProps { icon: string; size?: string | number; className?: string; } export default function AxIcon({ icon, size = 16, className }: AxIconProps) { const [brand, iconName] = icon.split(":"); const brandIcons: Record<string, IconifyJSON> = { tabler: tablerIcons, entypo: entypoIcons, ph: phIcons, "line-md": lineMDIcons }; if (!brandIcons[brand]) return; const iconData = getIconData(brandIcons[brand], iconName); if (!iconData) return; const renderData = iconToSVG(iconData, { height: size, width: size }); const svgStr = iconToHTML(replaceIDs(renderData.body), { ...renderData.attributes, class: cn("!w-auto !h-auto", className) }); return <div dangerouslySetInnerHTML={{ __html: svgStr }} />; } ```

avatar
出自
a1ex0012
nestjs
JavaScript
后端
nestjs 链接 notion API

测试

avatar
出自
a1ex0012
高级轮播效果实现

[https://spring-slider.uiinitiative.com/](https://spring-slider.uiinitiative.com/)

avatar
出自
a1ex0012
nextjs 加载 iconify 如何 SSR 化

> 由于开发项目页面多且复杂,所以选择使用到 iconify 图标库。使用到的是 nextjs,由于 iconify 在ssr中的一些缺陷,只能通过 json 去将图标 ssr 化,导致搭配 vscode 插件 Iconify IntelliSense 预览图标和书写体验不佳,所以进行封装改善开发体验。 ## 使用: ```typescript <AxIcon icon="line-md:close-circle-twotone" className="mr-2" size={48} /> ``` ## 完整代码 ```typescript "use client"; import { cn } from "@/lib/utils"; import { icons as entypoIcons, IconifyJSON } from "@iconify-json/entypo"; import { icons as lineMDIcons } from "@iconify-json/line-md"; import { icons as phIcons } from "@iconify-json/ph"; import { icons as tablerIcons } from "@iconify-json/tabler"; import { replaceIDs } from "@iconify/react"; import { getIconData, iconToHTML, iconToSVG } from "@iconify/utils"; interface AxIconProps { icon: string; size?: string | number; className?: string; } export default function AxIcon({ icon, size = 16, className }: AxIconProps) { const [brand, iconName] = icon.split(":"); const brandIcons: Record<string, IconifyJSON> = { tabler: tablerIcons, entypo: entypoIcons, ph: phIcons, "line-md": lineMDIcons }; if (!brandIcons[brand]) return; const iconData = getIconData(brandIcons[brand], iconName); if (!iconData) return; const renderData = iconToSVG(iconData, { height: size, width: size }); const svgStr = iconToHTML(replaceIDs(renderData.body), { ...renderData.attributes, class: cn("!w-auto !h-auto", className) }); return <div dangerouslySetInnerHTML={{ __html: svgStr }} />; } ```

avatar
出自
a1ex0012
nestjs
JavaScript
后端
nestjs 链接 notion API

测试

avatar
出自
a1ex0012
高级轮播效果实现

[https://spring-slider.uiinitiative.com/](https://spring-slider.uiinitiative.com/)

avatar
出自
a1ex0012
nextjs
typescript
react
知识
shadcn/ui 动态 pagination

最近由于在 nextui 和 shadcn/ui 做切换,遇到些问题,由于 shadcn/ui 灵活性高,导致没有 pagination 动态配置功能,所以基于以下链接封装了pagination组件 AwsomePagination。 [bookmark](https://medium.com/@enayetflweb/implementing-pagination-in-shadcn-ui-a-complete-guide-b7539e34908a) ## 首先 以上链接的 `buildLink` 是用的query的自适应,而对于的的需求都是用 params的形式去设计,所以调整为新的函数 ```typescript const buildLink = useCallback((newPage: number) => { return `${parentPath}/${newPage}`; }, []); ``` 我的component基本运用于服务端组件,所以不需要任何依赖,_parentPath则为 分页页面的父路由。_ ## 然后 renderPageNumbers也做了一些调整 ```typescript const renderPageNumbers = useCallback(() => { const items: ReactNode[] = []; if (totalPageCount <= maxVisiblePages) { for (let i = 1; i <= totalPageCount; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } } else { items.push( <PaginationItem key={1}> <PaginationLink href={buildLink(1)} isActive={page === 1}> 1 </PaginationLink> </PaginationItem> ); if (page > 3) { items.push( <PaginationItem key="ellipsis-start"> <PaginationLink href={buildLink(page - 2)}> <NavigationDots type="left" /> </PaginationLink> </PaginationItem> ); } const start = Math.max(2, page - 1); const end = Math.min(totalPageCount - 1, page + 1); for (let i = start; i <= end; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } if (page < totalPageCount - 2) { items.push( <PaginationItem key="ellipsis-end"> <PaginationLink href={buildLink(page + 2)}> <NavigationDots type="right" /> </PaginationLink> </PaginationItem> ); } items.push( <PaginationItem key={totalPageCount}> <PaginationLink href={buildLink(totalPageCount)} isActive={page === totalPageCount}> {totalPageCount} </PaginationLink> </PaginationItem> ); } return items; }, []); ``` 这里原文章用 pagination的 dots组件展示样式,并没有jump功能,这里实现了 hover 变 jump 功能。指定 buildLink 去 实现对应跳转 ```typescript export function NavigationDots({ type }: { type: "left" | "right" }) { const wrapperRef = useRef<HTMLDivElement>(null); const isHovered = useHover(wrapperRef); const hoverDom = useMemo(() => { if (type === "left") { return <Icon icon={"tabler:arrow-badge-left-filled"} />; } return <Icon icon={"tabler:arrow-badge-right-filled"} />; }, [type]); return ( <div className="cursor-pointer" ref={wrapperRef}> {isHovered ? hoverDom : <Icon icon={"tabler:dots"} />} </div> ); } ``` 这里用到的是 iconify 和 ahooks 去实现 ,hover 图标切换,点击 jump 对应页面 也就是这样用: ```typescript <PaginationItem key="ellipsis-end"> <PaginationLink href={buildLink(page + 2)}> <NavigationDots type="right" /> </PaginationLink> </PaginationItem> ``` ## 最后 使用: ```typescript <AwsomePagination parentPath="/home" pageSize={6} page={1} totalCount={total} /> ``` 源码: ```typescript "use client"; import { Icon } from "@iconify/react/dist/iconify.js"; import { useHover } from "ahooks"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { type ReactNode, useCallback, useMemo, useRef } from "react"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "../ui/pagination"; export interface AwsomePaginationProps { totalCount: number; pageSize: number; page: number; maxVisiblePages?: number; parentPath: string; } export function NavigationDots({ type }: { type: "left" | "right" }) { const wrapperRef = useRef<HTMLDivElement>(null); const isHovered = useHover(wrapperRef); const hoverDom = useMemo(() => { if (type === "left") { return <Icon icon={"tabler:arrow-badge-left-filled"} />; } return <Icon icon={"tabler:arrow-badge-right-filled"} />; }, [type]); return ( <div className="cursor-pointer" ref={wrapperRef}> {isHovered ? hoverDom : <Icon icon={"tabler:dots"} />} </div> ); } export function AwsomePagination({ pageSize, totalCount, page, maxVisiblePages = 5, parentPath }: AwsomePaginationProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const totalPageCount = Math.ceil(totalCount / pageSize); const buildLink = useCallback((newPage: number) => { return `${parentPath}/${newPage}`; }, []); const renderPageNumbers = useCallback(() => { const items: ReactNode[] = []; if (totalPageCount <= maxVisiblePages) { for (let i = 1; i <= totalPageCount; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } } else { items.push( <PaginationItem key={1}> <PaginationLink href={buildLink(1)} isActive={page === 1}> 1 </PaginationLink> </PaginationItem> ); if (page > 3) { items.push( <PaginationItem key="ellipsis-start"> <PaginationLink href={buildLink(page - 2)}> <NavigationDots type="left" /> </PaginationLink> </PaginationItem> ); } const start = Math.max(2, page - 1); const end = Math.min(totalPageCount - 1, page + 1); for (let i = start; i <= end; i++) { items.push( <PaginationItem key={i}> <PaginationLink href={buildLink(i)} isActive={page === i}> {i} </PaginationLink> </PaginationItem> ); } if (page < totalPageCount - 2) { items.push( <PaginationItem key="ellipsis-end"> <PaginationLink href={buildLink(page + 2)}> <NavigationDots type="right" /> </PaginationLink> </PaginationItem> ); } items.push( <PaginationItem key={totalPageCount}> <PaginationLink href={buildLink(totalPageCount)} isActive={page === totalPageCount}> {totalPageCount} </PaginationLink> </PaginationItem> ); } return items; }, []); return ( <div className="flex flex-col md:flex-row h-9 overflow-y-hidden items-center gap-3 w-full"> <Pagination> <PaginationContent className="max-sm:gap-0"> <PaginationItem> <PaginationPrevious href={buildLink(Math.max(page - 1, 1))} aria-disabled={page === 1} tabIndex={page === 1 ? -1 : undefined} className={page === 1 ? "pointer-events-none opacity-50" : undefined} /> </PaginationItem> {renderPageNumbers()} <PaginationItem> <PaginationNext href={buildLink(Math.min(page + 1, totalPageCount))} aria-disabled={page === totalPageCount} tabIndex={page === totalPageCount ? -1 : undefined} className={page === totalPageCount ? "pointer-events-none opacity-50" : undefined} /> </PaginationItem> </PaginationContent> </Pagination> </div> ); } ``` css ```css .body{ color: #F00; } ``` [bookmark](https://www.a1ex.tech/zh/post/15ed250f8d4d806cb37ed4af14ff0b57)

avatar
出自
a1ex0012
vitest
playwright
单元测试
nextjs 单元测试 编写

> 最近在写国际化适配阿拉伯语时,发现在跑翻译的时候,有一些json解析翻译出来有些引号被替换,导致JSON.parse 报错,其中几个页面会直接error但是并没有发现,所以打算写一个测试,测试所有页面是否加载正常。 ### 组件单元测试(vitest) 1. 准备工作 1. vitest.config.mts ```typescript import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [tsconfigPaths(), react()], test: { globals: true, environment: "jsdom", setupFiles: ["./vitest.setup.ts"], include: ["**/*.test.{js,jsx,ts,tsx}"], exclude: ["**/node_modules/**", "**/dist/**"] } }); ``` b. vitest.setup.ts ```typescript import { cleanup } from "@testing-library/react"; import { afterEach, vi } from "vitest"; afterEach(() => { cleanup(); vi.clearAllMocks(); }); ``` 2. 基本使用 1. 判断 渲染组件后关键DOM是否存在 ```typescript import Footer from "@/components/Footer"; import { render, screen } from "@testing-library/react"; import { expect, test } from "vitest"; test("test", () => { render(<Footer />); expect(screen.getByText("[赣ICP备2022002397号]")).toBeDefined(); }); ``` ### e2e测试(playwright) 1. 准备工作 1. playwright.config.ts ```typescript import { defineConfig, devices } from "@playwright/test"; import path from "path"; // Use process.env.PORT by default and fallback to port 3000 const PORT = process.env.PORT || 3000; // Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port const baseURL = `http://localhost:${PORT}`; // Reference: https://playwright.dev/docs/test-configuration export default defineConfig({ // Timeout per test timeout: 30 * 1000, // Test directory testDir: path.join(__dirname, "e2e"), // If a test fails, retry it additional 2 times retries: 0, // Artifacts folder where screenshots, videos, and traces are stored. outputDir: "test-results/", // Run your local dev server before starting the tests: // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests webServer: { command: "pnpm run dev", url: baseURL, timeout: 120 * 1000, reuseExistingServer: !process.env.CI }, use: { // Use baseURL so to make navigations relative. // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url baseURL, // Retry a test if its failing with enabled tracing. This allows you to analyze the DOM, console logs, network traffic etc. // More information: https://playwright.dev/docs/trace-viewer trace: "retry-with-trace" // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context // contextOptions: { // ignoreHTTPSErrors: true, // }, }, projects: [ { name: "Desktop Chrome", use: { ...devices["Desktop Chrome"] } } // { // name: 'Desktop Firefox', // use: { // ...devices['Desktop Firefox'], // }, // }, // { // name: 'Desktop Safari', // use: { // ...devices['Desktop Safari'], // }, // }, // Test against mobile viewports. // { // name: "Mobile Chrome", // use: { // ...devices["Pixel 5"] // } // }, // { // name: "Mobile Safari", // use: devices["iPhone 12"] // } ] }); ``` 1. 基本使用 1. 加载所有页面是否正常 svg 过渡变换 并且包含预览 codebox ```typescript import { RouteInfo, scanRoutes } from "@/scripts/scanRoutes"; import { expect, test } from "@playwright/test"; import { filter } from "lodash"; const routes = scanRoutes(); const isStaticRoute = filter(routes, (r: RouteInfo) => !r.isDynamic); const generatePageTestByRoute = (route: RouteInfo) => { test.describe(`<${route.path}> 加载测试`, () => { test(`<${route.path}> 加载成功`, async ({ page }) => { // 检查页面响应状态码 const response = await page.goto(route.path); expect(response?.status()).toBe(200); }); if (route.path.includes("svgTransform")) { test(`<${route.path}> svg 过渡变换 并且包含预览 codebox`, async ({ page }) => { const response = await page.goto(route.path); expect(response?.status()).toBe(200); const htmlContent = await page.content(); const bol = htmlContent.includes("IconTransformProps"); expect(bol).toBe(true); }); } }); }; isStaticRoute.forEach(generatePageTestByRoute); ```

avatar
出自
a1ex0012
JavaScript
react
前端
微信支付实现(nuxt)

### **微信支付实现(nuxt)** 1. 获取token ```text async asyncData({ $axios, query, n2token }) {    // 1. 从query中拿到code    const { code } = query    // const code = '081ozf300lj2aR1rCI000lRQOc3ozf33'    const useRes = await toAuthentication({      code,      state: 'sharp'   })    let token    if (useRes.data.success) {      token = useRes.data.data.token   } else {      token = ''   }    // 4. 通过token拿到用户套餐    const configRes = await getVipConfig({})    // 支付时用product_id 调用 makeOrder 拿到支付需要的参数并调起微信支付    return {      token,      payList: configRes.data.data,      currType: configRes.data.data[0]   } }, ``` 1. 点击支付 ```text async makeOrder() {  // 支付时用product_id 调用 makeOrder 拿到支付需要的参数并调起微信支付  const res = await makeMiniH5Order(   {      product_id: this.currType.params.productId,      trade_type: this.isIOS ? 'ios' : 'android'   },   {      headers: {        Authorization: this.token     }   } )  const openData = res.data.data.open_data  this.wxJSPay(   { ...openData, appId: res.data.data.weichatpay.appid },   () => {      this.$toast('支付成功, 快去小程序体验会员功能吧!~')   },   () => {      this.$toast('支付失败')   } ) }, ​ ​ // 微信环境  wxJSPay(configObj, successCb, failCb) {    function onBridgeReady() {      WeixinJSBridge.invoke(        'getBrandWCPayRequest',       {          ...configObj       },        function(res) {          if (res.err_msg == 'get_brand_wcpay_request:ok') {            // 使用以上方式判断前端返回,微信团队郑重提示:            // res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。            successCb && successCb()         } else {            failCb && failCb()         }       }     )   }    if (typeof WeixinJSBridge === 'undefined') {      if (document.addEventListener) {        document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)     } else if (document.attachEvent) {        document.attachEvent('WeixinJSBridgeReady', onBridgeReady)        document.attachEvent('onWeixinJSBridgeReady', onBridgeReady)     }   } else {      onBridgeReady()   } } ```

avatar
出自
a1ex0012