diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b967fc610bab874542dd510b2db30e0b672765ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/openInula/node_modules +/openInula/dist +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +#IDE +.idea/ + +package-lock.json +yarn.lock +pnpm-lock.yaml + +_pagefind/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..af819872621a9e96615073d358bf9db16d94ee8c --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +# OpenInula 官网(Next.js 版) + +## 项目简介 + +OpenInula 是一个现代化的 JavaScript UI 框架,致力于提供极致性能和开发体验。本仓库为 OpenInula 新一代官网,基于 Next.js 13+(App Router)与 Tailwind CSS 构建,提供文档、教程、示例与 Playground 等站点能力。 + +--- + +## 核心特性 + +- **编译优先**:在编译期完成大部分工作,缩短运行时开销 +- **细粒度响应式**:自动追踪依赖,减少不必要重渲染 +- **API 兼容性**:与 React 生态高度契合,平滑迁移 +- **模板增强**:内置 if/for 等模板指令 +- **站点体验**:深色模式、搜索、高亮、课程与示例、交互动效 + +--- + +## 环境要求 + +- Node.js ≥ 18 +- 包管理器:推荐 pnpm(也可使用 yarn/npm) +- 由于本仓库包含 `openInula` 子项目(Vite 应用),其 Vite 插件要求 Node ≥ 18 + +--- + +## 安装与启动 + +1. 安装依赖(根项目): + +```bash +pnpm install +``` + +2. 安装依赖(可选,openInula 子项目用于 Playground REPL 示例): + +```bash +cd openInula && pnpm install +``` + +3. 本地开发: + +```bash +# 在根目录启动 Next.js 开发服务器 +pnpm dev + +# 如需同时启动 openInula 子项目(可选) +cd openInula && pnpm dev +``` +4. 启用文档搜索 +```bash +#首先构建项目 +pnpm build --no-lint + +#然后运行 postbuild 生成搜索索引 +pnpm postbuild +``` +--- + +## 构建与部署 + +- 根站点构建: +首先将openInula文件夹移到其他地方,不要在项目根目录。同时,也需要进入openInula文件夹进行一次pnpm build构建 + +```bash +pnpm build --no-lint +pnpm postbuild +``` + +- 站点启动(生产): + +```bash +pnpm start +``` + +- 构建后搜索索引:构建完成会自动执行 `postbuild`,用 Pagefind 生成到 `public/_pagefind`。 + +- 注意事项(重要): + - 本仓库包含 `openInula/` 子目录(一个独立的 Vite 应用,仅供 REPL/Playground),默认情况下并不会影响 Next.js 的构建。但如果你的自定义流程将其纳入同一产物或分析,请避免把 `openInula` 误当作 Next.js 的子模块进行打包。 + +--- + +## 目录结构(节选) + +``` +website-next/ +├─ src/ +│ ├─ app/ # Next.js App Router 页面与路由 +│ │ ├─ docs/ # 文档(MDX)与文档页 +│ │ ├─ playground/ # 教学与 Playground 页面 +│ │ ├─ api/ # API 文档/说明页 +│ │ └─ ... +│ ├─ components/ # 站点通用组件(含 HomePage、UI 组件等) +│ └─ lib/ # 工具、hooks、性能与 utils +├─ public/ # 静态资源(含 Pagefind 索引输出 _pagefind) +├─ openInula/ # 独立 Vite 应用(用于 REPL/Playground 示例) +├─ package.json +└─ README.md +``` + +--- + +## 常用脚本 + +根目录 `package.json`: + +```json +{ + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind" + } +} +``` + +- **dev**: 启动 Next.js 开发服务器 +- **build**: 生产构建(会为 App Router 产出 `.next`) +- **start**: 启动生产服务 +- **lint**: 运行 ESLint 检查 +- **postbuild**: 构建后生成 Pagefind 搜索索引 + +`openInula/package.json`(子项目): + +```json +{ + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + } +} +``` + +--- + +## FAQ / 故障排查 + +- **Pagefind 索引未生成或搜索无结果** + - 确认构建成功后已执行 `postbuild`。也可手动运行: + ```bash + pnpm build && pnpm exec pagefind --site .next/server/app --output-path public/_pagefind + ``` +- **本地开发页面加载缓慢或样式异常** + - 删除 `.next/` 与 `node_modules/` 重新安装依赖;检查 `tailwindcss` 版本与 PostCSS 配置是否匹配。 +- **Playground 示例不工作** + - `openInula/` 子项目需单独 `pnpm install` 与 `pnpm dev`;确保依赖 `openinula`、`inula-router` 正常安装。 + +--- + +## 贡献 + +欢迎任何形式的贡献! + +1. Fork 本仓库并克隆到本地 +2. 从 `main` 切出特性分支进行开发 +3. 发起 MR 并补充说明变更点与动机 + +详细流程与行为准则请参阅: + +- `src/app/docs/conventional/ContributingGuide/page.mdx` +- `src/app/docs/conventional/CodeOfConduct/page.mdx` + +--- \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000000000000000000000000000000000000..ffe928f5b6dfe484f57a5fd47d0487f21e164fa3 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000000000000000000000000000000000..c85fb67c463f20d1ee449b0ffee725a61dfb9259 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..90103deed549c74f25a4ca360162f0614163704e --- /dev/null +++ b/index.d.ts @@ -0,0 +1,461 @@ +import { ReactNode } from "react"; + +// ==================== 通用类型 ==================== +export type Theme = "light" | "dark"; + +// ==================== 导航相关接口 ==================== +export interface DropdownItem { + name: string; + href: string; +} + +export interface DropdownMenuProps { + name: string; + href: string; + dropdown: DropdownItem[]; + isActive: boolean; + isHovered: boolean; + onMouseEnter: () => void; + onMouseLeave: () => void; +} + +export interface SearchBarProps { + placeholder?: string; + onSearch: (query: string) => void; +} + +export interface CustomSearchProps { + onSearch: (query: string) => void; +} + +// ==================== 首页相关接口 ==================== +export interface Partner { + name: string; + logo: string; + url: string; +} + +export interface FeatureCardProps { + title: string; + description: string; + icon: ReactNode; + className?: string; +} + +export interface ActivityProps { + tabItems?: string[]; +} + +export interface LoadMoreSectionProps { + title: string; + items: T[]; + renderItem: (item: T) => ReactNode; + initialCount?: number; + step?: number; +} + +// ==================== 活动、资讯组件相关接口 ==================== +export interface NewsItem { + key: number; + img: string; + title: string; + introduce: string; + time: string; +} + +export interface ActivityItem { + key: number; + img: string; + name: string; + introduce: string; + beginTime: string; + endTime: string; + type: string; +} + +// ==================== 博客相关接口 ==================== +export interface BlogItem { + key: number; + img?: string; + title: string; + time: string; + author: string; + type: string; + tags?: string[]; +} + +export interface BlogCardProps { + blog: BlogItem; + theme: string | undefined; +} + +export interface BlogListProps { + blogs: BlogItem[]; + theme: string | undefined; +} + +export interface BlogFilterBarProps { + onFilterChange: (filter: string) => void; + activeFilter: string; +} + +export interface BlogPaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +// ==================== 贡献者相关接口 ==================== +export interface Contributor { + name: string; + avatar: string; + role: string; + description: string; + github?: string; + email?: string; +} + +// ==================== 文档相关接口 ==================== +export interface DocumentItem { + id: string; + title: string; + url?: string; + description: string; +} + +export interface DocumentSidebarProps { + documents: DocumentItem[]; + defaultActiveId?: string; + title?: string; + onDocumentChange?: (docId: string) => void; + className?: string; + children?: (currentDoc: DocumentItem | undefined) => ReactNode; +} + +// ==================== Playground相关接口 ==================== +export interface PlaygroundItem { + id: number; + title: string; + description: string; + category: string; + subcategory?: string; + slug: string; + content?: string; + code?: string; +} + +export interface PlaygroundSidebarProps { + playgroundData: PlaygroundItem[]; + selectedItem: PlaygroundItem | null; + isCollapsed: boolean; + onToggleCollapse: () => void; +} + +export interface PlaygroundContentProps { + selectedItem: PlaygroundItem | null; +} + +// ==================== 项目相关接口 ==================== +export interface Course { + id: string; + title: string; + description: string; + image: string; + duration: string; + level: string; + tags: string[]; +} + +// ==================== 页面组件接口 ==================== +export interface MDXDetailPageProps { + children: ReactNode; +} + +export interface MarkdownPageProps { + title: string; + description?: string; + children: ReactNode; + className?: string; +} + +// ==================== MagicUI组件接口 ==================== +export interface AuroraTextProps { + children: ReactNode; + className?: string; +} + +export interface MeteorsProps { + number?: number; +} + +export interface MagicCardProps { + children: ReactNode; + className?: string; + gradientColor?: string; +} + +export interface CodeComparisonProps { + leftCode: string; + rightCode: string; + leftTitle?: string; + rightTitle?: string; + language?: string; +} + +// ==================== 主题相关接口 ==================== +export interface ThemeToggleProps { + className?: string; +} + +export interface ThemeScriptProps { + theme?: string; +} + +// ==================== 性能相关接口 ==================== +export interface PerformanceMetrics { + renderTime: number; + memoryUsage: number; + bundleSize: number; +} + +export interface PerformanceComparisonProps { + inulaMetrics: PerformanceMetrics; + reactMetrics: PerformanceMetrics; + vueMetrics: PerformanceMetrics; +} + +// ==================== API相关接口 ==================== +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; +} + +export interface PaginationParams { + page: number; + limit: number; + total?: number; +} + +export interface SearchParams { + query: string; + filters?: Record; + sortBy?: string; + sortOrder?: "asc" | "desc"; +} + +// ==================== 表单相关接口 ==================== +export interface FormField { + name: string; + label: string; + type: + | "text" + | "email" + | "password" + | "textarea" + | "select" + | "checkbox" + | "radio"; + required?: boolean; + placeholder?: string; + options?: Array<{ value: string; label: string }>; + validation?: { + pattern?: RegExp; + message?: string; + minLength?: number; + maxLength?: number; + }; +} + +export interface FormProps { + fields: FormField[]; + onSubmit: (data: Record) => void; + submitText?: string; + className?: string; +} + +// ==================== 路由相关接口 ==================== +export interface RouteConfig { + path: string; + component: React.ComponentType; + exact?: boolean; + children?: RouteConfig[]; + meta?: { + title?: string; + requiresAuth?: boolean; + roles?: string[]; + }; +} + +// ==================== 国际化相关接口 ==================== +export interface LocaleConfig { + code: string; + name: string; + flag?: string; +} + +export interface I18nContextType { + locale: string; + setLocale: (locale: string) => void; + t: (key: string, params?: Record) => string; +} + +// ==================== 错误处理相关接口 ==================== +export interface ErrorBoundaryState { + hasError: boolean; + error?: Error; + errorInfo?: React.ErrorInfo; +} + +export interface ErrorBoundaryProps { + children: ReactNode; + fallback?: React.ComponentType<{ error: Error; resetError: () => void }>; +} + +// ==================== 动画相关接口 ==================== +export interface AnimationConfig { + duration: number; + easing: string; + delay?: number; + direction?: "normal" | "reverse" | "alternate" | "alternate-reverse"; + fillMode?: "none" | "forwards" | "backwards" | "both"; + iterationCount?: number | "infinite"; +} + +export interface AnimatedComponentProps { + animation: AnimationConfig; + children: ReactNode; + className?: string; + onAnimationEnd?: () => void; +} + +// ==================== 存储相关接口 ==================== +export interface StorageConfig { + key: string; + defaultValue?: any; + serializer?: { + serialize: (value: any) => string; + deserialize: (value: string) => any; + }; +} + +export interface UseStorageReturn { + value: T; + setValue: (value: T | ((prev: T) => T)) => void; + removeValue: () => void; +} + +// ==================== 网络请求相关接口 ==================== +export interface RequestConfig { + url: string; + method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + headers?: Record; + body?: any; + timeout?: number; + retries?: number; + retryDelay?: number; +} + +export interface UseRequestReturn { + data: T | null; + loading: boolean; + error: Error | null; + execute: (config?: Partial) => Promise; + reset: () => void; +} + +// ==================== 工具函数类型 ==================== +export type DebounceFunction any> = ( + func: T, + delay: number +) => (...args: Parameters) => void; + +export type ThrottleFunction any> = ( + func: T, + delay: number +) => (...args: Parameters) => void; + +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + +export type Optional = Omit & Partial>; + +export type Required = T & Required>; + +export type ValueOf = T[keyof T]; + +export type ArrayElement = T extends Array ? U : never; + +// ==================== 事件相关接口 ==================== +export interface EventHandler { + (event: T): void; +} + +export interface KeyboardEvent { + key: string; + code: string; + ctrlKey: boolean; + shiftKey: boolean; + altKey: boolean; + metaKey: boolean; +} + +export interface MouseEvent { + x: number; + y: number; + button: number; + buttons: number; +} + +// ==================== 媒体相关接口 ==================== +export interface MediaQuery { + query: string; + matches: boolean; +} + +export interface UseMediaQueryReturn { + matches: boolean; + addListener: (callback: (matches: boolean) => void) => void; + removeListener: (callback: (callback: boolean) => void) => void; +} + +// ==================== 时间相关接口 ==================== +export interface TimeConfig { + format: string; + timezone?: string; + locale?: string; +} + +export interface UseTimerReturn { + time: Date; + start: () => void; + stop: () => void; + reset: () => void; + isRunning: boolean; +} + +// ==================== 验证相关接口 ==================== +export interface ValidationRule { + required?: boolean; + minLength?: number; + maxLength?: number; + pattern?: RegExp; + custom?: (value: any) => boolean | string; +} + +export interface ValidationResult { + isValid: boolean; + errors: string[]; +} + +export interface UseValidationReturn { + values: T; + errors: Partial>; + touched: Partial>; + handleChange: (field: keyof T, value: any) => void; + handleBlur: (field: keyof T) => void; + validate: () => ValidationResult; + reset: () => void; +} diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..a24f1be8c258094ed590dcec54bbe4e5d1695436 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,22 @@ +import type { NextConfig } from "next"; +import nextra from 'nextra' + +const nextConfig: NextConfig = { + pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'openinula-website.obs.ap-southeast-1.myhuaweicloud.com', + port: '', + pathname: '/img/**', + }, + ], + }, +}; + +const withNextra = nextra({ + defaultShowCopyCode: true, +}) + +export default withNextra(nextConfig) diff --git a/openInula/README.md b/openInula/README.md new file mode 100644 index 0000000000000000000000000000000000000000..542c363af9c0adaf5c7f2e51635204f0da16886f --- /dev/null +++ b/openInula/README.md @@ -0,0 +1,4 @@ +# openinula + vite + +该模板提供了 `openinula` 工作在 `vite`的基础配置。 +> 请注意由于Vite插件有node版本限制,请使用`node -v`命令确认node版本大于等于node v18。 \ No newline at end of file diff --git a/openInula/index.html b/openInula/index.html new file mode 100644 index 0000000000000000000000000000000000000000..36465750d0be84de41dd2bd4b998348dd2a15865 --- /dev/null +++ b/openInula/index.html @@ -0,0 +1,12 @@ + + + + + PlayGround + + + +
+ + + diff --git a/openInula/package.json b/openInula/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d558964ea29e6748b6b0003a312df3ebfc73917d --- /dev/null +++ b/openInula/package.json @@ -0,0 +1,28 @@ +{ + "name": "inula-vite-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "", + "license": "MulanPSL2", + "dependencies": { + "@babytian/openinula-repl-next": "^0.0.1", + "inula-router": "^1.0.16", + "openinula": "^0.1.1" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@babel/preset-env": "^7.21.4", + "@babel/preset-react": "^7.18.6", + "@vitejs/plugin-react": "^3.1.0", + "@vitejs/plugin-react-refresh": "^1.3.6", + "babel-plugin-import": "^1.13.6", + "vite": "^4.2.1" + } +} diff --git a/openInula/pnpm-workspace.yaml b/openInula/pnpm-workspace.yaml new file mode 100644 index 0000000000000000000000000000000000000000..44c7411ca8b7024d569d4d4f4531c612cb301bc1 --- /dev/null +++ b/openInula/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +overrides: + openinula-repl-next: link:../../repl diff --git a/openInula/src/components/HomePage.jsx b/openInula/src/components/HomePage.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b0ffdb49d771c0d735f066dde01e17c908a9b4e4 --- /dev/null +++ b/openInula/src/components/HomePage.jsx @@ -0,0 +1,32 @@ + +import Inula, {useMemo, useRef} from 'openinula'; +import { Repl, Editor, Viewer } from '@babytian/openinula-repl-next' + +export default function App() { + const coded = useMemo(() => ` +import { render } from '@openinula/next'; +function HelloWorld() { + return

Hello OpenInula_next!!

; +} + +render(HelloWorld(), document.getElementById('app')); +`, []); + + const editorRef = useRef(); + const viewerRef = useRef(); + return ( +
+ + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/openInula/src/components/PlaygroundPage.jsx b/openInula/src/components/PlaygroundPage.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b6878492ac13f9c37fe763644a220ff92322d41e --- /dev/null +++ b/openInula/src/components/PlaygroundPage.jsx @@ -0,0 +1,79 @@ +import { useRef, useEffect, useState } from 'openinula'; +import { Repl, Editor, Viewer } from '@babytian/openinula-repl-next'; +import { getCodeById } from '../lib/code.ts'; + +function PlaygroundPage() { + // 从 URL 查询参数获取初始代码 ID,避免首屏回退到 HelloWorld + let initialId = 1; + try { + const params = new URLSearchParams(window.location.search); + const idParam = params.get('id'); + if (idParam) { + const parsed = Number(idParam); + if (!Number.isNaN(parsed)) initialId = parsed; + } + } catch (e) { + // 忽略解析错误,保持默认 id = 1 + } + + const initialCode = getCodeById(initialId) || getCodeById(1) || ''; + + const [code, setCode] = useState(initialCode); + + useEffect(() => { + const handler = (event) => { + const { type, payload } = event.data || {}; + + // 处理传统的代码字符串方式(向下兼容) + if (type === "SET_CODE" && payload) { + setCode(payload); + } + + // 处理新的代码ID方式 + if (type === "SET_CODE_BY_ID" && payload) { + const codeFromId = getCodeById(payload); + if (codeFromId) { + console.log('Setting new code for ID:', payload); + setCode(codeFromId); + } else { + console.warn(`未找到ID为 ${payload} 的代码示例`); + } + } + }; + + // 添加消息监听器 + window.addEventListener("message", handler); + + // 通知父窗口 iframe 已就绪,方便外部在需要时重发 + try { + window.parent?.postMessage({ type: 'IFRAME_READY' }, '*'); + } catch (e) {} + + return () => { + window.removeEventListener("message", handler); + }; + }, []); + + const editorRef = useRef(); + const viewerRef = useRef(); + const CustomEmptyMenu = () =>
; + + return ( +
+ + +
+ +
+
+
+ ); +} + +export default PlaygroundPage; diff --git a/openInula/src/index.css b/openInula/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..be0ab6c6d5bd3b1406afe759ae8a4e28518e64ce --- /dev/null +++ b/openInula/src/index.css @@ -0,0 +1,3 @@ +* { + box-sizing: border-box; +} \ No newline at end of file diff --git a/openInula/src/index.jsx b/openInula/src/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3dfd911958cca17121502216bfaf57b9437b4dce --- /dev/null +++ b/openInula/src/index.jsx @@ -0,0 +1,19 @@ +import { BrowserRouter, Route, Switch } from 'inula-router'; + +import HomePage from "./components/HomePage"; +import PlaygroundPage from "./components/PlaygroundPage"; +import Inula from "openinula"; + +function App() { + return ( + + + + + {/* 可以在这里添加更多路由 */} + + + ); +} + +Inula.render(, document.getElementById('root')); diff --git a/openInula/src/lib/code.ts b/openInula/src/lib/code.ts new file mode 100644 index 0000000000000000000000000000000000000000..d34c813c6ba0a5c7c8a17c198b88771d88759a35 --- /dev/null +++ b/openInula/src/lib/code.ts @@ -0,0 +1,199 @@ +export const codeSamples = [ + { + id: 1, + title: "入门", + code: `import { render } from '@openinula/next'; + + function HelloWorld() { + return

Hello World!

; +} + +render(HelloWorld(), document.getElementById('app'));`, + }, + { + id: 2, + title: "组件", + code: `import { render } from '@openinula/next'; + + function App() { + return

This is my first component

; +} + +render(App(), document.getElementById('app'));`, + }, + { + id: 3, + title: "状态管理", + code: `import { render } from '@openinula/next'; + + function UserInput() { + let count = 0; + function incrementCount() { + count = count + 1; + } + + return ( + <> +

{count}

+ + + ); +} + +render(UserInput(), document.getElementById('app')); +`, + }, + { + id: 4, + title: "计算属性", + code: `import { render } from '@openinula/next'; + + function DoubleCounter() { + let count = 0; + // 计算值:double 会自动随着 count 变化 + const double = count * 2; + + return ( +
+

当前计数:{count}

+

双倍值:{double}

+ +
+ ); +} + +render(, document.getElementById('app'));`, + }, + { + id: 5, + title: "监听系统", + code: `import { render, watch} from '@openinula/next'; + + function MultiWatch() { + let a = 1; + let b = 2; + + watch(() => { + console.log('a 变化:' + a); + }); + watch(() => { + console.log('b 变化:' + b); + }); + + return ( +
+ + +
+ ); +} + +render(, document.getElementById('app'));`, + }, + { + id: 6, + title: "条件渲染", + code: `import { render } from '@openinula/next'; + +function TrafficLight() { + let lightIndex = 0; + + let light = lightIndex ? 'green' : 'red'; + + function nextLight() { + lightIndex = (lightIndex + 1) % 2; + } + + return ( + <> + +

Light is: {light}

+

+ You must: + + STOP + + + GO + +

+ + ); +} + +render(TrafficLight(), document.getElementById('app')); +`, + }, + { + id: 7, + title: "列表渲染", + code: `import { render } from '@openinula/next'; + +function FruitList() { + const fruits = ['苹果', '香蕉', '橙子']; + + return ( +
    + + {(fruit) =>
  • {fruit}
  • } +
    +
+ ); +} + +render(, document.getElementById('app'));`, + }, + { + id: 8, + title: "事件处理", + code: `import { render } from '@openinula/next'; + +function ClickCounter() { + let count = 0; + + function handleClick() { + count++; + } + + return ( + + ); +} + +render(, document.getElementById('app'));`, + }, + { + id: 9, + title: "生命周期", + code: `import { render } from '@openinula/next'; + +function PageTitle() { + let counts = 0; + didMount(() => { + counts = counts + 5; + }); + return

页面标题:{counts}

; +} + +render(, document.getElementById('app'));`, + }, +]; + +export const getCodeById = (id: number): string | null => { + console.log( + "getCodeById called with ID:", + id, + "Available samples:", + codeSamples.map((s) => ({ id: s.id, title: s.title })) + ); + const sample = codeSamples.find((sample) => sample.id === id); + if (sample) { + console.log("Found sample:", sample.title); + return sample.code; + } else { + console.warn("No sample found for ID:", id); + return null; + } +}; diff --git a/openInula/vite.config.js b/openInula/vite.config.js new file mode 100644 index 0000000000000000000000000000000000000000..ae06c555f366b930ab9ce77739109cd91d3f1629 --- /dev/null +++ b/openInula/vite.config.js @@ -0,0 +1,58 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +let alias = { + react: "openinula", + "react-dom": "openinula", + "react/jsx-dev-runtime": "openinula/jsx-dev-runtime", + "react/jsx-runtime": "openinula/jsx-runtime", + "@openinula/next": "openinula", +}; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias, + }, + + build: { + outDir: "dist", + assetsDir: "assets", + sourcemap: false, // 禁用 sourcemap 以避免混淆问题 + minify: false, + + rollupOptions: { + input: { + main: "./index.html", + }, + output: { + manualChunks: undefined, + globals: { + openinula: "Inula", + "inula-router": "InulaRouter", + }, + }, + }, + + target: "es2015", + cssCodeSplit: false, + chunkSizeWarningLimit: 1000, + lib: false, + ssr: false, + }, + + server: { + port: 5173, + open: true, + }, + + optimizeDeps: { + include: ["openinula", "inula-router", "@babytian/openinula-repl-next"], + force: true, + }, + + esbuild: { + keepNames: true, + legalComments: "none", + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..5591dd2028456ae3d5854766978feb6875bcfc09 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "website-next", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind" + }, + "dependencies": { + "@ant-design/icons": "^6.0.0", + "@mdx-js/loader": "^3.1.0", + "@mdx-js/react": "^3.1.0", + "@next/mdx": "^15.3.5", + "@shikijs/transformers": "^3.7.0", + "@types/mdx": "^2.0.13", + "antd": "^5.26.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "gsap": "^3.13.0", + "highlight.js": "^11.11.1", + "lucide-react": "^0.525.0", + "motion": "^12.23.6", + "next": "15.4.1", + "next-themes": "^0.4.6", + "nextra": "^4.2.17", + "nextra-theme-docs": "^4.2.17", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-intersection-observer": "^9.16.0", + "react-markdown": "^10.1.0", + "shiki": "^3.7.0", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/typography": "^0.5.16", + "@types/node": "^20.19.4", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "eslint": "^9.30.1", + "eslint-config-next": "15.3.4", + "pagefind": "^1.3.0", + "tailwindcss": "^4.1.11", + "tw-animate-css": "^1.3.5", + "typescript": "^5.8.3" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000000000000000000000000000000000000..61e36849cf7cfa9f1f71b4a3964a4953e3e243d3 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/public/ContactUsPage/img.png b/public/ContactUsPage/img.png new file mode 100644 index 0000000000000000000000000000000000000000..bd3cb3f338c83342008c0a5f389bdc0f765f3c6e Binary files /dev/null and b/public/ContactUsPage/img.png differ diff --git a/public/ContactUsPage/logo.normal.png b/public/ContactUsPage/logo.normal.png new file mode 100644 index 0000000000000000000000000000000000000000..44cbadcb987df16e1a83a3d37ca305ff34f1e20b Binary files /dev/null and b/public/ContactUsPage/logo.normal.png differ diff --git a/public/ContactUsPage/map.png b/public/ContactUsPage/map.png new file mode 100644 index 0000000000000000000000000000000000000000..4cdb5d3035ac0eb66d094c4725e0c12b350ac469 Binary files /dev/null and b/public/ContactUsPage/map.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b1919800714d1ce2b1266ce7b76e42c8088b5bd9 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000000000000000000000000000000000000..004145cddf3f9db91b57b9cb596683c8eb420862 --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/font/geist-v3-latin-regular.woff2 b/public/font/geist-v3-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a190bc9f36ee8e3453fbe6324f940cd884c727bb Binary files /dev/null and b/public/font/geist-v3-latin-regular.woff2 differ diff --git a/public/footerIcon.bilibili.png b/public/footerIcon.bilibili.png new file mode 100644 index 0000000000000000000000000000000000000000..6445ea1850acbc4c7af12f7ec413e8bccb183b97 Binary files /dev/null and b/public/footerIcon.bilibili.png differ diff --git a/public/footerIcon.gitee.png b/public/footerIcon.gitee.png new file mode 100644 index 0000000000000000000000000000000000000000..773358b6e3efbbed23ff81dc102bca97567aa7fc Binary files /dev/null and b/public/footerIcon.gitee.png differ diff --git a/public/footerIcon.qrcode.png b/public/footerIcon.qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..ce5e89bec3fb384be089060f83a4a206a9968bce Binary files /dev/null and b/public/footerIcon.qrcode.png differ diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000000000000000000000000000000000000..567f17b0d7c7fb662c16d4357dd74830caf2dccb --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/navlogo.png b/public/navlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..7009246c037aaefd6f28b924be92912d67f1b696 Binary files /dev/null and b/public/navlogo.png differ diff --git a/public/navlogowhite.png b/public/navlogowhite.png new file mode 100644 index 0000000000000000000000000000000000000000..5f2c57b4d4410b7d0aea2790cb3f05f008650faa Binary files /dev/null and b/public/navlogowhite.png differ diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000000000000000000000000000000000000..5174b28c565c285e3e312ec5178be64fbeca8398 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/qrcode.inula.jpg b/public/qrcode.inula.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f853b30799af1b418553713b3e6db076657450d0 Binary files /dev/null and b/public/qrcode.inula.jpg differ diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000000000000000000000000000000000000..77053960334e2e34dc584dea8019925c3b4ccca9 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000000000000000000000000000000000000..b2b2a44f6ebc70c450043c05a002e7a93ba5d651 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/api/core/page.mdx b/src/app/api/core/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6a69239fc196fb976ed7bf9206466426c4e3addd --- /dev/null +++ b/src/app/api/core/page.mdx @@ -0,0 +1,1804 @@ +# Inula + +## 组件 + +组件是构建用户界面(UI)的基础,是可重用和独立的代码单元。开发者可以将相关功能、状态以及模板封装在组件中,提高可复用性,使得程序开发具备模块化和可维护特性。 + +### Component + +**功能介绍** + +`Component` 是类组件的基础形式,可以通过继承 `Component` 组件来定义一个类组件,并且使用类的特性,如构造函数、生命周期以及内部状态。 + +**组件定义** + +```jsx +class Component { + props: P; + context: C; + state: S | null; + refs: any; + forceUpdate: any; + isReactComponent: boolean; + constructor(props: P, context: C); + setState(state: S, callback?: Callback): void; +} +``` + +- `props`:组件接收的输入属性; +- `context`:组件的上下文对象; +- `state`:组件内部的状态; +- `refs`:用于引用组件中的 DOM 元素或其他组件; +- `forceUpdate`:用于强制重新渲染组件; +- `isReactComponent`:用于标识当前类是否是 React 组件类; +- `constructor(props: p, context: c)`:构造函数用于初始化组件的状态和属性; +- `setState(state: S, callback?: Callback): void`:用于更新组件的状态,并触发重新渲染。 + +**示例** + +```jsx +import * as Inula from 'openinula'; + +class Counter extends Inula.Component { + constructor(props) { + super(props); + this.state = { + counter: 0 + }; + console.log('Constructor'); + } + + componentDidMount() { + console.log('Component Did Mount'); + } + + componentDidUpdate() { + console.log('Component Did Update'); + } + + componentWillUnmount() { + console.log('Component Will Unmount'); + } + + handleIncrement = () => { + this.setState(prevState => ({ + counter: prevState.counter + 1 + })); + }; + + render() { + console.log('Render'); + return ( +
+

Counter: {this.state.counter}

+ +
+ ); + } +} + +Inula.render( + , + document.getElementById('root') +); +``` + +在上述示例中,基于 `Component` 组件类创建了一个 `Component` 组件以实现计数器功能,并在组件内部测试了一些声明周期的钩子函数。 + +### PureComponent + +**功能介绍** + +`PureComponent` 类继承自 `Component`,但是它内置了 `shouldComponentUpdate` 方法,会自动比较 `props` 和 `state` 的变化,如果没有变化则不会重新渲染组件,从而提高性能。 + +**组件定义** + +```jsx +class PureComponent extends Component { + constructor(props: P, context: C); +} +``` + +**示例** + +```jsx +import Inula, { PureComponent } from 'openinula'; + +// 创建一个继承自PureComponent的组件 +class App extends PureComponent { + render() { + console.log('Render PureDemo'); + return
{this.props.value}
; + } +} +``` + +上述示例中,基于 `PureComponent` 类创建了一个 `App` 组件,其使用上与 `Component` 类组件类似,但是其内置了 `shouldComponentUpdate` 方法,可以直接使用。 + +> 提示:在实际开发中,可以根据其优缺点,选择使用 `Component` 和 `PureComponent` 两种常用组件类 + +| 组件 | 优点 | 缺点 | +| ------------- |----------------------------------|--------------------------------------------------------| +| `PureComponent` | 1.自带浅比较,提高性能
2.适用于大多数情况 | 1.复杂的数据结构可能产生错误或者性能问题
2.对简单组件来说内置方法可能会存在额外开销 | +| `Component` | 1.较为灵活,没有内置方法,可以细粒度控制
2.适用于多控制场景 | 需要手动编写 `shouldComponentUpdate` 方法 | + +### memo + +**功能介绍** + +`memo` 是一个高阶组件(HOC),用于封装函数组件,以使其仅在其 `props` 发生更改时重新渲染。 + +**组件定义** + +```jsx +const MemoizedComponent = memo(type, compare?: Function); +``` + +- `type`:表示需要被优化的函数式组件; +- `compare`:可选参数,用于比较前后两次渲染的属性和状态是否相同的比较函数。如果未提供该函数,默认使用浅层比较。 + +**示例** + + +```jsx +import { memo, useState, render } from 'openinula'; + +const ExpensiveComponent = ({ value }) => { + // 模拟一个昂贵的计算 + const expensiveCalculation = () => { + console.log("Calculating..."); + let result = 0; + for (let i = 0; i < value * 1000000; i++) { + result += i; + } + return result; + }; + + const result = expensiveCalculation(); + + return
Result: {result}
; +}; + +const MemoizedExpensiveComponent = memo(ExpensiveComponent); + +function App() { + const [count1, setCount1] = useState(1); + const [count2, setCount2] = useState(1); + + return ( +
+ + + +
+ ); +} +render( + , + document.getElementById('root') +); +``` + + +在上述示例中,`` 是一个昂贵的计算组件,它在每次渲染时都会执行昂贵的计算操作。通过使用 `memo`,可以对 `` 进行优化,避免在 `count1` 没有变化时重新渲染。`memo` 会对前后两次渲染的属性和状态进行浅层比较,如果相同则不会重新渲染。 + +### Fragment + +**功能介绍** + +`` 是一个虚拟组件,用于包裹一组元素而不添加额外的 DOM 元素,可以提高渲染性能并减少不必要的包装元素。它可以用来解决在渲染多个元素时产生的包裹元素问题。在一些情况下,如果你不使用 ``,可能会出现额外的 DOM 层次结构,从而可能影响性能和样式。 + +**示例** + + +```jsx +import Inula, { Fragment, render } from 'openinula'; + +function App() { + return ( +
+ {/* 使用 Fragment */} + +
Element 4
+
Element 5
+
Element 6
+
+ + {/* 或者使用简化形式,使用空尖括号 */} + <> +
Element 7
+
Element 8
+
Element 9
+ +
+ ); +} +render( + , + document.getElementById('root') +); +``` + + +在上面的示例中,你可以看到 `` 的用法。当需要在组件中渲染多个元素时,使用 `` 可以避免添加额外的 DOM 元素。除了使用完整的 `` 标签外,你还可以使用空尖括号的简化形式 `<>` 和 ``。 + +### Children + +**功能介绍** + +`Children` 用于处理组件的子元素。它允许开发者在组件中访问和操作传递给组件的子元素,无论子元素是什么类型(单个元素、多个元素、文本等)。`Children` 提供了一些方法来遍历、映射和操作子元素,使得操作子元素变得更加方便。 + +**示例** + +```jsx +import Inula, { Children, cloneElement, render } from 'openinula'; + +// 示例 1: 遍历子元素并输出它们的类型 +function ElementList({ children }) { + const childArray = Children.toArray(children); + + const childTypes = childArray.map((child, index) => ( +

