Skip to content

Commit

Permalink
feat: 自定义 JSX Runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
c0dedance committed Oct 26, 2023
1 parent 439899b commit 6b0422c
Show file tree
Hide file tree
Showing 11 changed files with 66 additions and 12 deletions.
1 change: 0 additions & 1 deletion src/node/babel-plugin-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default declare((api) => {
const visitor: Visitor<PluginPass> = {
// 核心逻辑实现
JSXOpeningElement(path, state) {
console.log(JSON.stringify(path.node, null, 4))
const { node, scope } = path
const component = node.name
// 组件名字,如 <Aside />: Aside; <Side.Aside />: Side;
Expand Down
4 changes: 2 additions & 2 deletions src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function renderPage({
await Promise.all(
routes.map(async (r) => {
// 渲染路由对应的页面
const appHtml = render(r.path)
const appHtml = await render(r.path)
// 组件HTML嵌入到模板中
const html = renderIndexHTML(appHtml)
// htlm文件名处理
Expand Down Expand Up @@ -104,7 +104,7 @@ async function resolveBuildConfig({
return {
mode: 'production',
root,
plugins: await createVitePlugins(config),
plugins: await createVitePlugins(config, null, isSSR),
ssr: {
// 注意加上这个配置,防止 cjs 产物中 require ESM 的产物,因为 react-router-dom 的产物为 ESM 格式
noExternal: ['react-router-dom', 'lodash-es'], // 加入编译
Expand Down
13 changes: 11 additions & 2 deletions src/node/vitePlugins.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import path from 'path'
import pluginReact from '@vitejs/plugin-react'
import pluginUnocss from 'unocss/vite'
import { pluginIndexHtml } from './plugin-r-press/indexHtml'
import { pluginConfig } from './plugin-r-press/config'
import { pluginRoutes } from './plugin-routes'
import { createPluginMdx } from './plugin-mdx'
import pluginUnocss from 'unocss/vite'
import babelPluginIsland from './babel-plugin-island'
import unocssOptions from './unocssOptions'
import { ROOT } from './constant'

import type { SiteConfig } from 'shared/types'

export async function createVitePlugins(
config: SiteConfig,
restartServer?: () => Promise<void>
restartServer?: () => Promise<void>,
isSSR: boolean = false
) {
return [
pluginUnocss(unocssOptions),
pluginIndexHtml(),
pluginReact({
jsxRuntime: 'automatic',
jsxImportSource: isSSR ? path.join(ROOT, 'src', 'runtime') : 'react',
babel: {
plugins: [babelPluginIsland],
},
}),
pluginConfig(config, restartServer),
pluginRoutes({
Expand Down
1 change: 0 additions & 1 deletion src/runtime/client-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ async function renderInBrowser() {
}
// 初始化 PageData
const pageData = await initPageData(location.pathname)
console.log(pageData, 'pageData')

createRoot(containerEl).render(
<DataContext.Provider value={pageData}>
Expand Down
40 changes: 40 additions & 0 deletions src/runtime/jsx-runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as jsxRuntime from 'react/jsx-runtime'

// 拿到 React 原始的 jsxRuntime 方法,包括 jsx 和 jsxs
// 注: 对于一些静态节点,React 会使用 jsxs 来进行创建,优化渲染性能
const originJsx = jsxRuntime.jsx
const originJsxs = jsxRuntime.jsxs

export const data = {
islandProps: [], // Islands 组件的数据
islandToPathMap: {}, // 记录 Island 组件的路径信息 id -> path(importer & import path)
}
const internalJsx = (jsx, type, props, ...args) => {
// 如果发现有 __island 这个 prop,则视为一个 Island 组件,记录下来
if (props && props.__island) {
data.islandProps.push(props)
const id = type.name
data.islandToPathMap[id] = props.__island

delete props.__island
// 对 island 组件包裹一层div,并通过 __island 进行标识 Islands 组件的 ID 以及 props 数据在 islandProps 中的位置。
return jsx('div', {
__island: `${id}:${data.islandProps.length - 1}`, // 自增的id
children: jsx(type, props, ...args),
})
}
// 否则走原始的 jsx/jsxs 方法
return jsx(type, props, ...args)
}

// 下面是我们自定义的 jsx 和 jsxs
export const jsx = (...args) => internalJsx(originJsx, ...args)

export const jsxs = (...args) => internalJsx(originJsxs, ...args)

export const Fragment = jsxRuntime.Fragment // jsx-runtime 文件中,不能缺少 Fragment 的导出

export const clearIslandData = () => {
data.islandProps = []
data.islandToPathMap = {}
}
4 changes: 4 additions & 0 deletions src/runtime/ssr-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { DataContext } from './hooks'
export async function render(pagePath: string) {
// 生产 pageData
const pageData = await initPageData(pagePath)
// 清除 islands 数据
const { clearIslandData } = await import('./jsx-runtime')
clearIslandData()

return renderToString(
<DataContext.Provider value={pageData}>
<StaticRouter location={pagePath}>
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/theme-default/Layout/DocLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function DocLayout() {
<DocFooter />
</div>
<div className={styles.asideContainer}>
<Aside headers={toc} />
<Aside headers={toc} __island />
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/theme-default/components/Aside/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useEffect } from 'react'
import { bindingAsideScroll, scrollToTarget } from '../../utils/asideScroll'
import { useHeaders } from '../../utils/useHeaders'
import type { Header } from 'shared/types'
import type { Header, PropsWithIsland } from 'shared/types'

interface AsideProps {
headers: Header[]
}

export function Aside(props: AsideProps) {
export function Aside(props: AsideProps & PropsWithIsland) {
const { headers: rawHeaders = [] } = props
// 是否展示大纲栏
const { headers } = useHeaders(rawHeaders)
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/theme-default/components/Nav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SwitchAppearance } from '../SwitchAppearance'
import styles from './index.module.scss'
import type { NavItemWithLink } from 'shared/types'

export function MenuItem(item: NavItemWithLink) {
export function MenuItem({ item }: { item: NavItemWithLink }) {
return (
<div className="text-sm font-medium mx-3">
<a href={item.link} className={styles.link}>
Expand Down Expand Up @@ -38,7 +38,7 @@ export function Nav() {
{/* 普通菜单 */}
<div flex="~">
{nav.map((item) => (
<MenuItem {...item} key={item.text} />
<MenuItem item={item} key={item.text} />
))}
</div>
{/* 白天/夜间模式切换 */}
Expand Down
1 change: 0 additions & 1 deletion src/runtime/theme-default/utils/useHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export function useHeaders(initHeaders) {
import.meta.hot.on(
'mdx-changed',
({ filePath }: { filePath: string }) => {
console.log(filePath, 'filePath')
// 1. 导入最新的模块
import(/* @vite-ignore */ `${filePath}?import&t=${Date.now()}`).then(
(module) => {
Expand Down
4 changes: 4 additions & 0 deletions src/shared/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { UserConfig as ViteUserConfig } from 'vite'
import type { ComponentType } from 'react'

export type PropsWithIsland = {
__island?: boolean
}

export interface PageModule {
default: ComponentType
frontmatter?: FrontMatter
Expand Down

0 comments on commit 6b0422c

Please sign in to comment.