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
vue 的 transition基本使用

> vue提供内置组件transition进行状态变化的过渡 ## **transition基本使用** ## **基本使用** ```text  <div>    <p>Acom</p>    <button @click="isShow = !isShow">change</button>    <transition name="fade"><div v-if="isShow">这是个div</div></transition>  </div> ``` 当transition组件中的元素被插入或移除时: 1. Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 css过渡或动画会在适当的时机被添加和移除。 2. 如果有作为监听器的 [JavaScript 钩子](https://staging-cn.vuejs.org/guide/built-ins/transition.html#javascript-hooks),这些钩子函数会在适当时机被调用。 3. 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。 ### **过渡类(vue3)** ```text .fade-enter-from {  opacity: 0; } .fade-enter-to {  opacity: 1; } .fade-enter-active, .fade-leave-active {  transition: opacity 0.5s; } .fade-leave-from {  opacity: 1; } .fade-leave-to {  opacity: 0; } ``` ### **过渡类(vue2)** ```text .fade-enter {  opacity: 0; } .fade-enter-to {  opacity: 1; } .fade-enter-active, .fade-leave-active {  transition: opacity 0.5s; } .fade-leave {  opacity: 1; } .fade-leave-to {  opacity: 0; } ``` > 也就是说,vue3的改动就是说把元素插入之前和元素离开之前的类改了名字,我说怎么写的时候进入时没动画,百度害*人~

avatar
出自
a1ex0012
基于Vue3的 ssr 自实现

# **基于vue3实现ssr应用** - 大致流程 ## **搭建Node Server** 1. 依赖 -> koa webpack webpack-cli webpack-node-externals 2. 打包配置 排除包 ```text const path = require("path"); const external = require("webpack-node-externals"); module.exports = {  target: "node", // 去除 path fs ...  mode: "development",  entry: "./src/server/index.js",  output: {    filename: "server_bundle.js",    path: path.resolve(__dirname, "../build/server"), },  externals: [external()], //排除node_module的包 };    "start": "nodemon ./src/server/index.js",    "build:server": "webpack --config ./config/wp.config.js",    "server": "node ./build/server/server_bundle.js" ``` ## **vue3 + SSR 搭建** 1. 依赖项 -> vue vue-loader babel-loader @babel/preset-env 2. 挂载根组件, 导出创建SSRAPP的方法 ```text import { createSSRApp } from "vue"; import App from "./App.vue"; // 防止跨请求状态污染 // 返回app让页面每次创建不同的app export default function createApp() {  return createSSRApp(App); } ​ ``` 3. 访问/ssr时,处理响应,拿到返回的字符串并渲染 ```text  // 拿到vue的str  const appStringHTML = await renderToString(ssrApp); ​  ctx.body = `  <!DOCTYPE html> <html lang="en">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>  <div id="app">${appStringHTML}</div>  </body> </html>  `; ``` 4. 对页面进行激活(水合Hydration) 1. 创建app -> createApp() 2. 打包配置 ```text const path = require("path"); const { VueLoaderPlugin } = require("vue-loader/dist"); module.exports = {  target: "web",  mode: "development",  entry: "./src/client/index.js",  output: {    filename: "client_bundle.js",    path: path.resolve(__dirname, "../build/client"), },  module: {    rules: [     {        test: /\.js$/,        loader: "babel-loader",        options: {          presets: ["@babel/preset-env"],       },     },     {        test: /\.vue$/,        loader: "vue-loader",     },   ], },  plugins: [new VueLoaderPlugin()],    // 不用externals需要的依赖文件, 因为交互需要使用 }; ​ ``` 3. 打包后的资源通过静态资源共享挂载到script上 ```text    ctx.body = `  <!DOCTYPE html> <html lang="en">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Document</title>  </head>  <body>  <div id="app">${appStringHTML}</div>  </body>  <script>${jsStr}</script> </html>  `; ``` 4. 报警告flags **VUE_OPTIONS_API**, **VUE_PROD_DEVTOOLS** are not explicitly defined. You are running the esm-bundler build of Vue, which expects these compile-time feature flags to be globally injected via the bundler config in order to get better tree-shaking in the production bundle. 1. 不使用optionsapi...,让它treeshaking ```text    new DefinePlugin({      __VUE_OPTIONS_API__: false,      __VUE_PROD_DEVTOOLS__: false,   }), ], ``` ## **跨请求状态污染问题** - 每次访问重新创建一个ssrAPP ## **效果**

avatar
出自
a1ex0012
nextjs 接入 sentry

## sentry 接入

avatar
出自
a1ex0012
iframe 通讯

> iframe 没有 click 事件,所以要监听内部点击,可以从内部触发,或给父盒子添加 click 监听 ## 基本实现 ```javascript import { isSylvia } from "@/config/live2d"; import { IModalConfig } from "@/services/user"; import { CircularProgress } from "@nextui-org/progress"; import clsx from "clsx"; import { sample } from "lodash"; import { useEffect, useImperativeHandle, useState } from "react"; import ModelPreviewCover from "../ModelPreviewCover"; interface PixiLive2DRenderProps { currModel: IModalConfig; isCubismCoreLoaded: boolean; intimacy: number; } export default function ModalRender({ currModel, isCubismCoreLoaded, intimacy, ref, iframeWindow, setIframeWindow, }: PixiLive2DRenderProps) { const [loadingState, setLoadingState] = useState(true); const randomMotion = () => { const ins = iframeWindow?.ins; if (!ins?.motion) return; const actions = intimacy > 80 ? [0, 1, 2] : [0, 2]; ins?.motion("Tap", sample(actions)); }; useImperativeHandle(ref, () => ({ randomMotion, })); if (!currModel?.uuid) return; return ( <div className="flex items-center justify-center relative"> {/* {isCubismCoreLoaded && window?.location?.host === "wingman.bestie.icu" && ( <FpsDisplay app={modelApp} /> )} */} <div className={clsx( "w-[600px] h-[1000px] object-cover", !loadingState ? "opacity-0" : "opacity-100", )} onClick={(e) => { randomMotion?.(); }} > <iframe src={`/bestie/${currModel?.uuid}`} frameBorder="0" onLoad={(e) => { const cw = e?.target?.contentWindow; if (cw) { setIframeWindow(cw); } }} title="Live2D" className={clsx( "w-[600px] h-[1000px] object-cover pointer-events-none", !loadingState ? "opacity-0" : "opacity-100", )} /> </div> {!loadingState && ( <div className="flex justify-center items-center absolute inset-0 bg-center left-1/2 -translate-x-[45%]" style={{ width: 600, height: 1000, }} > <ModelPreviewCover width={600} height={isSylvia(currModel?.uuid) ? 1200 : 1000} modelUuid={currModel?.uuid} className={clsx( isSylvia(currModel?.uuid) ? "w-full h-full !object-cover !-translate-x-8" : "", )} /> <CircularProgress aria-label="Loading..." color="warning" size="lg" className="absolute left-1/2 top-1/2 -translate-y-1/2 -translate-x-1/2" /> </div> )} </div> ); } ```

avatar
出自
a1ex0012
命令合集

1. docker 1. **清除所有未使用或悬空镜像、容器、卷和网络** ```javascript docker system prune -a ``` 2. git 1. git 同步远程分支 ```bash git remote prune origin ```

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