Child {index + 1} is of type: {child.type.name}

+ )); + + return
{childTypes}
; +} + +// 示例 2: 映射子元素并添加额外的 props +function AddPropsToChildren({ children }) { + const enhancedChildren = Children.map(children, (child) => { + return cloneElement(child, { isHighlighted: true }); + }); + + return
{enhancedChildren}
; +} + +// 示例 3: 只渲染特定类型的子元素 +function OnlyRenderCertainChildren({ children }) { + const filteredChildren = Children.toArray(children).filter(child => { + return child.type === 'p'; + }); + + return
{filteredChildren}
; +} + +function App() { + return ( +
+ +

Heading

+

Paragraph 1

+

Paragraph 2

+
Div element
+
+ + + Child 1 + Child 2 + + + +

Paragraph 1

+
Div element
+

Paragraph 2

+
+
+ ); +} +render( + , + document.getElementById('root') +); +``` + +上述展示了的示例包含以下方法: + +- `Children.map(children, fn, thisArg?)` +- `Children.forEach(children, fn, thisArg?)` +- `Children.only(children)` +- `Children.toArray(children)`。 + +在上面的示例中,展示了如何使用 `Children` 对子元素进行遍历、映射和筛选。`Children.toArray` 方法用于将子元素转换为数组,从而可以使用数组的方法。`Children.map` 方法可以映射子元素并为其添加额外的 `props`。同时还演示了如何通过 `child.type` 来识别子元素的类型,从而选择性地渲染特定类型的子元素。 + +### Suspense + +**功能介绍** + +``是一个组件,用于在异步加载数据时展示一个 fallback 视图 + +**接口定义** + +```tsx +}> + + +``` + +- `fallback`: 用于定义在组件加载期间显示的 fallback 视图; +- ``: 表示需要异步加载的组件。 + +**示例** + + +```jsx +import Inula, { lazy, Suspense } from 'openinula'; + +// 异步加载组件 +const AsyncComponent = lazy(() => import('./AsyncComponent')); + +const App = () => ( +
+ {/* 在加载异步组件时显示fallback元素 */} + Loading...
}> + +
+ +); + +// 组件可以是一个普通的函数式组件或类组件 +export default AsyncComponent = () => { + // 随意模拟一个异步加载的操作 + setTimeout(() => { + console.log('AsyncComponent is loaded.'); + }, 2000); + + return
Async Component
; +}; +``` + +在上面的示例中,使用 [lazy](#lazy) 函数来异步加载一个组件 ``。然后,使用 `` 组件来包裹异步组件,并在加载组件时显示 fallback 元素。在这个示例中,“fallback ...” 文本。 + +## 钩子函数 + +在 openInula 中,可以使用钩子函数来在函数组件中添加状态和其他特性函数,从而允许我们在函数组件中使用类组件拥有的功能。通过使用钩子函数,我们可以更方便地编写和管理React组件的状态和行为,使代码更简洁、可读性更高。 + +钩子函数可以让我们在函数组件中使用状态管理、生命周期方法、副作用等功能。常用的钩子函数包括 `useState`、`useEffect`、`useContext`、`useReducer` 等。 + +> 注意: +> 1. 只能在组件的顶层或自己的 Hook 中调用钩子函数。 +> 2. 不能在循环语句或条件语句中调用钩子函数。 + +### useState + +**功能介绍** + +`useState` 是一个用于在函数组件中添加状态( `state` )的钩子函数。使用 `useState` 可以让函数组件拥有类组件的状态管理能力。 + +**接口定义** + +```jsx +const [state, setState] = useState(initialState); +``` + +- `initialState`:状态的初始值,在组件挂载时使用。 + +`useState` 的返回值: + +- `state`:表示当前状态的值; +- `setState`:用于更新状态的函数,可以传递新的状态值或一个函数来计算新的状态值。 + +**示例** + +```jsx +import Inula, { useState, render } from "openinula"; + +function Counter() { + const [count, setCount] = useState(0); + + const increment = () => { + setCount(prevCount => prevCount + 1); + }; + + const decrement = () => { + setCount(prevCount => prevCount - 1); + }; + + return ( +
+

Count: {count}

+ + +
+ ); +} +render( + , + document.getElementById('root') +); +``` + +### useCallback + +**功能介绍** + +`useCallback` 是一个用于优化性能的钩子函数,它可以用来缓存函数引用,避免在每次渲染时重新创建函数。这对于将函数作为 `props` 传递给子组件时特别有用,可以避免不必要的重新渲染。 + +**接口定义** + +```tsx +const cachedFn = useCallback(callback: T, deps: DependencyList) +``` + +- `callback`:要缓存的回调函数; +- `deps`:一个数组,包含了所有可以作为依赖项的值,当依赖项发生变化时,`callback` 函数会被重新创建。如果依赖项列表为空,那么每次渲染都会返回相同的缓存函数。 + +`useCallback`的返回值: + +- `cachedFn`:若是初次渲染,返回已传入的 `callback` 函数,当依赖项 `deps` 无变化时,返回上次渲染缓存的 `callback` 函数,当依赖项 `deps` 有变化时,则返回这次渲染传入的 `callback` 函数。 + +**示例** + +```jsx +import Inula, { useState, useCallback, render } from 'openinula'; + +function App() { + const [count, setCount] = useState(0); + + // 使用 useCallback 缓存 handleIncrement 函数 + const handleIncrement = useCallback(() => { + setCount(count + 1); + }, [count]); // 仅当 count 发生变化时,才会重新创建函数 + + return ( +
+

Count: {count}

+ +
+ ); +} +render( + , + document.getElementById('root') +); +``` + + +> 注意:使用 `useCallback`,可以确保每次渲染时都使用相同的函数引用,从而减少不必要的函数重新创建,提高性能。这在传递回调函数给子组件或在使用 `useEffect` 时尤为有用,因为它可以防止因为函数的重新创建而导致子组件的不必要重新渲染。 + +### useContext + +**功能介绍** + +`useContext` 用于在组件之间共享状态。通过使用 `useContext`,可以避免在组件树中手动传递 `props`,从而更方便地访问共享的数据。 + +**接口定义** + +```tsx +const value = useContext(context: Context) +``` + +- `context`:是 openInula 提供的上下文,可以通过 `createContext` 创建,`useContext` 接收上下文并返回该上下文当前值。 + +`useContext`的返回值: + +- `value`:其返回值是一个包含Provider的值,其中 `value.Provider` 是由最近的上层上下文提供,如果没有找到匹配的上层上下文,则会使用上下文对象的默认值。 + +**示例** + +```jsx +import Inula, { createContext, useContext, useState, render } from 'openinula'; + +// 创建一个上下文对象 +const Context = createContext(); + +// 组件A:向上下文中提供一个共享的数据 +const ComponentA = () => { + const [count, setCount] = useState(0); + + return ( + + + + + ); +} + +// 组件B:从上下文中获取共享的数据 +const ComponentB = () => { + const count = useContext(Context); + return
当前计数:{count}
; +} + +// 主组件 +const App = () => { + return ; +} +render( + , + document.getElementById('root') +); +``` + +在这个示例中,`` 创建了一个共享的数据 `count`,它使用 `useState` 来进行状态管理,并将数据存储在上下文对象` Context.Provider` 中。`` 使用 `useContext` 从 `Context` 中获取共享的数据 `count`。最后,`` 组件作为根组件渲染 ``,这样就能够在整个组件树中共享 `count` 数据。 + +> 说明:在函数组件中,`useContext` 代替了类组件中的 `Consumer`。 + +### useEffect + +**功能介绍** + +`useEffect` 是一个用于处理副作用(如数据获取、订阅、DOM 操作等)的钩子函数。它在组件的生命周期中执行,并且可以用于在组件渲染后执行一些操作,或在组件卸载前执行一些清理工作。 + +**接口定义** + +```tsx +useEffect(create: EffectCallBack, deps?: DependencyList): void +``` + +- `create`:一个函数,用于定义副作用操作。该函数在每次渲染后都会被调用; +- `deps`:一个数组,指定了哪些依赖项的变化会触发 `create` 的重新执行。如果不传递这个数组,那么 `create` 将在每次渲染后都被调用。如果依赖项列表为空,那么 `create` 将只在组件挂载和卸载时执行。 + +**示例** + +- 示例一 + +```jsx +import Inula, { useState, useEffect, render } from "openinula"; + +function App() { + const [count, setCount] = useState(0); + + useEffect(() => { + document.title = `点击次数:${count}`; + }, [count]); + + return ( +
+

点击次数: {count}

+ +
+ ); +} +render( + , + document.getElementById('root') +); +``` + +在上述示例中,`useEffect` 可以用于数据的获取,当点击 ` + + ); +} +render( + , + document.getElementById('root') +); +``` + +在示例二中,`useEffect` 可以用于设置和清理定时器。 + +- 示例三: + +```jsx +useEffect(() => { + // 更新元素样式 + document.getElementById('myElement').style.color = 'red'; + + // 添加元素 + const newElement = document.createElement('div'); + document.body.appendChild(newElement); + + // 移除元素 + return () => { + document.body.removeChild(newElement); + }; +}, []); +``` + +上述示例展示 `useEffect` 的DOM操作,可以在 `useEffect` 中进行DOM操作,比如更新元素的样式、添加/移除元素等。 + +- 示例四: + +根据依赖项的变化,`useEffect` 的行为会有所不同。在函数组件中,通常使用 `useEffect` 来模拟组件的生命周期。 + +```jsx +useEffect(() => { + // componentDidMount + console.log('Component mounted'); + + // componentDidUpdate + return () => { + // componentWillUnmount + console.log('Component unmounted'); + }; +}, []); + +useEffect(() => { + // componentDidUpdate + console.log('Component updated'); +}, [data]); +``` + +上述示例中,使用 `useEffect `来模拟组件的生命周期方法,比如 `componentDidMount`、`componentDidUpdate` 和 `componentWillUnmount`。 + +### useLayoutEffect + +**功能介绍** + +`useLayoutEffect` 是一个钩子函数,用于在 DOM 更新完成后同步执行副作用操作。与 `useEffect` 不同,`useLayoutEffect` 会在 DOM 更新之后、浏览器绘制之前同步执行,因此适用于需要立即操作 DOM 的情况。 + +**接口定义** + +```jsx +useLayoutEffect(create: EffectCallBack, deps?: DependencyList): void; +``` + +- `create`:一个函数,用于定义副作用操作。该函数会在 DOM 更新完成后、浏览器绘制之前立即执行; +- `deps`:一个数组,指定了哪些依赖项的变化会触发 `create` 的重新执行。如果不传递这个数组,那么 `create` 将在每次渲染后都被调用。如果依赖项列表为空,那么 `create` 将只在组件挂载和卸载时执行。 + +**示例** + +```jsx +import Inula, { useState, useLayoutEffect, render } from 'openinula'; + +function App() { + const [width, setWidth] = useState(0); + + useLayoutEffect(() => { + const handleResize = () => { + setWidth(window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + handleResize(); // 初始渲染时执行一次 + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); // 仅在挂载和卸载时执行 + + return ( +
+

Window Width: {width}

+
+ ); +} +render( + , + document.getElementById('root') +); +``` +在上述示例中,`useLayoutEffect` 用于更新页面上窗口的宽度。它添加了一个 `resize` 事件监听器,在窗口大小变化时同步更新 `width` 状态。注意,这里使用了空数组作为依赖项,这意味着 `useLayoutEffect` 只在组件挂载和卸载时执行。 + +> 注意: +> `useEffect` 和 `useLayoutEffect` 都用于在组件渲染时执行副作用操作,但是主要区别与执行的时机。`useEffect` 会在组件渲染完成后异步执行,不会阻塞组件的渲染过程。而 `useLayoutEffect` 会在组件渲染完成后同步执行,会阻塞组件的渲染过程。因此,如果需要在组件渲染完成后立即执行某些操作,可以使用 `useLayoutEffect`,否则可以使用 `useEffect`。 + +```jsx +import Inula, { useState, useEffect, useLayoutEffect, render } from 'openinula'; + +const App = () => { + const [count, setCount] = useState(0); + + useEffect(() => { + console.log('useEffect'); + document.title = `Count: ${count}`; + }, [count]); + + useLayoutEffect(() => { + console.log('useLayoutEffect'); + document.title = `Count: ${count}`; + }, [count]); + + return ( +
+

Count: {count}

+ +
+ ); +}; +render( + , + document.getElementById('root') +); +``` + +在上面的例子中,若使用 `useEffect` 来更新页面标题,它会在组件渲染完成后执行,并且只有在 `count` 发生变化时才会执行。若使用 `useLayoutEffect` 来更新页面标题,它会在组件渲染完成后立即执行,并且只有在 `count` 发生变化时才会执行。 + +> 注意: +> 由于 `useLayoutEffect` 是同步执行的,如果操作的是 DOM,可能会影响页面性能。在大多数情况下,使用 `useEffect` 就足够了,因为它会在 DOM 更新完成后异步执行副作用,不会阻塞浏览器的渲染。只有在需要即时操作 DOM 以获取测量值等情况下,才考虑使用 `useLayoutEffect`。 + +### useMemo + +**功能介绍** + +`useMemo` 是一个用于性能优化的钩子函数,它用于在组件重新渲染时,缓存和返回一个计算结果,以避免不必要的重复计算。 + +**接口定义** + +```jsx +const cachedValue = useMemo(create, deps) +``` + +- `create`:一个函数,用于计算需要缓存的值; +- `deps`:一个数组,指定了哪些依赖项的变化会触发 `create` 的重新执行。当依赖项发生变化时,`create` 将会被重新执行,返回一个新的缓存值。如果依赖项列表为空,那么` create` 只在组件挂载时执行。 + +`useMemo` 的返回值: + +- `cachedValue`:要缓存计算值的函数,可以为任意类型,如果 `deps` 没有发生变化,会直接返回相同的值,如果 `deps` 发生变化,则调用 `create`,并返回新的结果,然后缓存这个结果。 + +**示例** + +```jsx +import Inula, { useState, useMemo, render } from 'openinula'; + +function ExpensiveComponent({ number }) { + // 模拟一个昂贵的计算 + const expensiveCalculation = () => { + console.log("Calculating..."); + let result = 0; + for (let i = 0; i < number * 1000000; i++) { + result += i; + } + return result; + }; + + // 使用 useMemo 缓存计算结果 + const memoizedValue = useMemo(() => expensiveCalculation(), [number]); + + return ( +
+

Number: {number}

+

Calculated Value: {memoizedValue}

+
+ ); +} + +function App() { + const [count, setCount] = useState(1); + + return ( +
+ + +
+ ); +} +render( + , + document.getElementById('root') +); +``` + +在这个示例中,`ExpensiveComponent` 组件包含一个昂贵的计算操作,该计算在每次渲染时都会执行。通过使用 `useMemo`,可以缓存计算结果,只有在 `number` 发生变化时才重新计算。这可以有效避免在每次渲染时都执行昂贵的计算,从而提高性能。 + +> 注意:`useMemo` 可以在性能优化方面发挥作用,但不应该被滥用。在某些情况下,过度使用 `useMemo` 可能会导致代码更难理解。只有在确实需要避免重复计算的情况下,才应该使用 `useMemo`。 + +### useReducer + +**功能介绍** + +`useReducer` 是一个用于管理状态的钩子函数,它结合了 `useState` 和自定义的状态更新逻辑,通常用于管理复杂的状态逻辑。 + +**接口定义** + +```jsx +const [state, dispatch] = useReducer(reducer, initialState, init?); +``` + +- `reducer`:一个函数,用于定义状态更新逻辑。它接收当前状态 state 和一个表示动作的对象 action,并返回新的状态; +- `initialState`:初始状态的值, 计算逻辑取决于 `init` 参数; +- `init`:可选参数,用于计算初始状态值的函数,如果存在,则使用 `init(initialState)` 的执行结果作为初始值,否则使用 `initialState`。 + +`useReducer` 有两个返回值: + +- `state`:当前状态的值; +- `dispatch`:一个函数,用于触发状态更新。它接收一个表示动作的对象,并将该对象传递给 `reducer`,从而更新状态 `state`。 + +**示例** + +```jsx +import Inula, { useReducer, render } from 'openinula'; + +// 定义 reducer 函数 +const counterReducer = (state, action) => { + switch (action.type) { + case 'INCREMENT': + return { count: state.count + 1 }; + case 'DECREMENT': + return { count: state.count - 1 }; + default: + return state; + } +}; + +function App() { + // 使用 useReducer 来管理状态 + const [state, dispatch] = useReducer(counterReducer, { count: 0 }); + return ( +
+

Count: {state.count}

+ + +
+ ); +} +render( + , + document.getElementById('root') +); +``` + +在上述示例中,使用了 `useReducer` 以允许通过 `dispatch` 函数触发 `state` 的更新。在需要更新状态时,可以向 `dispatch` 传递一个表示动作的对象(`action`),随后 `reducer` 函数,即 `counterReducer` 函数将根据动作类型 (`action.type`)为`INCREMENT` 还是 `DECREMENT` 更新状态 `state`。 + +> 注意:相比使用多个`useState`来管理状态,`useReducer` 适用于具有复杂状态逻辑的情况,它能够更好地组织和管理状态变化。 + +### useRef + +**功能介绍** + +`useRef` 用于创建一个引用对象,可以用来存储组件的引用或持久化的值。与 `useState` 不同,`useRef` 创建的引用对象在更新时不会触发组件重新渲染。 + +**接口定义** + +```jsx +const refContainer = useRef(initialValue); +``` + +- `initialValue`:引用对象的初始值,可以是任何值,通常设置为 `null`。 + +`useRef` 的返回值: + +- `refContainer`:一个引用对象,其中的 `current` 属性可以被用于存储和访问值。 + +**示例** + +```jsx +import Inula, { useRef, render } from 'openinula'; + +function RefDemo() { + const inputRef = useRef(null); + + const handleButtonClick = () => { + // 使用 ref 获取 input 元素的引用,并设置焦点 + inputRef.current.focus(); + }; + + return ( +
+ + +
+ ); +} +render( + , + document.getElementById('root') +); +``` + +上面的示例中,创建了一个函数组件 `RefDemo`,使用 `useRef` 钩子创建了一个名为 `inputRef` 的 `ref` 对象,并在 JSX 中将这个 `ref` 对象绑定到输入框元素上。 + +当按钮被点击时,`handleButtonClick` 函数会使用 `inputRef.current` 来获取输入框的 DOM 元素,并调用 `focus()` 方法来设置焦点。这样,点击按钮后,输入框就会自动获得焦点。 + +> 注意:`useRef` 不仅适用于获取 DOM 元素的引用,还可以在函数组件中存储和访问任意可变值。与 `state`一样,`ref` 可以指向任何东西:比如字符串、对象甚至函数。与 `state` 不同的是,`ref` 是一个带有当前属性的普通对象,可以读取和修改。 + +**存储变量** + +```jsx +import Inula, { useState, useRef, useCallback, render } from 'openinula' + +function ClickWatch() { + let ref = useRef(0); + + const handleClick = useCallback(() => { + ref.current = ref.current + 1; + alert('You clicked ' + ref.current + ' times!'); + }, []) + + return ( + + ); +} +render( + , + document.getElementById('root') +); +``` + +在上述示例中,将在每次点击后增加 `ref.current`。 + +> 注意:组件不会在每次递增时都触发重新渲染。 像 `state` 一样,`ref` 在重新渲染时被 openInula 保留。然而,`setState` 将会重新渲染组件,修改 `ref` 则不会。 + +### useImperativeHandle + +**功能介绍** + +`useImperativeHandle` 是一个用于定制在父组件中访问子组件实例方法的钩子函数。通常情况下,openInula 鼓励使用 `props` 和 `state` 来进行组件之间的通信,但在某些情况下,可能需要从父组件中直接调用子组件。 + +**接口定义** + +```jsx +useImperativeHandle(ref, createHandle, deps?):void; +``` + +- `ref`:一个 `ref` 引用对象,通常通过 `createRef()` 创建; +- `createHandle`:一个函数,用于创建子组件实例中需要暴露给父组件的方法或属性; +- `deps`:可选参数,一个依赖数组,当依赖项变化时,会重新计算 `createHandle`。通常在这里指定子组件的内部状态或其他信息,以确保暴露给父组件的方法在这些依赖项变化时得到更新。 + +**示例** + +```jsx +import Inula, { useRef, useImperativeHandle, forwardRef, render } from 'openinula'; + +// 子组件 +const ChildComponent = forwardRef((props, ref) => { + const inputRef = useRef(); + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + focusInput: () => { + inputRef.current.focus(); + } + })); + + return ; +}); + +// 父组件 +function App() { + const childRef = useRef(); + + const handleFocusInput = () => { + childRef.current.focusInput(); + }; + + return ( +
+ + +
+ ); +} +render( + , + document.getElementById('root') +); +``` + +在这个例子中,`` 使用 `useImperativeHandle` 来创建一个名为 `focusInput` 的方法,父组件可以通过` ref` 调用这个方法来聚焦输入框。通过这种方式,你可以在需要时从父组件中控制子组件的行为。 + +> 注意:`useImperativeHandle` 应该避免过度使用,因为它破坏了组件之间的封装性,通常情况下应该优先考虑使用 `props` 和状态来实现组件之间的通信。 + +## APIs + +### render + +**功能介绍** + +在指定的 DOM 元素下渲染 openInula 组件。 + +**接口定义** + +```tsx +function render(children: any, container: Container, callback?: any): Element | Text | null +``` + +* `children`:需要渲染的 openInula 组件; +* `container`:DOM元素,`children` 将会渲染在该元素中; +* `callback`:挂载后的回调函数。 + +**示例** + +```tsx +import { render } from 'openinula'; + +function App() { + return

hello

; +} + +render(, document.getElementById('root')); +``` + + +上述示例展示在 DOM 元素 `
` 下挂载了组件 ``,组件 `` 返回了一个 html 标题元素。 + +### createElement + +**功能介绍** + +当无法使用 JSX 语法时,可以使用 `createElement` 创建一个 openInula 元素。 + +**接口定义** + +```tsx +function createElement(type: any, setting: any, ...children: any[]): { + [x: string]: any; + vtype: number; + src: null; + type: any; + key: any; + ref: any; + props: any; +} +``` + +* `type`:创建的组件类型; +* `setting`:创建元素时传入的 `props` 对象。 +* `...children`:组件的零个或多个子节点。 + +**示例** + +```tsx +import { createElement } from 'openinula'; + +function Welcome() { + return createElement( + 'h1', + { className: 'welcome' }, + '你好' + ); +} +``` + +在上述示例中,创建了一个 `` 函数组件,这个组件返回了一个 `h1` 元素,其中包含了一个 `className` 属性,属性值为 'welcome',并包含文本内容'你好'。 + +### createPortal + +**功能介绍** + +`createPortal` 可以将某些元素的子元素渲染到 DOM 中的不同位置。 + +**接口定义** + +```tsx +function createPortal(children: any, realNode: any, key?: string): PortalType + +type PortalType = { + vtype: number; + key: null | string; + realNode: any; + children: any; +}; +``` + +* `children`:需要渲染的 openInula 组件; +* `readNode`:是某个已经存在的DOM节点,`children` 将会渲染在该元素中; +* `key`:可选参数,用作 portal key 的独特字符串或数字。 + +**示例** + +```jsx +import { createPortal, render } from 'openinula'; + +function Component() { + return ( +
+

这个子节点被放置在父节点 div 中

+ {createPortal( +

这个子节点被放置在 document body 中

, + document.body + )} +
+ ); +} +render(, document.getElementById('root')); +``` + +在这段代码中,`createPortal` 函数传入的第一个参数是一个 `

` 标签,第二个参数是 `document.body`,意味着这个`

` 标签将被创建并插入到文档的 `body` 元素中。这样,即使父组件的 `

` 被其他元素覆盖,这个 `

` 标签也会被放置在 `body` 的最顶层,而不会被覆盖。 + +> 注意:portal 只改变 DOM 元素所处的位置,而不改变组件在 React 树中的位置。子节点仍能访问父节点的上下文,且事件仍将从子节点冒泡到父节点。 + +### createRef + +**功能介绍** + +`createRef` 用于在类组件中声明一个 `ref` 对象,该对象可以用来引用DOM元素或组件实例。 + +**接口定义** + +```tsx +function createRef(): RefType + +type RefType = { + current: any; +}; +``` + +**示例** + +```jsx +import { Component, createRef, render } from 'openinula'; + +class App extends Component { + inputRef = createRef(); + + handleClick = () => { + this.inputRef.current.value++; + } + + render() { + return ( + <> + + + + ); + } +} +render(, document.getElementById('root')); +``` + +上述示例创建了一个 `App` 组件,通过调用 `createRef()` 函数创建一个 `ref` 对象 `inputRef`。在渲染时,`handleClick` 方法会被调用,它会通过 `this.inputRef.current.value++` 来增加 `input` 元素的值。`input` 元素通过 `ref` 属性引用 `inputRef`。这样就可以通过 `this.inputRef` 来操作输入框。 + +### forwardRef + +**功能介绍** + +`forwardRef` 允许组件通过参数中的 `ref` 将一个 DOM 节点暴露给父组件。 + +**类型定义** + +```tsx +function forwardRef(render: Function): { + vtype: number; + $$typeof: number; + render: Function; +} +``` + +* `render`: 是一个组件渲染函数,可以接收 `props` 和 `ref` 作为参数。 + +**示例** + +```tsx +import { forwardRef, useRef, render } from 'openinula'; + +const MyInput = forwardRef(function MyInput(props, ref) { + const { label, ...otherProps } = props; + return ( + + ); +}); + +export default function Form() { + const ref = useRef(null); + + function handleClick() { + ref.current.value++; + } + + return ( +

+ + + + ); +} +render(
, document.getElementById('root')); +``` + +上述示例中,`MyInput` 组件通过 `forwardRef` 将 `input` 输入框暴露给父组件 `Form`,这样 `Form` 可以通过该 `ref` 操作该 `inupt`。 + +### lazy + +**功能介绍** + +`lazy` 声明一个懒加载组件,直到该组件第一次被加载前,才会加载该组件的代码。 + +**接口定义** + +```tsx +function lazy(promiseCtor: () => PromiseType<{ + default: T; +}>): LazyComponent> +``` + +* `promiseCtor` 是一个返回 `Promise` 对象的函数,第一次渲染组件前,openInula 才会调用该函数获取组件。 + +**示例** + +```jsx +import { lazy } from 'openinula'; + +const LazyComp = lazy(() => import { App } from './App'); +``` + +在上述示例中,使用`lazy`函数来异步加载``组件。`lazy` 函数接受一个函数,该函数返回一个`import()`语句,用于动态加载组件模块。 + +### memo + +**功能介绍** + +使用 `memo` 函数包裹组件,通常情况下,只要该组件的 props 没有改变,可以防止组件的重新渲染。 + +**接口定义** + +```tsx +function memo(type: any, compare?: ((oldProps: Props, newProps: Props) => boolean) | undefined): { + vtype: number; + $$typeof: number; + type: any; + compare: ((oldProps: Props, newProps: Props) => boolean) | null; +} +``` + +* `type`:需要进行记忆化的组件; +* `compare`:可选参数,是一个函数,接收一对新旧 `props`,判断两者是否完全相同。 + +**示例** + +```jsx +import { memo, useState, render } from 'openinula'; + +const MyComponent = ({ name }) => { + return
Hello, {name}!
; +}; + +// 使用memo将MyComponent包装 +const MemoizedComponent = memo(MyComponent); + +const App = () => { + const [count, setCount] = useState(0); + + const handleClick = () => { + setCount(prevCount => prevCount + 1); + }; + + return ( +
+ {/* 每次更新会重新渲染MyComponent */} + + + {/* 使用MemoizedComponent,只有在name发生变化时才会重新渲染 */} + + +
+ Current count: {count} + +
+
+ ); +}; +render(, document.getElementById('root')); +``` + +在上述示例中,创建了两个组件,一个是普通 `MyComponent` 组件,一个是使用 `memo` 包装的 `MemoizedComponent`。使用 `memo` 包装的 `MemoizedComponent` 只会在 `props` 的 `name` 属性发生变化时重新渲染。在点击按钮时,由于 `MemoizedComponent` 的 `name` 属性没有变化,所以不会重新渲染。 + +> 注意:在实际场景中,memo可以有效地避免无需更新的组件重新渲染,提高应用的性能。 + +### createContext + +**功能介绍** + +`createContext` 创建一个可供不同组件调用的上下文对象,这样各层组件之间不需要通过逐层传递信息。 + +**接口定义** + +```tsx +function createContext(val: T): ContextType +``` + +* `val`: 是上下文的初始值。 + +**示例** + +```tsx +import { createContext, render, useContext } from 'openinula'; + +const LanguageContext = createContext('zh-cn'); + +function Header() { + const locale = useContext(LanguageContext); + return

Header {locale}

; +} + +function Footer() { + const locale = useContext(LanguageContext); + return

Footer {locale}

; +} + +function Page() { + const locale = useContext(LanguageContext); + return ( +
+
+

Page {locale}

+
+
+ ); +} +render(, document.getElementById('root')); +``` + +上述示例展示了 `createContext` 方法的使用方式,可以通过 `createContext` 创建一个供全局组件使用的 `LanguageContext` ,组件可以直接获取,不需要通过逐级传值的方式进行获取。 + +### cloneElement + +**功能介绍** + +`cloneElement` 可以从一个已有元素创建一个新的 Inula 元素。 + +**接口定义** + +```jsx +function cloneElement(element: any, setting: any, ...children: any[]): { + [x: string]: any; + vtype: number; + src: null; + type: any; + key: any; + ref: any; + props: any; +} +``` + +* `element`:需要克隆的 Inula 元素; +* `setting`:克隆后元素的 `props`; +* `...children`:是零个或多个子节点,如果不传入,将使用原始 `element.props.children`。 + +**示例** + +```jsx +import { cloneElement, createElement, render } from 'openinula'; + +function Time({ time }) { + return createElement( + 'h1', + { className: 'clock' }, + 'Time is ', + createElement('i', null, time), + ); +} +function Clock() { + const time = new Date().toLocaleTimeString(); + return cloneElement( +
}> + + inner loading...
}> + +
+ +``` +- 外层和内层 Suspense 可分别控制不同异步内容的 loading 状态。 + +## 错误处理 + +- 异步加载失败时可结合 ErrorBoundary 捕获错误。 +- lazy 组件必须返回 Promise,resolve 时需有 default 导出。 + +## 注意事项 + +- Suspense 只对 lazy 组件生效。 +- fallback 必须为有效的 React 元素。 +- 支持多层嵌套,内层优先显示 fallback。 + +## 最佳实践 + +- 推荐为每个异步区域单独设置 Suspense,提升用户体验。 +- fallback UI 应简洁明了。 + +## 相关链接 + +- [React Suspense](https://react.dev/reference/react/Suspense) \ No newline at end of file diff --git a/src/app/docs/components/composition/page.mdx b/src/app/docs/components/composition/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..aff7a5dcea4c9b7205dc86fb7948cd3645a994be --- /dev/null +++ b/src/app/docs/components/composition/page.mdx @@ -0,0 +1,120 @@ +--- +id: composition +title: 组件组合 +sidebar_label: 组件组合 +--- + +# 组件组合 + +组件组合是构建复杂 UI 的基础。openInula 2支持多种组合模式,包括容器组件、复合组件、插槽(children)、高阶组件等。 + +## 容器组件与展示组件 + +将逻辑与展示分离,提升复用性。 + +```tsx filename="UserCardAndList.jsx" +// 展示组件 +function UserCard({ user }) { + return ( +
+

{user.name}

+

邮箱:{user.email}

+
+ ); +} + +// 容器组件 +function UserList({ users }) { + return ( +
+ + {(user) => } + +
+ ); +} +``` + +## 插槽(children) + +通过 `children` 实现灵活的内容插入。 + +```tsx filename="PanelWithSlot.jsx" +function Panel({ title, children }) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function App() { + return ( + +

这是插槽内容

+ +
+ ); +} +``` + +## 复合组件模式 + +将多个子组件组合为一个整体。 + +```tsx filename="TabsComponent.jsx" +function Tabs({ children }) { + // 省略 tab 切换逻辑 + return
{children}
; +} + +function Tab({ label, children }) { + return ( +
+

{label}

+
{children}
+
+ ); +} + +function App() { + return ( + + 内容A + 内容B + + ); +} +``` + +## 高阶组件(HOC) + +高阶组件用于复用逻辑或增强组件功能。 + +```tsx filename="HigherOrderComponent.jsx" +function withLogger(Component) { + return function Wrapper(props) { + console.log('props:', props); + return ; + }; +} + +const LoggedPanel = withLogger(Panel); +``` + +## 最佳实践 + +- 组件应职责单一,便于组合和复用 +- 善用 children 和 props 传递内容和行为 +- 复合组件可提升灵活性 + +## 注意事项 + +- 避免过度嵌套,保持结构清晰 +- 插槽内容建议通过 children 传递 + +## 相关链接 + +- [Props 传递](./props) +- [上下文](./context) \ No newline at end of file diff --git a/src/app/docs/components/context/page.mdx b/src/app/docs/components/context/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..e7e58765bcc18349c06a3b0eb9ff62e613433133 --- /dev/null +++ b/src/app/docs/components/context/page.mdx @@ -0,0 +1,90 @@ +--- +id: context +title: 上下文 +sidebar_label: 上下文 +--- + +# 上下文(Context) + +openInula 2(zouyu)支持类似 React 的 Context 机制,用于在组件树中跨层级传递数据,避免 props 层层传递。Context 机制包括 Provider(提供者)和 Consumer(消费者),并且支持响应式更新。 + +## 基本用法 + +### 1. 创建 Context + +```tsx filename="createContext.jsx" +import { createContext } from 'openinula'; + +const UserContext = createContext({ level: 0, path: '' }); +``` + +### 2. Provider(提供者) + +通过将 context 作为组件标签包裹子组件,并传递属性来提供数据: + +```tsx filename="ContextProvider.jsx" +function App() { + let level = 1; + let path = '/home'; + return ( + + + + ); +} +``` + +- context 属性可以是任意响应式变量,变化时会自动通知所有消费组件。 + +### 3. Consumer(消费者) + +在子组件中通过 `useContext` 获取 context 数据: + +```tsx filename="ContextConsumer.jsx" +import { useContext } from 'openinula'; + +function Child() { + const { level, path } = useContext(UserContext); + return
{level} - {path}
; +} +``` + +- 解构 useContext 返回的对象即可获得 context 的所有属性。 +- context 属性变化时,消费组件会自动响应更新。 + +## 响应式原理 + +- context 属性是响应式的,父组件更新属性,所有 useContext 的子组件会自动重新渲染。 +- 编译器会自动为每个属性生成依赖追踪和更新函数。 + +## 只消费部分属性 + +可以只消费 context 的部分属性,只有被消费的属性变化时才会触发更新。 + +```tsx filename="PartialContextConsumer.jsx" +function OnlyLevel() { + const { level } = useContext(UserContext); + return {level}; +} +``` + +## 多层嵌套与多 context + +可以嵌套多个 context,useContext 会自动查找最近的 Provider。 + +## 最佳实践 + +- 用于全局配置、用户信息、主题等场景 +- 避免频繁变化的小数据放入 context +- 推荐解构useContext返回值,帮助响应式系统能够只在对应 key 更新时,仅更新对应 value 的消费组件,提升性能 + +## 注意事项 + +- context 属性必须通过 Provider 传递,且应为响应式变量 +- useContext 只能在函数组件中调用 +- context 变化会导致所有消费组件重新渲染 + +## 相关链接 + +- [组件组合](../composition) +- [官方测试用例参考](https://github.com/openinula/openinula/blob/main/next-packages/compiler/babel-inula-next-core/test/e2e/context.test.tsx) \ No newline at end of file diff --git a/src/app/docs/components/props/page.mdx b/src/app/docs/components/props/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f7c2de62b3501205295c00e35f968f8b51cf8fdb --- /dev/null +++ b/src/app/docs/components/props/page.mdx @@ -0,0 +1,125 @@ +--- +id: props +title: Props 传递 +sidebar_label: Props 传递 +--- + +# Props 传递 + +在 openInula 2(zouyu)中,props 用于父组件向子组件传递数据和回调函数,语法与 React 类似,但更贴近原生 JS。 + +## 基本用法 + +```tsx filename="Welcome.jsx" +function Welcome({ name }) { + return

你好,{name}!

; +} + +function App() { + return ; +} +``` + +## 默认值与解构 + +可以为 props 设置默认值,并使用解构赋值: + +```tsx filename="UserProfile.jsx" +function UserProfile({ name = '匿名', age = 0 }) { + return ( +
+

姓名:{name}

+

年龄:{age}

+
+ ); +} +``` + +## 传递回调函数 + +父组件可以通过 props 向子组件传递事件处理函数,实现子组件向父组件通信: + +```tsx filename="CounterWithCallback.jsx" +function Counter({ onIncrement }) { + return ; +} + +function App() { + let count = 0; + function handleIncrement() { + count++; + } + return ( +
+

计数:{count}

+ +
+ ); +} +``` + +## 传递对象和数组 + +可以直接传递对象、数组等复杂数据类型: + +```tsx filename="TodoList.jsx" +function TodoList({ todos }) { + return ( +
    + + {(todo) =>
  • {todo.text}
  • } +
    +
+ ); +} + +function App() { + let todos = [ + { text: '学习 OpenInula' }, + { text: '写文档' } + ]; + return ; +} +``` + +## children 插槽 + +通过 `children` 实现插槽功能: + +```tsx filename="PanelWithChildren.jsx" +function Panel({ title, children }) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function App() { + return ( + +

这是插槽内容

+
+ ); +} +``` + +## 最佳实践 + +- 使用解构赋值提升代码可读性,并帮助响应式系统能够只在对应 props key 更新时,更新组件 +- 为 props 设置合理的默认值 +- 只传递必要的数据,避免 props 过多 +- 通过回调函数实现父子通信 + +## 注意事项 + +- props 是只读的,不要在子组件内部修改 props +- children 也是 props 的一部分 +- 复杂数据建议使用不可变数据结构 + +## 下一步 + +- 学习[组件组合](../composition)的高级用法 +- 了解[上下文](../context)的全局数据传递 +- 探索[生命周期](../lifecycle/mounting)的管理 \ No newline at end of file diff --git a/src/app/docs/conventional/CodeOfConduct/page.mdx b/src/app/docs/conventional/CodeOfConduct/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..ae57632ebc58f49982b1958d47097b0d4c836bcd --- /dev/null +++ b/src/app/docs/conventional/CodeOfConduct/page.mdx @@ -0,0 +1,53 @@ +--- +sidebar_position: 6 +--- + +我们有一份 openinula 代码贡献行为准则,希望所有的贡献者都能遵守,请花时间阅读一遍全文以确保你能明白哪些是可以做的,哪些是不可以做的。 + +openinula代码贡献行为准则遵守开源社区[《贡献者公约》](https://contributor-covenant.org/) V1.4 中规定的行为守则,请参考[V1.4版本](https://www.contributor-covenant.org/zh-cn/version/1/4/code-of-conduct.html) + +如需举报侮辱、骚扰或其他不可接受的行为,您可以发送邮件至[team@inulajs.org](mailto:team@inulajs.org),联系 openinula 项目部处理。 + +## 贡献者们的承诺 + +为建设开放友好的环境,我们贡献者和维护者承诺:不论年龄、体型、身体健全与否、民族、性征、性别认同与表征、经验水平、教育程度、社会地位、国籍、相貌、种族、信仰、性取向,我们项目和社区的参与者皆免于骚扰。 + +## 我们的准则 + +有助于创造积极环境的行为包括但不限于: + +* 措辞友好且包容 +* 尊重不同的观点和经验 +* 耐心接受有益批评 +* 关注对社区最有利的事情 +* 与社区其他成员友善相处 + +参与者不应采取的行为包括但不限于: + +* 发布与性有关的言论或图像、不受欢迎地献殷勤 +* 捣乱/煽动/造谣行为、侮辱/贬损的评论、人身及政治攻击 +* 公开或私下骚扰 +* 未经明确授权便发布他人的资料,如住址、电子邮箱等 +* 其他有理由认定为违反职业操守的不当行为 + +## 我们的义务 + +项目维护者有义务诠释何谓“妥当行为”,并妥善公正地纠正已发生的不当行为。 + +项目维护者有权利和义务去删除、编辑、拒绝违背本行为标准的评论(comments)、提交(commits)、代码、wiki 编辑、问题(issues)等贡献;项目维护者可暂时或永久地封禁任何他们认为行为不当、威胁、冒犯、有害的参与者。 + +## 适用范围 + +本行为标准适用于本项目。当有人代表本项目或本社区时,本标准亦适用于此人所处的公共平台。 + +代表本项目或本社区的情形包括但不限于:使用项目的官方电子邮件、通过官方媒体账号发布消息、作为指定代表参与在线或线下活动等。 + +代表本项目的行为可由项目维护者进一步定义及解释。 + +## 贯彻落实 + +可以致信[team@inulajs.org](mailto:team@inulajs.org),向项目团队举报滥用、骚扰及不当行为。 + +维护团队将审议并调查全部投诉,妥善地予以必要的回应。项目团队有义务保密举报者信息。具体执行方针或将另行发布。 + +未切实遵守或执行本行为标准的项目维护人员,经项目负责人或其他成员决议,可能被暂时或永久地剥夺参与本项目的资格。 \ No newline at end of file diff --git a/src/app/docs/conventional/ContributingGuide/page.mdx b/src/app/docs/conventional/ContributingGuide/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..132a283920b32a0ed91b6aad5afa7c1da5d1cab0 --- /dev/null +++ b/src/app/docs/conventional/ContributingGuide/page.mdx @@ -0,0 +1,142 @@ +--- +sidebar_position: 7 +--- + +本指南会指导你如何为 openInula 贡献自己的一份力量,请你在提出 issue 或 pull request 前花费几分钟来了解 openInula 社区的贡献指南。 + +## 行为准则 + +我们有一份 openInula 代码贡献[行为准则](/docs/行为准则),希望所有的贡献者都能遵守,在参与社区贡献之前,请先阅读并遵守 openInula 的[行为准则](/docs/行为准则)。 + +## 参与贡献 + +贡献代码之前,您必须签署一份“openInula贡献者协议,然后才能参与代码贡献。根据您的参与身份,选择签署个人 CLA、员工 CLA 或企业 CLA,请点击**[这里](https://gitee.com/organizations/inula-js/cla/inula-js-contributor-protocol)**签署。 + +* 个人 CLA:以个人身份参与社区,请签署个人 CLA +* 企业 CLA: 以企业身份参与社区,请签署企业 CLA +* 员工 CLA: 以企业员工的身份参与社区,请签署员工 CLA + +#### 贡献流程 + +* 我们所有的工作都会放在 [Gitee](https://gitee.com/openInula) 上。 +* 针对Git的安装、环境配置及使用方法,请参考码云帮助中心的 Git 知识大全:[https://gitee.com/help/categories/43](https://gitee.com/help/categories/43) +* openInula 长期维护 master 分支。如果你要修复一个 Bug 或增加一个新的功能,那么请 Pull Request 到 master 分支上 + +#### 参与贡献 + +openInula 团队会关注所有 Pull Request,我们会经测试与评审后合入你的代码,也有可能要求你做一些修改或者告诉你我们为什么不能接受你的修改。 + +在你发送Pull Request之前,请确认你是按照下面的步骤来做的: + +1. 确保基于正确的分支进行修改。 +2. 在项目根目录下运行了 `npm install`。 +3. 如果你修复了一个bug或者新增了一个功能,请确保新增或完善了相应的测试,这很重要。 +4. 确认所有的测试都是通过的 `npm run test` +5. 确保你的代码通过了lint检查 `npm run lint`. + +#### 贡献流程 + +1. Fork 代码分支 + + 1. 找到并打开对应 Repository 的首页; + 2. 点击右上角的 **Forked** 按钮,按照指引,建立一个属于个人的分支。 + +2. 把 Fork 仓下载到本地 + + 1. 创建本地工作目录; + ```bash + mkdir ${your_working_dir} + cd ${your_working_dir} + ``` + + 2. 复制远程仓库到本地。 + ```bash + git clone {$remote_link} + ``` + +3. 代码修改 + + 1. 根据您的想法,修复bug或者新增功能; + + 2. 推送到您的线上代码仓分支。在此之前,请确认您已完成一下步骤: + 1. 确保基于正确的分支进行修改。 + 2. 在项目根目录下运行了 `npm install`。 + 3. 如果你修复了一个 bug 或者新增了一个功能,请确保新增或完善了相应的测试,这很重要。 + 4. 确认所有的测试都是通过的 `npm run test` + 5. 确保你的代码通过了 lint 检查 `npm run lint`. + + 确认无误后即可推送 + ```bash + git push origin master + ``` + +4. 提交 Pull Request + + 1. 在代码仓首页点击 **+Pull Request**,创建一个新PR + 2. 选择分支为您修改过的分支 + 3. 补充标题与 PR 描述等信息。openInula 库提供 PR 模板,帮助您快速高效的传递 PR 的描述信息。 + 4. 确认无误后,点击**创建 Pull Request** + + 提交完毕后,请耐心等待 openInula 团队完成测试与评审流程。流程通过后,您的代码就会被合入 openInula 的代码仓中。 + + 如果你还不清楚怎么在Gitee上提交 Pull Request,你可以通过[这篇文章](https://oschina.gitee.io/opensource-guide/guide/%E7%AC%AC%E4%B8%89%E9%83%A8%E5%88%86%EF%BC%9A%E5%B0%9D%E8%AF%95%E5%8F%82%E4%B8%8E%E5%BC%80%E6%BA%90/%E7%AC%AC%207%20%E5%B0%8F%E8%8A%82%EF%BC%9A%E6%8F%90%E4%BA%A4%E7%AC%AC%E4%B8%80%E4%B8%AA%20Pull%20Request/#%E4%BB%80%E4%B9%88%E6%98%AF-pull-request)学习 + + +### 门禁构建 + +#### 创建 Issue + +1. 找到并打开对应 Repository 的首页 +2. 选择 Issues 页签。 +3. 在点击右侧 **+新建 Issue** 按钮,建立一个专属的任务,用于相关联的代码(开发特性/修改 bug)执行 CI 门禁。 + +#### 将 Issue 与 PR 关联 + +创建 PR 或编辑已有的 PR 时,描述框输入 `#I[五位 Issue ID]`,例如 `#I12345`,即可将 Issue 与 PR 关联。 + +#### 约束 +- 一个 PR 只允许关联一个 Issue,关联多个 Issue 时无法触发CI。 +- 相关特性开发或 bug 修复涉及多个代码仓联合修改时,多个 PR 可关联同一个 Issue。 +- Issue 关联的 PR 中,不允许存在已被合入或关闭的 PR,否则无法触发 CI。 +- 若 Issue 已被合入或关闭的 PR 关联,则该 Issue 无法被重复使用,需重新创建 Issue 并进行 OPEN 的 PR 关联。 +- 当你想开始处理一个 Issue 时,先检查一下 Issue 下面的留言,确保没有其他人正在处理。如果没有,你可以留言告知其他人你将处理这个 Issue,避免重复劳动。 + + +#### 触发代码门禁 + +在 PR 中评论“start build”即可触发 CI 门禁。 + +多个 PR 关联同一个 Issue 时,在任一 PR 中评论“start build”均可触发该 Issue 的 CI 门禁。 + +门禁执行完成,会在该 Issue 关联的所有 PR 中自动评论门禁执行结果。 + +如果门禁通过,该 Issue 关联的所有PR均会自动标记“测试通过”。 + +#### Bug 提交 + +我们使用 Gitee Issues 来进行 Bug 跟踪。在你发现 Bug 后,请通过我们提供的模板来提 Issue,以便你发现的 Bug 能被快速解决。 + +在你报告一个 bug 之前,请先确保不和已有 Issue 重复以及查阅了我们的用户使用指南。 + +#### 新增功能 + + 如果你有帮助我们改进 API 或者新增功能的想法,我们同样推荐你使用我们提供 Issue 模板来新建一个添加新功能的 Issue。 + +### CI 门户 + +openInula 通过持续集成(CI,Continuous Integration)及时发现代码问题,确保代码质量可靠和功能稳定,包括: + +* 代码门禁:开发者向 openInula 提交代码合入申请后,会触发门禁检查,例如静态检查、代码编译、功能测试等,门禁通过后才能合入代码。 +* 每日构建:openInula 的持续集成流水线每日自动执行,以便提前发现代码静态检查、编译、功能等方面的问题,及时修复问题,确保代码质量。 + +CI 门户是为了便于开发者及时查看、分析每日构建和代码门禁的执行结果的一个可持续集成的门户。 + +## 内容版权 + +用户提交的内容、图片必须是原创内容,不得侵犯他人知识产权。 + +对应采纳的内容,openInula 有权根据相关规范修改用户提交的内容。 + +## License + +openInula 主要遵循 Mulan Permissive Software License v2 协议,详情请参考各代码仓 LICENSE 声明。 \ No newline at end of file diff --git a/src/app/docs/conventional/FAQ/page.mdx b/src/app/docs/conventional/FAQ/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..19f2526b79777bd9e11a330adf44affd41e02883 --- /dev/null +++ b/src/app/docs/conventional/FAQ/page.mdx @@ -0,0 +1,35 @@ +--- +sidebar_position: 5 +--- + +## openInula 支持哪些浏览器? + +最新版的 openInula 支持原生支持 ES2015 的浏览器,不包括 IE 浏览器。 + +--- + +## openInula 可以扩展吗? + +是的,openInula 是一个成熟的完全的前端框架,其不仅可无缝兼容 React 框架,也可以基于 openInula 库,扩展一些生态能力: + + * inula-intl 提供了一个模块化的国际化框架,允许其和 openInula 框架配合使用 + * inula-request 提供了请求能力,允许其和 openInula 框架配合使用 + * inula-router 提供了路由能力,允许其和 openInula 框架配合使用 + +--- + +## openInula 的进入门槛如何? + +openInula 的进入门槛较低,详细的文档以及简洁的代码逻辑能减少新前端开发人员的学习成本。 + +--- + +## openInula 中应该使用 JavaScript 还是 TypeScript? + +openInula 本身是基于TypeScript实现的,提供了一流的 TypeScript 支持,但不会对您是否应该使用 TypeScript 作为用户强制执行意见。在 openInula API 实现中也尽可能在 JavaScript 与 TypeScript 进行折中选择。 + +--- + +## openInula 是轻量级的吗?性能如何? + +openInula 是轻量级的。其提供的响应式 API 性能相比与主流框架 React 的虚拟 DOM 方式,渲染效率提升了30%以上。 \ No newline at end of file diff --git a/src/app/docs/conventional/QuickStart/page.mdx b/src/app/docs/conventional/QuickStart/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..81b23bf750eb881cd2abbfea4ffe0c3ca9c5d085 --- /dev/null +++ b/src/app/docs/conventional/QuickStart/page.mdx @@ -0,0 +1,94 @@ +欢迎学习 openInula 教程。此教程将会指导你使用 openInula 构建快速、轻便的 web 应用程序。 + +## 什么是openInula + +openInula 是一款用于构建用户界面的 JavaScript 库,提供响应式 API 帮助开发者简单高效构建 web 页面,比传统虚拟 DOM 方式渲染效率提升30%以上!同时 openInula 提供与 React 保持一致的 API,并且提供5大常用功能丰富的核心组件:**状态管理器**、**路由**、**国际化**、**请求组件**、**应用脚手架**,以便开发者高效、高质量的构筑基于 openInula 的前端产品。 + +### 在已有项目中安装 openInula + +#### 方式一:使用 `npm` 安装 + +在命令行中运行以下命令来通过 `npm` 安装 openInula: + +```sh filename="install.sh" +npm install openinula +``` + +上述命令会将最新版本的 openInula 包添加到您的项目中,当然,您也可以指定特定版本来安装包。最新版本号请在 npm 官网查看:[访问官网](https://www.npmjs.com/package/openinula) + +#### 方式二:使用 `yarn` 安装 + +openInula 也支持 `yarn` 命令安装,前提已安装了 `yarn`,如未安装,则通过 `npm install -g yarn` 命令来安装(全局安装)。 + +在命令行中运行以下命令来通过 `yarn` 安装 openInula: + +```sh filename="install.sh" +yarn add openinula +``` + +### 直接创建 openInula 项目 + +如果你已经安装了openInula,则可以简单地创建一个新的 JavaScript 文件,尝试使用 openInula,例如 `index.jsx`,并在其中添加以下代码: + +```jsx filename="index.jsx" +import { render } from 'openinula'; + +const HelloWorld = () => { + return ( +
+

Hello, World!

+
+ ); +}; + +render( + , + document.getElementById('root') +); +``` + +上述代码片段主要分为三部分,首先通过 `import` 语法导入 openInula 包,其次构建一个名为 `HelloWorld` 的函数组件,最后将 `HelloWorld` 组件挂载在 HTML 中 id 为 `root` 的 DOM元素上,详细使用请查阅[API参考Inula](https://docs.openinula.net/apis/Inula)。`index.jsx` 经过 babel 编译后,您将看到一个显示 "Hello, World!" 的页面。 + +### 使用脚手架创建openInula项目 + +#### 步骤一: 启动openInula脚手架 + +在您需要创建项目的目录下执行以下命令: + +```sh filename="create.sh" +npx create-inula <项目名> +``` + +#### 步骤二: 选择需要项目模板 + +执行启动命令后,您将收到以下回显信息询问,可根据回显信息进行相应的输入: + +```sh filename="select-template.txt" +Need to install the following packages:create-inula@0.0.2 +Ok to proceed? (y) y +? Please select the template (Use arrow keys) +> Simple-app +``` + +在创建项目过程中脚手架提供 Simple-app 模板供开发者使用: +- Simple-app 已默认安装 openInula,开发者可以直接在项目中专注于核心代码的开发。 + +#### 步骤三:选择打包方式 + +在创建项目过程中有两种打包方式供选择,您可以根据自己使用习惯选择: + +```sh filename="select-build.txt" +? Please select the build type (Use arrow keys) + > webpack + vite +``` + +如果您不知如何选择可分别参考 [Vite 文档](https://cn.vitejs.dev/) 以及 [webpack文档](https://webpack.js.org/)。 + +至此,可以使用 openInula 框架,通过 `npm run start` 命令运行项目,您会看到简单的 openInula 的示例。当然,您也可以基于 openInula 框架构建您的 web 项目。 + +### 开始使用openInula + +恭喜!您已经成功安装了 openInula。现在您可以根据项目需求自由使用 openInula 提供的组件和功能。 + +请查阅[《用户指南》](/docs/用户指南)以了解更多关于如何使用和配置框架的详细信息。 diff --git a/src/app/docs/conventional/ReleaseNotes/page.mdx b/src/app/docs/conventional/ReleaseNotes/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..45bd6441baeafc473a6e0c6da11aa6fa1358d70c --- /dev/null +++ b/src/app/docs/conventional/ReleaseNotes/page.mdx @@ -0,0 +1,24 @@ +--- +sidebar_position: 4 +--- +## 配套关系 + +表1 版本软件和工具配套关系 + +| 软件 | 版本 | 备注 | +|--------------|--------|----| +| inula | 0.0.11 | | +| create-inula | 1.0.11 | | + +## 新特性 + +**openInula** + +* 支持响应式 API(9月底发布β版本) +* 支持状态管理器 inulaX +* 兼容 React API,支持类式组件和函数式组件 + +**create-inula** + +- 支持选择 Simple-app 模板 +- 项目模板支持 webpack 和 vite 两种打包方式 \ No newline at end of file diff --git a/src/app/docs/conventional/SwitchingGuide/page.mdx b/src/app/docs/conventional/SwitchingGuide/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..73cf5db79b5ebd6cf30c02c65dcf77e260bcc7df --- /dev/null +++ b/src/app/docs/conventional/SwitchingGuide/page.mdx @@ -0,0 +1,180 @@ +--- +sidebar_position: 3 +--- + +为了便于开发者使用 openInula 进行页面开发,尤其是已经使用 React 的相关项目,openInula 提供了一种无感切换机制,可以从 React 切换到 openInula。您只需要按照如下的方式进行修改,就可以体验到 openInula 框架所提供的能力。 + +### 使用 React 的项目切换 openInula 步骤 + +- 步骤1:安装 openInula。 + +```bash filename="install.sh" +npm install openinula +``` + +最新版本号请在 npm 官网查看:[访问官网](https://www.npmjs.com/package/openinula) + +- 步骤2:引用切换,修改 openInula 相关 API 的加载方式。 + +```ts +// 修改前: +import React, { useContext, useEffect, Suspense, lazy, useState } from 'react'; +import ReactDOM from 'react-dom'; + +// 修改后: +import React, { useContext, useEffect, Suspense, lazy, useState } from 'openinula'; +import ReactDOM from 'openinula'; +``` +*注意*:修改后可以保留 React 全局变量。 + +- 步骤3:修改 webpack 配置文件中 `alias`,新增 `react`、`react-dom` 等别名,如下: + +```ts filename="webpack.config.js" +alias: { + // 省略其它... + 'react': 'openinula', // 新增 + 'react-dom': 'openinula', // 新增 + 'react-is': 'openinula', // 新增 +}, +``` + +### 切换FAQ + +#### openInula 编译 FAQ + +1. 在切换或者使用 openInula 框架时,`React.createElement` 无法转换成 `openinula.createElement`: + + 请排查编译命令中是否有 `--mode development`,该选项会导致 `React.createElement` 无法转换成 `openinula.createElement`,需要删除。 + +2. 如果在 webpack 配置文件中配置 `externals` 字段,需把 React 相关三方件的 `externals` 都替换,如下: + + ```ts filename="webpack.config.js" + externals: { + 'react': 'openinula', + 'react-dom': 'openinula', + 'react-redux': 'openinula', + 'react-thunk': 'openinula', + }, + ``` + +3、如果使用 babel,则需要修改 `.babelrc` (`babel.config.js`) 或 webpack 配置文件中 `babel-loader` 配置(解决编译后文件存留 `React.createElement` 的问题),如: + +- 如果你使用的是 `@babel/preset-react`,请确保版本号大于 `7.9.0` + + ```jsx + // 修改前: + { + "presets": [ + "@babel/preset-react" + ] + } + + // 修改后 + { + "presets": [ + [ + "@babel/preset-react", + { + "runtime": "automatic", // 新增 + "importSource": "openinula" // 新增 + } + ] + ] + } + ``` + +- 如若使用了 `@babel/plugin-transform-react-jsx`,请确保版本号大于 `7.9.0` + + ```jsx + // 修改前 + { + "plugins": [ + "@babel/plugin-transform-react-jsx" + ] + } + + // 修改后: + { + "plugins": [ + [ + "@babel/plugin-transform-react-jsx", + { + "runtime": "automatic", // 新增 + "importSource": "openinula" // 新增 + } + ] + ] + } + ``` + + > 注意:**`.babelrc` (`babel.config.js`)** 和 **webpack配置文件** 不要重复配置。 + +--- + +#### 不兼容 React 变更 FAQ + +1. openInula 不支持 Legacy Context,如:https://reactjs.org/docs/legacy-context.html ,请修改成new Context API,参考:https://reactjs.org/docs/context.html 。 + +2. openInula 不支持在 Class 组件中使用 `this.refs` + + ```jsx filename="InputFocusExample.jsx" + class InputFocusExample extends Component { + focusInput = () => { + this.refs.myInput.focus(); // 不支持 this.refs + }; + + render() { + return ( +
+ + +
+ ); + } + } + + // 请修改成以下新的 Ref API 方式: + class InputFocusExample extends Component { + constructor(props) { + super(props); + this.myInput = React.createRef(); // 新的 Ref API + } + + focusInput = () => { + this.myInput.current.focus(); // 通过 this.myInput.current 引用 + }; + + render() { + return ( +
+ + +
+ ); + } + } + ``` + +--- + +#### 测试框架 FAQ + +1. openInula 如何与 `@testing-library/react` 配套使用? + + openInula 支持与 React 提供的测试框架配合使用,方法配置简便,需要在 `jest.config.js` 中需要加入如下配置: + + ```js filename="jest.config.js" + module.exports = { + moduleNameMapper: { + "^react$": "/node_modules/openinula", + "^react-dom": "/node_modules/openinula", + "^react/jsx-runtime": "/node_modules/openinula/jsx-runtime" // 非必须,如存在 require("react/jsx-runtime") 这种引入方式才需要 + }, + }; + ``` + + > 注意:`"^react/jsx-runtime": "rootDir/node_modules/openinula/jsx-runtime"`是非必须添加,若存在 `require("react/jsx-runtime")` 这种引入方式才需要 + +2. openInula 项目如何使用 `react-test-renderer` 测试框架? + + openInula 不支持 `react-test-renderer` 这个测试框架,如果使用到需要自行修改。因为 `react-test-renderer` 用到了 React 的 `React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED` 接口,而 openInula 没有该接口。 \ No newline at end of file diff --git a/src/app/docs/conventional/UserInstruction/figure.png b/src/app/docs/conventional/UserInstruction/figure.png new file mode 100644 index 0000000000000000000000000000000000000000..ba5a2f6044ecde43edaf33dd100a1d1411a15dbb Binary files /dev/null and b/src/app/docs/conventional/UserInstruction/figure.png differ diff --git a/src/app/docs/conventional/UserInstruction/page.mdx b/src/app/docs/conventional/UserInstruction/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..cb988921ba3c4d2741b3e2025ffc1f29b0d455fa --- /dev/null +++ b/src/app/docs/conventional/UserInstruction/page.mdx @@ -0,0 +1,745 @@ +--- +sidebar_position: 2 +--- + +# 用户指南 + +openInula 是一款构建用户界面的 JavaScript 前端框架。本篇文章会对 openInula 的组件及生命周期、数据管理、事件绑定等知识进行详细介绍,相信通过本章学习您将掌握到 openinula 的基本知识和使用方法。 + +## 开始使用 + +### JSX + +JSX 是 JavaScript XML 的缩写,是一种用于构建用户界面的语法扩展。在 openInula 中,JSX 被广泛使用。开发者可以使用 JSX 来描述 UI 组件的结构和样式,并将其与 JavaScript 代码逻辑相结合。然后使用转译工具(例如`Babel`)将 JSX 代码转换为普通的 JavaScript 代码,以便浏览器能够理解并渲染。 + +- JSX 是类 HTML 语言,其除了拥有 HTML 的功能外,还可以通过括号`{}`嵌套 JavaScript 表达式,这使得 JSX 更加灵活,可以动态地生成UI组件; +- JSX 支持双向数据绑定,使得数据和界面之间的更新更加简单。当数据发生变化时,会智能地更新必要部分。HTML 没有直接支持数据绑定的功能; +- 每个 JSX 必须返回独有元素。如果要返回多个顶级元素,请使用 `Fragment` 元素或者 <>。 + +```jsx filename="fragment-example.jsx" +<> +

Hello, {props.name}!

; + + + +``` + +### 组件 + +组件是 openInula 的核心概念之一。它们是构建用户界面(UI)的基础,是可重用和独立的代码单元。开发者可以将相关功能、状态以及模板封装在组件中,提高可复用性,使得程序开发具备模块化和可维护特性。下面我们将展开介绍。 + +#### 组件的设计 + +组件不是 openInula 特有的概念,但是组件的设计思想会是优秀应用开发的基础。接下来我们就谈一谈组件设计的奥秘。 + +openInula 组件思想具有普适性,无论是在其他框架还是原生 Web component 都适用。组件与我们之前学到的HTML的元素非常相似,如将一个 UI 界面分成独立的可重复复用的单独的部分,每个部称之为一个组件,各个组件之间相互交互以实现基本功能。 + +![image](./figure.png) + +如果您已经了解使用组件会给您的程序带来的优点,如何设计一个好的组件将是您接下来关注的问题,openInula 框架认为一个好的组件会遵循以下原则: + +1. 单一职责:组件只应该做一件事情。单一职责带来最大好处就是在修改组件时,不必担心对其他组件造成影响。 +2. 开闭原则:一个组件应该对扩展开放,对修改关闭,即通过扩展组件的行为来实现功能的变化,而不是修改组件的源代码。 + +上述原则可以帮助开发者设计出高内聚、低耦合、可扩展、可维护的组件。如下示例是我们展示如何使用设计原则来实现一个安全可靠的组件。 + +```jsx filename="button-form-page.jsx" +import Inula, { render, Component } from 'openinula'; + +// 示例组件 - 按钮组件 +class Button extends Component { + render() { + const { text, onClick } = this.props; + return ( + + ); + } +} + +// 示例组件 - 表单组件 +class Form extends Component { + handleSubmit = (event) => { + event.preventDefault(); + // 处理表单提交逻辑 + } + + render() { + return ( +
+ +
+ + ); + }; + }; + ``` + +2. 函数组件 + + 函数组件是以函数形式呈现,可以接收一个属性(`props`)作为输入,并返回一个 JSX 元素。函数组件的代码量更少,易于阅读和理解。相较于类组件,没有繁琐的类声明和生命周期方法,更加直观和简洁。此外,函数组件没有实例化的过程,在一些情况下,函数组件比类组件具有更好的性能。 + + ```jsx filename="picture-greeting.jsx" + function Picture() { + return ( + + ); + }; + + const Greeting = (props) => { + return ( +
+

Hello, {props.name}!

+ +
+ ); + }; + ``` + +此外,无论是函数组件还是类组件,组件之间可以进行嵌套,实现更复杂的代码逻辑。在上述函数组件示例中包含了两个组件,`Greeting`为父组件,`Picture`是它的子组件。 + +> 注意: +> 1. 如果定义组件,则首字母需要大写,以区分 HTML 标签; +> 2. 在渲染JSX元素时,`return` 时需要通过括号`()`包含多个组件,否则 `return` 中除首行代码外都会被忽略。 + +#### 组件的渲染 + +我们已经介绍了函数组件和类组件,下一步我们将思考如何能将创建的组件展示到界面?首先我们创建了一个 `index.jsx` 文件,使用 `document.getElementById('root')` 表示获取HTML文档中具有id为 `root` 的元素作为挂载点,进而在浏览器中展示 `Counter` 组件内容。 + +```jsx filename="app.jsx" +import { render } from 'openinula'; +import { Counter } from './Counter.js' + +function App() { + render( + , + document.getElementById('root') + ); +}; +``` + +### 组件间通信 + +#### Props(属性) + +Props(属性)是组件之间传递数据的一种机制。Props 是从父组件向子组件传递的不可变对象。Props 的参数传递是一种单向传递,即从父组件单向传递子组件,因此 Props 是一种只读属性,子组件只能读取父组件传递的 Props,但却不能修改值。在如下示例中,我们创建一个父组件 `ParentComponent`,通过 Props 机制将参数传递给子组件 `ChildComponent`。 + +```jsx filename="parent-child-component.jsx" +function ParentComponent() { + return ( + + ); +} + +// 类组件 +class ChildComponent extends Component { + render() { + return ( +
+

Name: {this.props.name}

+

Age: {this.props.age}

+

Gender: {this.props.gender || 'Unknown'}

+
+ ); + } +} + +// 函数组件 +function ChildComponent(props) { + return ( +
+

Name: {props.name}

+

Age: {props.age}

+

Gender: {props.gender || 'Unknown'}

+
+ ); +} +``` + +在上述代码中,子组件 `ChildComponent` 通过 Props 机制可以获取到父组件传入的 `props` 并获取 `name`、`age` 以及 `gender` 等值。 + +```jsx +

Name: {props.name}

+``` + +有些读者可能已经发现,父组件传入的 `props` 中没有 `gender` 属性。但是如果子组件执意使用时,也可传入默认值。在上述例子中,当父组件没有传入对应属性 `props.gender` 时,可以设置默认值为`Unknown`,如果没有设置对应属性的默认值,则获取到的值为 `undefined`。 + +#### State(状态) + +openInula 中 State(状态) 是一种用于存储和管理组件内部数据的机制。它是一个对象,包含了组件的可变数据。当状态发生变化时会自动重新渲染组件,以反映最新的状态。 + +在介绍组件的分类时,我们在[类组件](#组件的分类)中定义了 `Counter` 组件让读者更直观地感受类组件特点,在 `Counter` 类组件中,我们使用 `this.state = { count: 0 }` 定义了计数器的初始状态值,并通过 `setState` 函数进行修改状态。但是这只是针对类组件中的状态的设置以及修改,如果在函数组件中,我们又如何使用呢? + +下面示例中我们将类组件 `Counter` 改写写成函数组件,用 `useState` 钩子函数来声明管理组件的状态,因此代码也可以写为: + +```jsx filename="counter-func.jsx" +import Inula, { useState } from 'openinula'; + +function Counter() { + const [count, setCount] = useState(0); + + const increment = () => { + setCount(count + 1); + }; + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + +`useState` 钩子用于定义名为 `count` 的状态变量,并使用初始值0进行初始化。同时返回的 `setCount` 函数用于更新 `count` 的值。当用户点击 `Increment` 按钮时,会触发 `increment` 函数并进一步调用 `setCount` 来增加计数器的值,并更新组件的状态。每次 `setCount` 被调用时会重新渲染 `Counter` 组件,并显示最新的计数值。 + +> Tips: `state` 与 `props`区别 +> `state` 是组件内部管理的可变数据,用于存储和更新组件的私有状态。而 `props` 是由父组件传递给子组件的不可变数据,用于组件之间的数据传递和通信。通过结合使用 `state` 和 `props`,可以实现动态的和可重用的组件,并在应用中管理和传递数据。 + +#### 组件通信的封装性 + +openInula 提倡开发者使用组件,对于单个组件来说,单一职责原则会保证组件的封装性,但是组件之间的应该存在必要的交互通信,才能相互协作协作,完整更复杂的功能逻辑。细心的读者已经发现在上述的例子中,我们已经使用 `props` 和 `state` 来进行组件的交互,但是如何在保证组件交互时同样具备封装性呢?下面我们通过一个错误案例以及对其的修改进行详细说明。 + +```jsx +// ParentComponent.js +import Inula, { Component } from 'openinula'; +import ChildComponent from './ChildComponent'; + +class ParentComponent extends Component { + constructor(props) { + super(props); + this.state = { + message: 'Hello', + }; + } + + handleChange = (e) => { + this.setState({ message: e.target.value }); + }; + + render() { + return ( +
+ + +
+ ); + } +} + +// ChildComponent.js +import Inula, { Component } from 'openinula'; + +class ChildComponent extends Component { + render() { + return
{this.props.message}
; + } +} +``` + +在上述示例中,`ParentComponent` 和 `ChildComponent` 是两个组件。然而,`ParentComponent` 直接将自己的状态(`message`)传递给了 `ChildComponent` 作为 `props` ,这违背了组件封装性的原则。 + +根据组件封装性的原则,一个组件应该尽可能地隐藏其内部实现细节,并通过 `props` 接口与外部进行通信。但在这个示例中,`ParentComponent` 将自己的状态直接传递给了 `ChildComponent`,导致 `ChildComponent` 依赖于 `ParentComponent` 的状态。 + +因此,我们在重构中,首先会考虑到以“粗暴、直接地”方式修改 `message` 值,并且考虑到组件的封装特性,保证组件只感知到自身的 `state`。我们可以将 `ChildComponent` 中的 `message` 属性改为通过 `props` 传递一个回调函数,然后在 `ParentComponent` 中定义这个回调函数,将 `message` 作为参数传递给它,最后在 `ChildComponent` 中调用这个回调函数来获取 `message`。 + +修改后的代码如下: + +```jsx +// ParentComponent.js +import Inula, { Component } from 'openinula'; +import ChildComponent from './ChildComponent'; + +class ParentComponent extends Component { + constructor(props) { + super(props); + this.state = { + message: 'Hello', + }; + } + + handleChange = (e) => { + this.setState({ message: e.target.value }); + }; + + render() { + return ( +
+ + this.state.message} /> +
+ ); + }; +} + + +// ChildComponent.js +import Inula, { Component } from 'openinula'; + +class ChildComponent extends Component { + render() { + return
{this.props.getMessage()}
; + }; +} + +export default ParentComponent; +``` + +这样,我们通过回调函数的方式,将 `message` 的获取封装在了 `ParentComponent` 中,`ChildComponent` 组件就不需要再知道 `ParentComponent` 组件的内部情况,保证了 `ChildComponent` 的封装性。 + +### 条件渲染与列表渲染 + +条件渲染与列表渲染是 openInula 中常用的功能,条件渲染的方式可以根据不同的情况来显示不同的内容,提高用户体验。列表渲染可以简化代码逻辑,更便捷的开发代码。因此,JSX 也满足在 openInula 中可以实现条件渲染以及列表渲染。 + +1. **条件渲染**:使用 if 条件语句或三元表达式来实现条件渲染。 + + ```jsx + function UserGreeting(props) { + return

Welcome back!

; + } + + function GuestGreeting(props) { + return

Please sign up.

; + } + + // 直接使用 if-else 语句 + function Example(props) { + const isLoggedIn = props.isLoggedIn; + if (isLoggedIn) { + return ; + } + return ; + } + + // 使用三元表达式 + function Example(props) { + const isLoggedIn = props.isLoggedIn; + return ( + {isLoggedIn ? : } + ); + } + ``` + + 在这个示例中我们定义了两种问候用户的组件 `` 与 ``,并希望根据用户是否登录 `isLoggedIn` 分别展示前后者。我们可以使用 if-else 语句分别返回不同的组件,也可以通过三元表达式择一返回。 + + +2. **列表渲染**:使用 for 循环或 `map` 来实现组件列表渲染。 + + ```jsx + function List(props) { + const items = props.items.map((item, index) => ( +
  • {item}
  • + )); + + return
      {items}
    ; + } + + function App() { + const items = ['Item 1', 'Item 2', 'Item 3']; + + return ( +
    +

    List Demo

    + +
    + ); + } + ``` + + 在这个示例中,我们定义了一个 `List` 组件,它接收一个 `items` 属性,该属性是一个字符串数组。我们使用 `map` 函数将每个字符串转换为一个 `
  • ` 元素,并将它们存储在一个 `items` 数组中。最后,我们将这个数组作为子元素传递给 `
      ` 元素,以渲染整个列表。 + +### 事件 + +openInula 中支持事件是为了实现交互性和动态性的用户界面,通过在组件中定义事件处理函数,可以将用户的操作与组件的状态和行为关联起来。当用户触发事件时,openInula 会调用相应的事件处理函数,并根据需要更新组件的状态或执行其他操作,从而实现动态的用户界面。 + +openInula组件感知到用户触发事件是通过 `onXxxx` 属性来监听 DOM 事件,并在事件触发时执行事件的处理函数。因此在 openInula 中必须定义一个事件处理函数,在使用时只需要传递事件即可。 + +> 事件的命名规则:on+驼峰形式事件名 = 事件处理函数 + +#### 事件处理函数的定义 + +事件处理函数其实就是一个函数,它被用来响应特定的事件。当事件被处理时,事件函数就会被触发。 + +```jsx +function handleClick() { + // 处理点击事件的逻辑 + console.log('Button clicked!'); +} +``` + +#### 事件处理函数的触发 + +在openInula中,我们使用`onXxxx`属性来绑定事件处理函数,例如,使用`onClick`属性来绑定点击事件。函数绑定为了确保事件处理函数可以正确地访问组件实例的上下文(`this`)。有几种方法可以实现函数绑定: + +1. 在构造函数中使用 `bind` 方法绑定函数 + + ```jsx + class App extends Component { + constructor(props) { + super(props); + this.handleClick = this.onHandleClick.bind(this); + } + + onHandleClick() { + // 处理点击事件的逻辑 + console.log('Button clicked!'); + }; + + render() { + return ; + }; + }; + ``` + +2. 使用箭头函数定义事件处理函数 + + ```jsx + class App extends Component { + handleClick = () => { + // 处理点击事件的逻辑 + console.log('Button clicked!'); + }; + + render() { + return ; + }; + }; + ``` + + > 箭头函数可以避免`this`指向问题,是因为箭头函数没有自己的`this`,它会继承外层函数的`this`。 + +#### openInula支持的事件 + +- Clipboard Events 剪切板事件 + - 事件名: `onCopy`, `onCut`, `onPaste` + - 属性: `DOMDataTransfer`, `clipboardData` +- compositionEvent 复合事件 + - 事件名: `onCompositionEnd`, `onCompositionStart`, `onCompositionUpdate` + - 属性: `string`, `data` +- Keyboard Events 键盘事件 + - 事件名: `onKeyDown`, `onKeyPress`, `onKeyUp` + - 属性: `number`, `keyCode` +- Focus Events 焦点事件 (这些焦点事件在 openinula DOM 上的所有元素都有效,不只是表单元素) + - 事件名: `onFocus`, `onBlur` + - 属性: `DOMEventTarget`, `relatedTarget` +- Form Events 表单事件 + - 事件名: `onChange`, `onInput`, `onInvalid`, `onSubmit` +- Mouse Events 鼠标事件 + - 事件名: `onClick`, `onContextMenu`, `onDoubleClick`, `onDrag`, `onDragEnd`, `onDragEnter`, `onDragExit`, `onDragLeave`, `onDragOver`, `onDragStart`, `onDrop`, `onMouseDown`, `onMouseEnter`, `onMouseLeave`, `, onMouseMove`, `onMouseOut`, `onMouseOver`, `onMouseUp` +- Pointer Events 指针事件 + - 事件名: `onPointerDown`, `onPointerMove`, `onPointerUp`, `onPointerCancel`, `onGotPointerCapture`, `onLostPointerCapture`, `onPointerEnter`, `onPointerLeave`, `onPointerOver`, `onPointerOut` + +### Hook + +Hook 是 openInula 目前提供的一种函数,Hook 函数可以让我们在函数组件中使用状态管理、生命周期方法、副作用等功能。常用的[钩子函数](/apis/Inula#钩子函数)包括 `useState`、`useEffect`、`useContext`、`useReducer`等。 + +当然,当 openInula 提供的 Hook 组件无法满足您的需求时,您可以自定义 Hook。在 openInula 中,自定义 Hook 函数的本质是一种重用状态逻辑思想。每一个 Hook 函数都是普通的函数。主要是其使用了 openInula 自带的 Hook API 等。下面我们就举例说明如何自定义一个 Hook 函数。 + +```jsx +import Inula, { useState, useEffect } from 'openinula'; + +function useFetch(url) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchData() { + const response = await fetch(url); + const data = await response.json(); + setData(data); + setLoading(false); + } + fetchData(); + }, [url]); + + return { data, loading }; +} +``` + +这个自定义的 Hook 函数 `useFetch` 接收一个URL作为参数,并返回一个包含 `data` 和 `loading` 状态的对象。在 `useEffect` 中,我们使用 `async/await` 语法来获取数据,并在获取数据后更新状态。 + +使用时: + +```jsx +import useFetch from './useFetch'; + +function MyComponent() { + const { data, loading } = useFetch('https://jsonplaceholder.typicode.com/todos/1'); + + if (loading) { + return
      Loading...
      ; + } + + return ( +
      +

      {data.title}

      +

      {data.completed ? 'Completed' : 'Not completed'}

      +
      + ); +} + +export default MyComponent; +``` + +在这个组件中,我们使用 `useFetch` 来获取数据,并根据 `loading` 状态显示不同的内容。这个自定义 Hook 函数可以在多个组件中重复使用,以避免重复的代码。 + +### 组件的生命周期 + +openInula 的组件创建时都会进行初始化步骤,在此过程会运行 openInula 所声明的生命周期钩子函数,便于开发者在特定阶段运行自己的代码,以实现复杂的项目逻辑。我们认为使用一个组件的主要流程遵循:**使用时创建,数据变化时更新,使用完卸载**。 + +#### 类组件的生命周期 + +在介绍类组件时,我们创建了一个 `Counter` 组件,其可以简单的实现计数器功能,但是当我们需要知道之前的值 `prevState` 以及当前的值 `currentState` 时,就需要使用更细节的代码控制,如生命周期。在下面新的 `Counter` 组件中我们调用了一些生命周期钩子函数,包含了组件的挂载、更新、卸载等过程。 + +```jsx +import { Component } from 'openinula'; + +class Counter extends Inula.Component { + constructor(props) { + super(props); + this.state = { + counter: 0 + }; + console.log('Constructor'); + } + + componentDidMount() { + console.log('Component Did Mount'); + } + + componentDidUpdate() { + console.log('Component Did Update'); + } + + componentWillUnmount() { + console.log('Component Will Unmount'); + } + + handleIncrement = () => { + this.setState(prevState => ({ + counter: prevState.counter + 1 + })); + }; + + render() { + console.log('Render'); + return ( +
      +

      Counter: {this.state.counter}

      + +
      + ); + } +} +``` + +**类组件的挂载** + +当 `Counter` 组件第一次通过 `render` 函数渲染在HTML页面时,这个方式被称为“挂载”。`componentDidMount` 钩子函数在组件挂载到 DOM 后被调用,可以用于执行初始化操作,例如发起网络请求、订阅事件等。 + +```jsx +componentDidMount() { + console.log('Component mounted'); +} +``` + +**类组件的更新** + +当组件中的内容变化时,则会触发组件的更新状态,会重新渲染页面。因此在组件挂载的情况存在两种,一种是初始化渲染,另一种是状态更新之后重新挂载。`componentDidUpdate` 在组件更新后被调用,可以用于处理组件的更新逻辑,比较前后状态的变化等。 + +```jsx +componentDidUpdate(prevProps, prevState) { + console.log('Component updated'); +} +``` + +**类组件的卸载** + +`componentWillUnmount` 在组件即将从 DOM 中卸载之前被调用,可以用于清理操作,例如取消网络请求、清除定时器等。 + +```jsx +componentWillUnmount() { + console.log('componentWillUnmount'); +} +``` + +#### 函数组件的生命周期 + +函数组件的生命周期是通过 `useEffect` 钩子函数来模拟的。 + +**函数组件的挂载** + +```jsx +// 模拟 componentDidMount +useEffect(() => { + console.log('Component did mount'); + // 在这里可以执行一些初始化操作 + // 也可以发送网络请求等操作 + // 注意:这个回调函数只会在组件挂载时执行一次 +}, []); +``` + +`useEffect`的第一个参数中的回调函数会被调用,模拟了`componentDidMount`。第二个参数为依赖项,可以控制回调函数的触发时机,这里未指定依赖项,则意味着只会在初次挂载时触发回调函数。 + +**函数组件的更新** + +```jsx +// 模拟 componentDidUpdate +useEffect(() => { + console.log('Component did update'); + // 在这里可以处理一些副作用操作 + // 例如,更新文档标题等 + // 注意:这个回调函数在组件更新时会执行 +}, [count]); +``` + +这里指定参数中的依赖项为 `count`,即只有依赖项 `count` 发生变化时,回调函数才会被调用。 + +**函数组件的卸载** + +```jsx +// 模拟 componentWillUnmount +useEffect(() => { + return () => { + console.log('Component will unmount'); + // 在这里可以执行一些清理操作 + // 例如,取消订阅、清除定时器等 + // 注意:这个回调函数只会在组件卸载时执行 + }; +}, []); +``` + +这里为回调函数设置了返回函数,在函数卸载时将调用这个返回函数中的操作,例如,取消订阅、清除定时器等。 + +## 快速进阶 + +### Ref + +openInula已经提供了两种管理数据的方式(`state` 和 `props`),但是针对一些特殊场景,当您的 openInula 组件需要与外部 API 通信时,通常需要在组件挂载后进行一些 DOM 操作,例如获取元素的尺寸、绑定事件监听器等。这些操作需要直接访问 DOM 元素,而 `props` 和 `state` 主要用于组件之间的数据传递和状态管理,不能达到操作 DOM 的目的,因此,openInula 还提供了 Ref 方式,允许我们直接操作组件的 DOM 元素或者调用组件内的方法,以进行数据交换。 + +#### Refs对象的创建 + +1. 使用 `createRef` 的方式可以创建一个 Ref 对象,可通过附加到 `ref` 属性上访问一个原生 DOM 元素或者组件。这种方式既可以在函数组件中使用,也可以在类组件中使用。 + + ```jsx + const ref = createRef(); + ``` + +2. 使用 `useRef(initialValue)` 创建,这种方式只能在函数组件中使用。 + + ```jsx + const ref = useRef(initialValue); + ``` + +> 上述两种方法的区别:`createRef` 每次渲染都会返回一个新的引用,而 `useRef` 每次都会返回相同的引用。详细请查阅[API参考](/apis/Inula)。 + +#### Ref 的两种使用场景 + +1. 存储变量 + + ```jsx + import Inula, { useState, useRef, useCallback } from 'openinula' + + export default function ClickWatch() { + let ref = useRef(0); + + const handleClick = useCallback(() => { + ref.current = ref.current + 1; + alert('You clicked ' + ref.current + ' times!'); + }, []); + + return ( + + ); + } + ``` + + 在上述示例中,通过使用`useRef(0)`我们创建了 + + ```jsx + { + current: 0 + } + ``` + + 这个对象,当对 `ref.current` 操作时,这个值是可变的,但是对象是不变的,这保证组件不会在每次递增时都触发重新渲染。 + +2. 操作DOM + + 通过使用 Ref,我们能够直接访问和操作 DOM 元素,而无需通过其他方式来获取或修改其值。下面的例子很好展示了 Ref 的用法。 + + ```jsx + import { useRef } from "openinula"; + + function Component() { + const inputRef = useRef(null); + + const handleClick = () => { + alert(`Input value: ${inputRef.current.value}`); + }; + + return ( +
      + + +
      + ); + } + ``` + + 在上述案例中,为了使用 Ref 特性,我们使用 `useRef` 钩子创建了一个 `inputRef` 变量,并将其初始化为 `null`。将 `inputRef` 传递给 `` 元素的 `ref` 属性。当按钮被点击时,则可以通过 `inputRef.current` 访问到DOM节点,并获取输入框的值。 + +> 注意:由于`ref`是可以避免重新渲染,因此,不要在渲染的过程中读取和写入,会导致结果不可控制。 \ No newline at end of file diff --git a/src/app/docs/introduction/page.mdx b/src/app/docs/introduction/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..64c47a6032ed85d0e3e79931a0ac17fc683d6d46 --- /dev/null +++ b/src/app/docs/introduction/page.mdx @@ -0,0 +1,41 @@ +--- +id: introduction +title: 项目介绍 +sidebar_label: 项目介绍 +--- + +# 项目介绍 + +OpenInula 是一个现代化的 JavaScript UI 框架,致力于提供极致性能和开发体验。 + +## 设计理念 + +- 无 API 哲学:只用 JSX + 原生 JS +- 编译优先:大部分逻辑编译期完成 +- 响应式:自动追踪依赖,极致性能 + +## 核心特性 + +- 直观的状态管理 +- 内置模板语法(if/for) +- 计算值与 watch +- 生命周期钩子 +- 兼容 React 生态 + +## 主要模块 + +- runtime:核心运行时 +- shared:通用工具 +- compiler:编译器 +- inula-bridge:兼容层 + +## 未来方向 + +- 更强的类型推导 +- 更丰富的生态组件 +- 更高效的编译优化 + +## 下一步 + +- [快速入门](./getting-started) +- [基础概念](next/core-concepts/introduction) \ No newline at end of file diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6eee41789fa2475bc50a2e9f556d1915bc182486 --- /dev/null +++ b/src/app/docs/layout.tsx @@ -0,0 +1,233 @@ +import { ReactNode } from 'react' +import { Layout } from 'nextra-theme-docs' +/*import { getPageMap } from 'nextra/page-map'*/ +import 'nextra-theme-docs/style.css' + +export default async function RootLayout({children}: Readonly<{ children: ReactNode }>) { + //const allPages = await getPageMap() + + const docsMap = [ + { + name: "概述", + route: '/docs', + title: '概述', + kind: 'MdxPage', + }, + { + name: 'introduction', + route: '/docs/introduction', + title: 'Introduction', + kind: 'MdxPage' + }, + { + name: 'Quick start', + route: '/docs/quick-start', + title: '快速开始', + kind: 'MdxPage' + }, + { + name: 'reactivity', + route: '/docs/reactivity', + title: '响应式系统', + kind: 'Folder', + children: [ + { + name: 'state', + route: '/docs/reactivity/state', + title: '状态管理', + kind: 'MdxPage' + }, + { + name: 'computed', + route: '/docs/reactivity/computed', + title: '计算值', + kind: 'MdxPage' + }, + { + name: 'watch', + route: '/docs/reactivity/watch', + title: '监听系统', + kind: 'MdxPage' + }, + { + name: 'untrack', + route: '/docs/reactivity/untrack', + title: 'untrack(非响应式访问)', + kind: 'MdxPage' + } + ] + }, + { + name: 'template-system', + route: '/docs/template-system', + title: '模板系统', + kind: 'Folder', + children: [ + { + name: 'conditional', + route: '/docs/template-system/conditional', + title: '条件渲染', + kind: 'MdxPage' + }, + { + name: 'event-handling', + route: '/docs/template-system/event-handling', + title: '事件处理', + kind: 'MdxPage' + }, + { + name: 'list-rendering', + route: '/docs/template-system/list-rendering', + title: '列表渲染', + kind: 'MdxPage' + } + ] + }, + { + name: 'lifecycle', + route: '/docs/lifecycle', + title: '生命周期', + kind: 'Folder', + children: [ + { + name: 'mounting', + route: '/docs/lifecycle/mounting', + title: '挂载阶段', + kind: 'MdxPage' + }, + { + name: 'unmounting', + route: '/docs/lifecycle/unmounting', + title: '卸载阶段', + kind: 'MdxPage' + }, + { + name: 'cleanup', + route: '/docs/lifecycle/cleanup', + title: '清理阶段', + kind: 'MdxPage' + } + ] + }, + { + name: 'components', + route: '/docs/components', + title: '组件', + kind: 'Folder', + children: [ + { + name: 'composition', + route: '/docs/components/composition', + title: '组合模式', + kind: 'MdxPage' + }, + { + name: 'context', + route: '/docs/components/context', + title: '上下文', + kind: 'MdxPage' + }, + { + name: 'Dynamic', + route: '/docs/components/Dynamic', + title: '动态组件', + kind: 'MdxPage' + }, + { + name: 'ErrorBoundary', + route: '/docs/components/ErrorBoundary', + title: '错误边界', + kind: 'MdxPage' + }, + { + name: 'Fragment', + route: '/docs/components/Fragment', + title: 'Fragment', + kind: 'MdxPage' + }, + { + name: 'Portal', + route: '/docs/components/Portal', + title: 'Portal', + kind: 'MdxPage' + }, + { + name: 'props', + route: '/docs/components/props', + title: 'Props', + kind: 'MdxPage' + }, + { + name: 'Suspense', + route: '/docs/components/Suspense', + title: 'Suspense', + kind: 'MdxPage' + } + ] + }, + { + name: 'conventional', + route: '/docs/conventional', + title: '传统API文档', + kind: 'Folder', + children: [ + { + name: 'UserInstruction', + route: '/docs/conventional/UserInstruction', + title: '用户指南', + kind: 'MdxPage', + }, + { + name: 'QuickStart', + route: '/docs/conventional/QuickStart', + title: '快速入门', + kind: 'MdxPage' + }, + { + name: 'SwitchingGuide', + route: '/docs/conventional/SwitchingGuide', + title: '切换指导书', + kind: 'MdxPage' + }, + { + name: 'ReleaseNotes', + route: '/docs/conventional/ReleaseNotes', + title: '发行说明', + kind: 'MdxPage' + }, + { + name: 'FAQ', + route: '/docs/conventional/FAQ', + title: '常见问题', + kind: 'MdxPage' + }, + { + name: 'CodeOfConduct', + route: '/docs/conventional/CodeOfConduct', + title: '行为准则', + kind: 'MdxPage' + }, + { + name: 'ContributingGuide', + route: '/docs/conventional/ContributingGuide', + title: '贡献指南', + kind: 'MdxPage' + } + ] + } + ] + + return ( +
      + + {children} + +
      + ) +} \ No newline at end of file diff --git a/src/app/docs/lifecycle/cleanup/page.mdx b/src/app/docs/lifecycle/cleanup/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8611801757462c5802c892e28a94435127ea51a6 --- /dev/null +++ b/src/app/docs/lifecycle/cleanup/page.mdx @@ -0,0 +1,59 @@ +--- +id: cleanup +title: 清理操作 +sidebar_label: 清理操作 +--- + +# 清理操作 + +清理操作用于移除副作用,如定时器、事件监听、watch 清理函数等。 + +## 定时器清理 + +```tsx filename="TimerCleanup.jsx" +function Timer() { + let timer; + didMount(() => { + timer = setInterval(() => {}, 1000); + }); + willUnmount(() => clearInterval(timer)); + return
      定时器已启动
      ; +} +``` + +## 事件监听清理 + +```tsx filename="EventListener.jsx" +function Listener() { + function onResize() {} + didMount(() => window.addEventListener('resize', onResize)); + willUnmount(() => window.removeEventListener('resize', onResize)); + return
      监听窗口大小
      ; +} +``` + +## watch 清理函数 + +```tsx filename="WatchCleanup.jsx" +function Watcher() { + let timer; + watch(() => { + timer = setInterval(() => {}, 1000); + return () => clearInterval(timer); + }); + return
      watch 清理
      ; +} +``` + +## 最佳实践 + +- 所有副作用都应有清理逻辑 +- 推荐统一在 willUnmount/didUnmount 或 watch 清理函数中处理 + +## 注意事项 + +- 避免遗留定时器、事件监听等,防止内存泄漏 + +## 相关链接 + +- [卸载阶段](../unmounting) \ No newline at end of file diff --git a/src/app/docs/lifecycle/mounting/page.mdx b/src/app/docs/lifecycle/mounting/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..2ea05b05178f9a691699a2669953c11388bd7355 --- /dev/null +++ b/src/app/docs/lifecycle/mounting/page.mdx @@ -0,0 +1,49 @@ +--- +id: mounting +title: 挂载阶段 +sidebar_label: 挂载阶段 +--- + +# 挂载阶段 + +组件挂载时可执行副作用,如数据请求、事件监听等。OpenInula 提供 didMount 钩子。 + +## 基本用法 + +```tsx filename="PageTitle.jsx" +function PageTitle() { + let title = ''; + didMount(() => { + title = document.title; + }); + return

      页面标题:{title}

      ; +} +``` + +## 数据请求 + +```tsx filename="FetchData.jsx" +function FetchData() { + let data = null; + didMount(() => { + fetch('/api/data') + .then(res => res.json()) + .then(d => { data = d; }); + }); + return
      {JSON.stringify(data)}
      ; +} +``` + +## 最佳实践 + +- 只在 didMount 中执行副作用 +- 清理副作用请用 willUnmount/didUnmount + +## 注意事项 + +- didMount 只在组件首次挂载时执行一次 + +## 相关链接 + +- [卸载阶段](../unmounting) +- [清理操作](../cleanup) \ No newline at end of file diff --git a/src/app/docs/lifecycle/unmounting/page.mdx b/src/app/docs/lifecycle/unmounting/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..01316bb4e0407fc74744a0b37fe31b6c2802478f --- /dev/null +++ b/src/app/docs/lifecycle/unmounting/page.mdx @@ -0,0 +1,43 @@ +--- +id: unmounting +title: 卸载阶段 +sidebar_label: 卸载阶段 +--- + +# 卸载阶段 + +组件卸载时需清理副作用,防止内存泄漏。OpenInula 提供 willUnmount/didUnmount 钩子。 + +## 基本用法 + +```tsx filename="Timer.jsx" +function Timer() { + let timer; + didMount(() => { + timer = setInterval(() => {}, 1000); + }); + willUnmount(() => clearInterval(timer)); + // 或 didUnmount(() => clearInterval(timer)); + return
      定时器已启动
      ; +} +``` + +## 副作用清理 + +- 清理定时器、事件监听、watch 副作用等 +- 推荐所有副作用都在卸载时清理 + +## 最佳实践 + +- 所有副作用都应在卸载时清理 +- 推荐用 willUnmount/didUnmount + +## 注意事项 + +- 清理函数只在组件卸载时执行一次 +- 避免遗留定时器、事件监听等,防止内存泄漏 + +## 相关链接 + +- [挂载阶段](../mounting) +- [清理操作](../cleanup) \ No newline at end of file diff --git a/src/app/docs/page.mdx b/src/app/docs/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..d606a91294cfe4d40fb7573bc6e968c7640679a4 --- /dev/null +++ b/src/app/docs/page.mdx @@ -0,0 +1,110 @@ +# openInula 2 + +openInula 2版本是一个现代化的 JavaScript UI 开发库,具有以下特点: + +- **无 API 设计理念**:只需使用 JSX 和原生 JavaScript 即可直观编程 +- **编译优先**:通过编译时分析优化性能 +- **轻量级响应式系统**:高效的运行时更新机制 + +## 核心特性 + +### 直接状态管理 + +```tsx filename="Counter.jsx" +function Counter() { + let count = 0; // 直接声明状态 + + return ( +
      +

      计数: {count}

      + +
      + ); +} +``` + +### 内置流程控制 + +```tsx filename="UserList.jsx" +function UserList({ users }) { + return ( +
        + + {(user) => ( + +
      • {user.name}
      • +
        + )} +
        +
      + ); +} +``` + +### 计算值 + +```tsx filename="DoubleCounter.jsx" +function DoubleCounter() { + let count = 0; + const double = count * 2; // 自动计算的值 + + return
      双倍值: {double}
      ; +} +``` + +### 监听系统 + +```tsx filename="DataFetcher.jsx" +function DataFetcher() { + let data = null; + + watch(() => { + fetch('https://api.example.com/data') + .then(res => res.json()) + .then(_data => { + data = _data; + }); + }); + + return ( +
      + +
      {JSON.stringify(data, null, 2)}
      +
      + +

      加载中...

      +
      +
      + ); +} +``` + +## 快速开始 + +让我们通过以下章节来学习 openInula 2版本: + +1. [基础概念](./core-concepts/introduction) + +- 组件基础 +- JSX 语法 +- 状态管理 +2. [模板系统](./template-system/conditional) + +- 条件渲染 +- 列表渲染 +- 事件处理 +3. [响应式系统](./reactivity/state) + +- 状态声明 +- 计算值 +- 监听系统 +4. [组件开发](./components/props) + +- Props 传递 +- 组件组合 +- 上下文管理 +5. [生命周期](./lifecycle/mounting) + +- 挂载阶段 +- 卸载阶段 +- 清理操作 diff --git a/src/app/docs/quick-start/page.mdx b/src/app/docs/quick-start/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..09b2b169570a5021cb5e73d974b98e555d178dc3 --- /dev/null +++ b/src/app/docs/quick-start/page.mdx @@ -0,0 +1,141 @@ +--- +id: getting-started +title: 快速入门 +sidebar_label: 快速入门 +--- + +# 快速入门 + +欢迎使用 openInula 2!本节将带你完成环境搭建、插件集成、项目初始化和第一个组件。 + +## 环境要求 + +- Node.js >= 16.14 +- 推荐使用 npm 8+ 或 pnpm/yarn + +## 使用 CLI 快速创建项目 + +OpenInula 提供了官方脚手架工具 `create-inula`,可以一键初始化新项目,无需手动配置。 + +### 安装与使用 + +无需全局安装,直接用 npx 或 npm create: + +```bash filename="create-project.sh" +# 推荐方式(无需全局安装) +npm create inula@latest my-app + +# 或者使用 npx +npx create-inula my-app +``` + +执行命令后,CLI 会引导你选择项目模板、打包方式(如 webpack/vite)等,并自动拉取依赖、初始化目录结构。 + +### 典型交互流程 + +1. 选择项目名称(如 my-app) +2. 选择模板(如 Next-app,后续会有更多模板) +3. 选择打包方式(webpack 或 vite) +4. 自动安装依赖并初始化项目 + +### 目录结构示例 + +```text filename="project-structure.txt" +my-app/ +├── src/ +│ └── main.tsx +├── public/ +│ └── index.html +├── package.json +└── ... +``` + +### 启动开发服务器 + +进入项目目录后,运行: + +```bash filename="start-dev.sh" +npm install +npm start +``` + +即可在浏览器中访问本地开发环境。 + +### 更多用法 + +详细参数和高级用法请参考 [create-inula README](https://github.com/openinula/openinula/tree/main/packages/create-inula/README.md)。 + + +## 集成到主流构建工具 + +OpenInula 提供统一的 unplugin 插件,支持 Vite、Rollup、Webpack、Nuxt、Vue CLI、esbuild 等主流工具。 + +### 安装依赖 + + +```bash filename="install-unplugin.sh" +npm i @openinula/unplugin +``` + +### Vite + +```ts filename="vite.config.ts" +import { defineConfig } from 'vite'; +import inulaNext from '@openinula/unplugin/vite'; + +export default defineConfig({ + plugins: [ + inulaNext({ /* 可选配置 */ }), + ], +}); +``` + +### Rollup + +```js filename="rollup.config.js" +import inulaNext from '@openinula/unplugin/rollup'; + +export default { + plugins: [ + inulaNext({ /* 可选配置 */ }), + ], +}; +``` + +### Webpack + +```js filename="webpack.config.js" +// webpack.config.js +module.exports = { + /* ... */ + plugins: [ + require('@openinula/unplugin/webpack')({ /* 可选配置 */ }) + ] +} +``` + + +## 第一个组件 + +```tsx filename="Hello.jsx" +function Hello() { + return

      你好,OpenInula!

      ; +} +``` + +## 目录结构 + +- src/ 代码目录 +- public/ 静态资源 +- package.json 项目配置 + +## 相关链接 + +- [unplugin 官方文档](https://github.com/unjs/unplugin) +- [OpenInula 插件源码](https://github.com/openinula/openinula/tree/main/next-packages/compiler/unplugin-inula-next) +- [更多示例 playground/](https://github.com/openinula/openinula/tree/main/next-packages/compiler/unplugin-inula-next/playground) + +## 下一步 + +- 阅读[项目介绍](./introduction) +- 深入[基础概念](next/core-concepts/introduction) \ No newline at end of file diff --git a/src/app/docs/reactivity/computed/page.mdx b/src/app/docs/reactivity/computed/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..f7c455391c0dc33217e876041e9253f2b7791706 --- /dev/null +++ b/src/app/docs/reactivity/computed/page.mdx @@ -0,0 +1,79 @@ +--- +id: computed +title: 计算值 +sidebar_label: 计算值 +--- + +# 计算值 + +在 openInula 2(zouyu)中,计算值(Computed)是指依赖于其他状态变量的表达式。你只需像写普通 JS 一样声明变量,编译器会自动识别并优化这些依赖关系。 + +## 基本用法 + +```tsx filename="DoubleCounter.jsx" +function DoubleCounter() { + let count = 0; + // 计算值:double 会自动随着 count 变化 + const double = count * 2; + + return ( +
      +

      当前计数:{count}

      +

      双倍值:{double}

      + +
      + ); +} +``` + +## 多重依赖 + +计算值可以依赖多个状态变量: + +```tsx filename="PriceCalculator.jsx" +function PriceCalculator() { + let price = 100; + let quantity = 2; + const total = price * quantity; + + return ( +
      +

      单价:{price}

      +

      数量:{quantity}

      +

      总价:{total}

      + +
      + ); +} +``` + +## 嵌套计算 + +计算值可以嵌套依赖其他计算值: + +```tsx filename="NestedComputed.jsx" +function NestedComputed() { + let a = 1; + const b = a + 2; + const c = b * 3; + + return
      c 的值:{c}
      ; +} +``` + +## 最佳实践 + +- 只需像写普通 JS 一样声明依赖关系,编译器会自动优化。 +- 避免在计算值中产生副作用(如修改外部状态)。 +- 计算值适合用于 UI 展示、派生数据等场景。 + +## 注意事项 + +- 计算值应为纯表达式,不要在其中执行异步操作或副作用。 +- 如果依赖的状态较多,建议提前拆分变量,提升可读性。 + +## 下一步 + +- 了解[监听系统](./watch)的用法 +- 学习[状态管理](./state)的更多技巧 +- 探索[组件组合](../components/composition)的高级用法 \ No newline at end of file diff --git a/src/app/docs/reactivity/state/page.mdx b/src/app/docs/reactivity/state/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..dcc8478a4b95df1620ebabfb6c37b073256e84d6 --- /dev/null +++ b/src/app/docs/reactivity/state/page.mdx @@ -0,0 +1,293 @@ +--- +id: state +title: 状态管理 +sidebar_label: 状态管理 +--- + +# 状态管理 + +OpenInula 的状态管理采用直观的方式,无需特殊的 API 或 Hook,直接使用 JavaScript 变量和赋值即可。 + +## 基础用法 + +### 声明状态 + +在组件中直接声明变量作为状态: + +```tsx filename="Counter.jsx" +function Counter() { + let count = 0; // 直接声明状态 + + return ( +
      +

      计数:{count}

      + +
      + ); +} +``` + +### 对象状态 + +对于复杂的状态,可以使用对象: + +```tsx filename="UserProfile.jsx" +function UserProfile() { + let user = { + name: '张三', + age: 25, + preferences: { + theme: 'dark', + language: 'zh' + } + }; + + function updateTheme(newTheme) { + user.preferences.theme = newTheme; // 直接修改对象属性 + } + + return ( +
      +

      {user.name}

      +

      年龄:{user.age}

      +
      + 主题:{user.preferences.theme} + +
      +
      + ); +} +``` + +### 数组状态 + +处理数组类型的状态: + +```tsx filename="TodoList.jsx" +function TodoList() { + let todos = [ + { id: 1, text: '学习 OpenInula', done: false }, + { id: 2, text: '写文档', done: false } + ]; + + function addTodo(text) { + todos = [ + ...todos, + { + id: todos.length + 1, + text, + done: false + } + ]; + } + + function toggleTodo(id) { + todos = todos.map(todo => + todo.id === id + ? { ...todo, done: !todo.done } + : todo + ); + } + + return ( +
      + +
        + + {(todo) => ( +
      • toggleTodo(todo.id)} + style={{ + textDecoration: todo.done ? 'line-through' : 'none' + }} + > + {todo.text} +
      • + )} +
        +
      +
      + ); +} +``` + +## 状态更新 + +### 直接赋值 + +最简单的状态更新方式是直接赋值: + +```tsx filename="CounterWithMessage.jsx" +function Counter() { + let count = 0; + let message = ''; + + function increment() { + count++; // 直接赋值 + message = `当前计数:${count}`; // 状态之间可以互相依赖 + } + + return ( +
      +

      {message}

      + +
      + ); +} +``` + +### 批量更新 + +当需要同时更新多个状态时: + +```tsx filename="UserForm.jsx" +function UserForm() { + let formData = { + username: '', + email: '', + age: 0 + }; + + function resetForm() { + // 一次性更新多个字段 + formData = { + username: '', + email: '', + age: 0 + }; + } + + function updateField(field, value) { + formData[field] = value; // 更新单个字段 + } + + return ( + + updateField('username', e.target.value)} + /> + updateField('email', e.target.value)} + /> + updateField('age', parseInt(e.target.value))} + /> + + + ); +} +``` + +## 最佳实践 + +### 1. 状态初始化 + +将复杂的初始状态提取为函数: + +```tsx filename="createInitialState.jsx" +function createInitialState() { + return { + user: null, + preferences: { + theme: 'light', + language: 'zh' + }, + todos: [] + }; +} + +function App() { + let state = createInitialState(); + + function reset() { + state = createInitialState(); // 重置为初始状态 + } + + return ( +
      + {/* 使用状态 */} +
      + ); +} +``` + +### 2. 状态组织 + +对于复杂组件,建议将相关状态组织在一起: + +```tsx filename="ShoppingCart.jsx" +function ShoppingCart() { + let cart = { + items: [], + total: 0, + + addItem(product) { + this.items = [...this.items, product]; + this.updateTotal(); + }, + + removeItem(productId) { + this.items = this.items.filter(item => item.id !== productId); + this.updateTotal(); + }, + + updateTotal() { + this.total = this.items.reduce((sum, item) => sum + item.price, 0); + } + }; + + return ( +
      +

      购物车 ({cart.items.length})

      +

      总计:¥{cart.total}

      + {/* 渲染购物车内容 */} +
      + ); +} +``` + +### 3. 派生状态 + +避免重复存储可以从现有状态计算出的值: + +```tsx filename="TodoApp.jsx" +function TodoApp() { + let todos = []; + + // 好的做法:通过计算获得派生状态 + const completedCount = todos.filter(todo => todo.done).length; + const remainingCount = todos.length - completedCount; + + return ( +
      +

      完成:{completedCount}

      +

      剩余:{remainingCount}

      +
      + ); +} +``` + +## 注意事项 + +1. 避免直接修改复杂对象的深层属性,推荐使用不可变更新模式 +2. 确保状态更新是同步的,避免在异步操作中直接修改状态 +3. 对于大型应用,考虑使用状态管理库 +4. 注意状态的作用域,避免全局状态 + +## 下一步 + +学习完状态管理后,你可以: + +1. 了解[计算值](./computed.mdx)的使用 +2. 探索[监听系统](./watch)的功能 +3. 学习[组件组合](../components/composition)的最佳实践 \ No newline at end of file diff --git a/src/app/docs/reactivity/untrack/page.mdx b/src/app/docs/reactivity/untrack/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..b96f273f54dda1c76ee7ede24ac64826d59725c0 --- /dev/null +++ b/src/app/docs/reactivity/untrack/page.mdx @@ -0,0 +1,124 @@ +--- +id: untrack +title: untrack(非响应式访问) +sidebar_label: untrack +--- + +# untrack(非响应式访问) + +`untrack` 是 openInula 2(zouyu)响应式系统中的一个高级工具,用于在响应式计算、watch、或其他依赖收集场景下,临时"跳出"依赖追踪,获取某个状态的当前值但不建立响应式依赖。 + +## 作用 + +- **避免依赖收集**:在 computed、watch、或其他响应式回调中,某些变量的读取不希望被追踪为依赖时使用。 +- **性能优化**:减少不必要的响应式更新,避免副作用函数被频繁触发。 +- **实现只读快照**:获取某一时刻的状态快照,而不关心后续变化。 + +## 基本用法 + +```tsx filename="Example.jsx" +import { untrack } from 'openinula'; + +function Example() { + let count = 0; + let log = []; + + watch(() => { + // 只在 count 变化时触发 + log.push(`count: ${count}, time: ${untrack(() => Date.now())}`); + }); + + return ( +
      + +
        + {(item) =>
      • {item}
      • }
        +
      +
      + ); +} +``` + +## Demo 示例 + +下面是一个最小可运行的 untrack 用法示例: + +```tsx filename="Demo.jsx" +import { watch, untrack } from 'openinula'; + +function Demo() { + let a = 0; + let b = 0; + watch(() => { + // 只依赖 b,a 的变化不会触发 watch + console.log('sum:', untrack(() => a) + b); + }); + return ( + <> + + + + ); +} +``` +点击 a 按钮不会触发 watch,点击 b 按钮会触发 watch 并输出 sum。 + +## 在 computed 中使用 + +```tsx filename="ComputedUntrack.jsx" +import { untrack } from 'openinula'; + +function ComputedUntrack() { + let count = 0; + // 只依赖 count,不依赖 Date.now() + const message = `当前计数:${count},时间:${untrack(() => Date.now())}`; + + return
      {message}
      ; +} +``` + +## 在 watch 中使用 + +```tsx filename="WatchUntrack.jsx" +import { untrack } from 'openinula'; + +function WatchUntrack() { + let count = 0; + let lastTime = 0; + + watch(() => { + // 只在 count 变化时触发 + lastTime = untrack(() => Date.now()); + }); + + return ( +
      + +

      上次更新时间:{lastTime}

      +
      + ); +} +``` + +## 在响应式系统的其他场景 + +- **避免副作用循环**:在副作用函数中读取但不追踪依赖,防止死循环。 +- **只读快照**:在复杂计算或日志记录时,获取当前值但不建立响应式关系。 + +## 最佳实践 + +- 仅在确实不希望建立响应式依赖时使用 `untrack`。 +- 用于性能优化、避免不必要的副作用触发。 +- 保持代码可读性,避免滥用。 + +## 注意事项 + +- `untrack` 只影响其回调函数内部的依赖收集。 +- 不要在 `untrack` 内部修改响应式状态,只做只读访问。 +- 滥用 `untrack` 可能导致响应式失效,需谨慎使用。 + +## 相关链接 + +- [计算值(computed)](./computed.mdx) +- [监听系统(watch)](./watch) +- [状态管理](./state.mdx) \ No newline at end of file diff --git a/src/app/docs/reactivity/watch/page.mdx b/src/app/docs/reactivity/watch/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..9e2e4eae40d1d7f1871241daa248b21edc92e853 --- /dev/null +++ b/src/app/docs/reactivity/watch/page.mdx @@ -0,0 +1,114 @@ +--- +id: watch +title: 监听系统 +sidebar_label: 监听系统 +--- + +# 监听系统(watch) + +openInula 2(zouyu)提供了 `watch` 方法,用于监听状态或计算值的变化,并在变化时执行副作用逻辑。 + +## 基本用法 + +```tsx filename="FetchData.jsx" +function FetchData({ url }) { + let data = null; + + watch(() => { + fetch(url) + .then(res => res.json()) + .then(_data => { + data = _data; + }); + }); + + return ( +
      + +
      {JSON.stringify(data, null, 2)}
      +
      + +

      加载中...

      +
      +
      + ); +} +``` + +## 依赖追踪 + +`watch` 会自动追踪其回调中用到的所有状态和计算值,只要依赖发生变化,回调就会重新执行。 + +```tsx filename="Logger.jsx" +function Logger() { + let count = 0; + watch(() => { + console.log('count 变化为:', count); + }); + + return ; +} +``` + +## 清理副作用 + +可以在 `watch` 回调中返回一个清理函数,用于移除定时器、事件监听等副作用: + +```tsx filename="Timer.jsx" +function Timer() { + let time = Date.now(); + + watch(() => { + const timer = setInterval(() => { + time = Date.now(); + }, 1000); + // 返回清理函数 + return () => clearInterval(timer); + }); + + return
      当前时间:{new Date(time).toLocaleTimeString()}
      ; +} +``` + +## 多个 watch + +可以在同一个组件中使用多个 `watch`,分别监听不同的状态: + +```tsx filename="MultiWatch.jsx" +function MultiWatch() { + let a = 1; + let b = 2; + + watch(() => { + console.log('a 变化:', a); + }); + watch(() => { + console.log('b 变化:', b); + }); + + return ( +
      + + +
      + ); +} +``` + +## 最佳实践 + +- 只在需要副作用(如数据请求、日志、定时器等)时使用 `watch` +- 避免在 `watch` 中直接修改依赖自身的状态,防止死循环 +- 善用清理函数,避免内存泄漏 + +## 注意事项 + +- `watch` 回调应为同步函数,避免直接返回 Promise +- 清理函数只在依赖变化或组件卸载时调用 +- 不要在 `watch` 外部直接调用副作用逻辑 + +## 下一步 + +- 学习[状态管理](./state.mdx)的更多用法 +- 了解[计算值](./computed.mdx)的高级技巧 +- 探索[生命周期](../lifecycle/mounting)的管理 \ No newline at end of file diff --git a/src/app/docs/template-system/conditional/page.mdx b/src/app/docs/template-system/conditional/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..677fc7a51a8d34b058c4c488a87aaf475c1dd08a --- /dev/null +++ b/src/app/docs/template-system/conditional/page.mdx @@ -0,0 +1,208 @@ +--- +id: conditional +title: 条件渲染 +sidebar_label: 条件渲染 +--- + +# 条件渲染 + +OpenInula 提供了内置的条件渲染标签,使得条件渲染变得简单直观。 + +## 基本用法 + +### if 条件 + +最简单的条件渲染使用 `if` 标签: + +```tsx filename="SimpleCondition.jsx" +function Notification({ message }) { + return ( +
      + +

      {message}

      +
      +
      + ); +} +``` + +### if-else 条件 + +使用 `if` 和 `else` 标签处理两种情况: + +```tsx filename="IfElseCondition.jsx" +function LoginStatus({ isLoggedIn }) { + return ( +
      + + + + + + +
      + ); +} +``` + +### 多重条件 + +使用 `else-if` 处理多个条件: + +```tsx filename="MultipleConditions.jsx" +function TrafficLight({ color }) { + return ( +
      + +

      停止

      +
      + +

      注意

      +
      + +

      通行

      +
      + +

      信号灯故障

      +
      +
      + ); +} +``` + +## 条件表达式 + +条件表达式可以是任何返回布尔值的表达式: + +```tsx filename="ComplexConditions.jsx" +function UserProfile({ user }) { + return ( +
      + = 18}> +

      成年用户

      +
      + +

      高级会员

      +
      +
      + ); +} +``` + +## 嵌套条件 + +条件标签可以嵌套使用: + +```tsx filename="NestedConditions.jsx" +function ProductDisplay({ product, user }) { + return ( +
      + + + + + + + + +

      请登录后购买

      +
      +
      + +

      商品不存在

      +
      +
      + ); +} +``` + +## 最佳实践 + +### 1. 提前返回 + +对于简单的条件判断,可以使用提前返回模式: + +```tsx filename="EarlyReturn.jsx" +function AdminPanel({ user }) { + if (!user) { + return

      请先登录

      ; + } + + if (!user.isAdmin) { + return

      权限不足

      ; + } + + return
      管理员面板
      ; +} +``` + +### 2. 条件组合 + +对于复杂的条件逻辑,建议将条件提取为变量: + +```tsx filename="ComplexConditionLogic.jsx" +function UserAccess({ user, resource }) { + const canView = user && user.permissions.includes('view'); + const canEdit = canView && user.permissions.includes('edit'); + const isOwner = resource.ownerId === user.id; + + return ( +
      + +
      {resource.content}
      + + + +
      + +

      无访问权限

      +
      +
      + ); +} +``` + +### 3. 避免过度嵌套 + +当条件逻辑变得复杂时,考虑将其拆分为多个组件: + +```tsx filename="ComponentComposition.jsx" +function UserActions({ user, resource }) { + return ( +
      + + + + + + +
      + ); +} + +function ViewPermission({ user, children }) { + return ( + + {children} + + +

      无访问权限

      +
      + ); +} +``` + +## 注意事项 + +1. 条件标签必须包含 `cond` 属性 +2. `else-if` 和 `else` 标签必须跟在 `if` 或 `else-if` 标签之后 +3. 条件表达式应该返回布尔值 +4. 避免在条件表达式中进行复杂的计算,建议提前计算并存储结果 + +## 下一步 + +学习完条件渲染后,你可以: + +1. 了解[列表渲染](../list-rendering)的使用方法 +2. 探索[事件处理](../event-handling)系统 +3. 学习[组件组合](../../components/composition)的技巧 \ No newline at end of file diff --git a/src/app/docs/template-system/event-handling/page.mdx b/src/app/docs/template-system/event-handling/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..6fd5c7f64ff01df0aaf866f347a598a1a98f2071 --- /dev/null +++ b/src/app/docs/template-system/event-handling/page.mdx @@ -0,0 +1,304 @@ +--- +id: event-handling +title: 事件处理 +sidebar_label: 事件处理 +--- + +# 事件处理 + +OpenInula 提供了简单直观的事件处理机制,使用 `onXxx` 属性来绑定事件处理函数。 + +## 基本用法 + +### 点击事件 + +最常用的事件处理是点击事件: + +```tsx filename="ClickCounter.jsx" +function ClickCounter() { + let count = 0; + + function handleClick() { + count++; + } + + return ( + + ); +} +``` + +### 内联事件处理 + +对于简单的事件处理,可以直接使用内联函数: + +```tsx filename="SimpleButton.jsx" +function SimpleButton() { + let message = ''; + + return ( +
      + +

      {message}

      +
      + ); +} +``` + +## 常用事件 + +### 表单事件 + +处理表单输入和提交: + +```tsx filename="LoginForm.jsx" +function LoginForm() { + let username = ''; + let password = ''; + + function handleSubmit(e) { + e.preventDefault(); + console.log('提交登录:', { username, password }); + } + + return ( +
      + username = e.target.value} + placeholder="用户名" + /> + password = e.target.value} + placeholder="密码" + /> + +
      + ); +} +``` + +### 鼠标事件 + +处理鼠标相关事件: + +```tsx filename="MouseTracker.jsx" +function MouseTracker() { + let position = { x: 0, y: 0 }; + + return ( +
      { + position = { + x: e.clientX, + y: e.clientY + }; + }} + style={{ height: '200px', background: '#f0f0f0' }} + > +

      鼠标位置:{position.x}, {position.y}

      +
      + ); +} +``` + +### 键盘事件 + +处理键盘输入: + +```tsx filename="KeyboardInput.jsx" +function KeyboardInput() { + let pressedKeys = []; + + return ( + { + pressedKeys = [...pressedKeys, e.key]; + }} + onKeyUp={e => { + pressedKeys = pressedKeys.filter(key => key !== e.key); + }} + placeholder="请按键..." + /> + ); +} +``` + +## 事件修饰符 + +OpenInula 支持事件修饰符来简化常见的事件处理场景: + +```tsx filename="EventModifiers.jsx" +function EventModifiers() { + return ( +
      + {/* 阻止默认行为 */} + e.preventDefault()} href="#"> + 阻止默认跳转 + + + {/* 停止事件传播 */} +
      console.log('外层点击')}> + +
      +
      + ); +} +``` + +## 自定义事件 + +在组件间通信时,可以通过 props 传递自定义事件处理函数: + +```tsx filename="CustomButton.jsx" +function CustomButton({ onClick, children }) { + return ( + + ); +} + +function App() { + return ( + console.log('处理点击')}> + 点击我 + + ); +} +``` + +## 最佳实践 + +### 1. 事件处理函数命名 + +建议使用 `handle` 前缀命名事件处理函数: + +```tsx filename="UserForm.jsx" +function UserForm() { + let formData = { + name: '', + email: '' + }; + + function handleNameChange(e) { + formData.name = e.target.value; + } + + function handleEmailChange(e) { + formData.email = e.target.value; + } + + function handleSubmit(e) { + e.preventDefault(); + console.log('提交表单:', formData); + } + + return ( +
      + + + +
      + ); +} +``` + +### 2. 事件参数处理 + +在需要传递额外参数时,使用箭头函数包装: + +```tsx filename="ItemList.jsx" +function ItemList() { + const items = ['项目1', '项目2', '项目3']; + + function handleItemClick(item, index, e) { + console.log('点击项目:', item, '索引:', index, '事件:', e); + } + + return ( +
        + + {(item, index) => ( +
      • handleItemClick(item, index, e)}> + {item} +
      • + )} +
        +
      + ); +} +``` + +### 3. 性能优化 + +对于频繁触发的事件,考虑使用节流或防抖: + +```tsx filename="SearchInput.jsx" +function SearchInput() { + let searchTerm = ''; + let timeoutId; + + function handleInput(e) { + // 使用防抖处理搜索 + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + searchTerm = e.target.value; + console.log('搜索:', searchTerm); + }, 300); + } + + willUnmount(() => { + clearTimeout(timeoutId); + }); + + return ( + + ); +} +``` + +## 注意事项 + +1. 事件处理函数中的 `this` 绑定 +2. 避免在渲染函数中创建内联函数 +3. 记得清理定时器和事件监听器 +4. 注意事件冒泡和捕获的顺序 + +## 下一步 + +学习完事件处理后,你可以: + +1. 探索[组件组合](../../components/composition)的技巧 +2. 了解[生命周期](../../lifecycle/mounting)的管理 +3. 学习[状态管理](../../reactivity/state)的最佳实践 \ No newline at end of file diff --git a/src/app/docs/template-system/list-rendering/page.mdx b/src/app/docs/template-system/list-rendering/page.mdx new file mode 100644 index 0000000000000000000000000000000000000000..8703d13294aa533f42abe1b3eb1ffaaf559f14b7 --- /dev/null +++ b/src/app/docs/template-system/list-rendering/page.mdx @@ -0,0 +1,257 @@ +--- +id: list-rendering +title: 列表渲染 +sidebar_label: 列表渲染 +--- + +# 列表渲染 + +OpenInula 提供了内置的 `for` 标签来实现列表渲染,使得数组的遍历和渲染变得简单直观。 + +## 基本用法 + +### 简单列表 + +使用 `for` 标签遍历数组: + +```tsx filename="FruitList.jsx" +function FruitList() { + const fruits = ['苹果', '香蕉', '橙子']; + + return ( +
        + + {(fruit) =>
      • {fruit}
      • } +
        +
      + ); +} +``` + +### 带索引的列表 + +通过第二个参数获取索引: + +```tsx filename="NumberedList.jsx" +function NumberedList() { + const items = ['第一项', '第二项', '第三项']; + + return ( +
        + + {(item, index) => ( +
      • #{index + 1}: {item}
      • + )} +
        +
      + ); +} +``` + +## 对象列表 + +渲染对象数组: + +```tsx filename="UserTable.jsx" +function UserList() { + const users = [ + { id: 1, name: '张三', age: 25 }, + { id: 2, name: '李四', age: 30 }, + { id: 3, name: '王五', age: 28 } + ]; + + return ( + + + + + + + + + + + {(user) => ( + + + + + + )} + + +
      ID姓名年龄
      {user.id}{user.name}{user.age}
      + ); +} +``` + +## 列表操作 + +### 列表过滤 + +可以在渲染前对列表进行过滤: + +```tsx filename="ActiveUserList.jsx" +function ActiveUserList() { + const users = [ + { id: 1, name: '张三', active: true }, + { id: 2, name: '李四', active: false }, + { id: 3, name: '王五', active: true } + ]; + + const activeUsers = users.filter(user => user.active); + + return ( +
        + + {(user) =>
      • {user.name}
      • } +
        +
      + ); +} +``` + +### 列表排序 + +可以在渲染前对列表进行排序: + +```tsx filename="SortedUserList.jsx" +function SortedUserList() { + const users = [ + { id: 1, name: '张三', age: 25 }, + { id: 2, name: '李四', age: 30 }, + { id: 3, name: '王五', age: 28 } + ]; + + const sortedUsers = [...users].sort((a, b) => a.age - b.age); + + return ( +
        + + {(user) => ( +
      • {user.name} ({user.age}岁)
      • + )} +
        +
      + ); +} +``` + +## 嵌套列表 + +可以嵌套使用 `for` 标签: + +```tsx filename="NestedList.jsx" +function NestedList() { + const departments = [ + { + name: '技术部', + teams: ['前端组', '后端组', '测试组'] + }, + { + name: '产品部', + teams: ['设计组', '运营组'] + } + ]; + + return ( +
      + + {(dept) => ( +
      +

      {dept.name}

      +
        + + {(team) =>
      • {team}
      • } +
        +
      +
      + )} +
      +
      + ); +} +``` + +## 最佳实践 + +### 1. 列表项组件化 + +对于复杂的列表项,建议将其抽取为单独的组件: + +```tsx filename="UserItemComponent.jsx" +function UserItem({ user }) { + return ( +
      + {user.name} +
      +

      {user.name}

      +

      {user.email}

      +
      +
      + ); +} + +function UserList() { + const users = [/* ... */]; + + return ( +
      + + {(user) => } + +
      + ); +} +``` + +### 2. 列表更新优化 + +当列表项可能发生变化时,建议使用不可变数据操作: + +```tsx +function TodoList() { + let todos = [ + { id: 1, text: '学习 OpenInula', done: false }, + { id: 2, text: '写文档', done: false } + ]; + + function toggleTodo(id) { + todos = todos.map(todo => + todo.id === id + ? { ...todo, done: !todo.done } + : todo + ); + } + + return ( +
        + + {(todo) => ( +
      • toggleTodo(todo.id)} + style={{ textDecoration: todo.done ? 'line-through' : 'none' }} + > + {todo.text} +
      • + )} +
        +
      + ); +} +``` + +## 注意事项 + +1. `for` 标签必须包含 `each` 属性 +2. 回调函数必须返回 JSX 元素 +3. 对于大列表,考虑使用分页或虚拟滚动 +4. 避免在渲染函数中进行复杂计算,建议提前处理数据 + +## 下一步 + +学习完列表渲染后,你可以: + +1. 了解[事件处理](../event-handling)系统 +2. 探索[组件组合](../../components/composition)的技巧 +3. 学习[状态管理](../../reactivity/state)的最佳实践 \ No newline at end of file diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fd91b8bc3a9aa5b85f7d594f308f0e92a4f9f57d --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,16 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
      +

      出错了

      +

      {error.message}

      + +
      + ); +} \ No newline at end of file diff --git a/src/app/examples/page.tsx b/src/app/examples/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..028441bb27f4907a7c7d00e916a8b56d5996b41e --- /dev/null +++ b/src/app/examples/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +export default function ExamplesPage() { + return ( +
      +
      +
      +
      +

      + 示例库 +

      +
      +
      +
      + +
      +
      +
      +