參考 react-live,如何實現代碼在線預覽?
目前很多平臺都支持嵌入 CodeSandbox、StackBlitz 等第三方在線代碼運行平臺,僅僅通過一個 iframe 標籤就可以完成,這個當然是很方便的,不過有時候我們只是想展示一些輕量級代碼,再加上經常網絡抽風,還需要我們在他們平臺維護一堆倉庫,不知道哪天自己手抖就刪了,導致文章中 demo 效果無法展示。那麼有沒有可能我們自己實現這種在線運行的功能呢,答案當然是可以。
應該提供哪些主要功能 🤔
編輯器組件應該與預覽組件分離,給予用戶只使用編輯器的機會,單獨使用編輯器時就是一個普通的代碼高亮展示組件,所以提供兩個單獨組件。爲了方便我們代碼中編輯器組件與預覽組件跨組件交換數據,我們再額外提供一個 Provider 組件,該組件負責爲編輯器組件與預覽組件包裹一個 ContextProvider,我們的配置都通過 Provider 組件傳入,大致寫法結構如下:
-
Provider 組件
-
ContextProvider 的再包裝,用於 Editor 與 Preview 的跨組件通訊
-
負責傳入參數配置
-
Editor 組件
-
支持代碼高亮
-
代碼更新後同步到 Context 中
-
Preview 組件
-
從 Context 中讀取最新代碼
-
需支持 react 組件預覽
-
除此之外,我還需要支持原生 html 的預覽
Provider 組件
上邊說 Provider 負責參數的統一傳入,讓我們看一下具體實現
import { useControllableValue } from 'ahooks'import React,
{ createContext, PropsWithChildren } from 'react'export
interface LiveProviderProps {
code?: string // 受控組件
defaultCode?: string // 非受控組件
language: string // 代碼語言
// 代碼運行需要的全局對象,僅在 language=jsx 時生效,
比如邊這塊代碼就需要保證 useState 能拿到纔可以運行
// function Count() {
// const [count, setCount] = useState(0)
// return (
// <>
// <p>count: {count}</p>
// <button onClick={() => setCount(count + 1)}>+1</button>
// </>
// )
// }
scope?: Record<string, any>
disabled?: boolean // 有時候我想禁止編輯
onCodeChange?: (code: string) => void}export interface Context
{ code: string
language: Language onCodeChange: (code: string) => void
scope: Record<string, any>
disabled: boolean}export const LiveContext = createContext
<Context>({} as Context)const LiveProvider: React.FC
<PropsWithChildren<LiveProviderProps>> = (props) =>
{ const { children, language, scope = {}, disabled = false }
= props // 這塊使用了 ahooks 中的 useControllableValue,
推薦去學習下如何使用
const [code, setCode] = useControllableValue(props,
{ defaultValue: '', defaultValuePropName: 'defaultCode',
valuePropName: 'code', trigger: 'onCodeChange',
}) function onCodeChange(newCode: string) {
setCode(newCode)
} return ( <LiveContext.Provider
value={{
code, language, onCodeChange,
scope, disabled,
}}
>
{children} </LiveContext.Provider>
)
}export default LiveProvider複製代碼
編輯器組件
編輯器組件僅負責代碼錄入不負責代碼編譯,只需要保證有代碼可編輯可高亮,編輯功能我這裏使用了 react-simple-code-editor,它是一個輕量級編輯器,或者也可以使用 use-editable,這裏就不具體進行了,代碼高亮這塊使用了 prism-react-renderer,兩者結合便可以實現一個基本的編輯器組件。
import React, { useContext } from 'react'
import Highlight, { defaultProps } from 'prism-react-renderer'
import theme from 'prism-react-renderer/themes/nightOwl'
import Editor from 'react-simple-code-editor'
import { LiveContext } from './LiveProvider'
const LiveEditor = () => {
const { code, disabled, language, onCodeChange } =
useContext(LiveContext)
return (
<Editor
value={code}
onValueChange={onCodeChange}
disabled={disabled}
tabSize={2}
padding={10}
style={{
fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: 12,
...theme.plain,
}}
highlight={(code) => (
<Highlight {...defaultProps} theme={theme} code=
{code} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps
}) => (
<>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</>
)}
</Highlight>
)}
/>
)
}
export default LiveEditor
效果預覽
截止到這裏,我們已經實現一個可編輯的代碼高亮組件了,如果目標只是如此,你就可以不繼續往下看了,當然我們目標應該不只如此,接下來考慮預覽組件怎麼做。
預覽組件
預覽組件相對 Editor 而言比較複雜,我們要考慮多種語言的情況,考慮下邊一串代碼,如何才能編譯成一個 react 組件?
不只 jsx 語法,我還要支持原生 html,爲了代碼可擴展性,首先讓我們把 Preview 組件細分一下再考慮具體不同語言的編譯。
在上邊組件中我們根據 Context 提供的 language 選項渲染出對應不同組件,接下來先看最麻煩的 react 怎麼渲染。
渲染 react
要實現運行時渲染 jsx 語法,我們需要藉助 sucrase,它可以在瀏覽器環境對代碼進行快速編譯,更多使用方式可以參考官方文檔。
import React, { useContext, useState, useEffect } from 'react'
import { LiveContext } from '../LiveProvider'
import { generateElement } from '../utils/transpile'
import { Transform, transform } from 'sucrase'
// 將編譯後的字符串變成可執行代碼,並且通過閉包方式將一些依賴傳入,
類似下邊這種,有種 UMD 模塊的感覺
// (function (name) {
// console.log(`hello ${name}`)
// })('張三')
function evalCode(code: string, scope: Record<string, any>) {
const scopeKeys = Object.keys(scope)
const scopeValues = Object.values(scope)
return new Function(...scopeKeys, code)(...scopeValues)
}
function generateNode({ code = '', scope = {} }) {
// 刪除末尾分號,因爲下邊會在 code 外包裝一個 return (code)
的操作,有分號會導致語法錯誤
const codeTrimmed = code.trim().replace(/;$/, '')
const opts = { transforms: ['jsx', 'imports'] as Transform[] }
// 前邊補上一個 return,以便下邊 evalCode 後能正確拿到生成的組件
const transformed = transform(`return (${codeTrimmed})`, opts).
code.trim()
// 編譯後只是一個字符串,我們通過 evalCode 函數將它變成可執行代碼
return evalCode(transformed, { React, ...scope })
}
// 如果是一個函數的話,說明它是一個組件,否則就是一個組件實例或者元素
function resolveElement(node: React.ReactNode) {
const Element =
typeof node === 'function' ? node : () => <>
{React.isValidElement(node) ? node : null}</>
return <Element />
}
const ReactPreview = () => {
const { code, scope } = useContext(LiveContext)
const [node, setNode] = useState<React.ReactNode>()
// 當 code 或者 scope 變了後都需要重新編譯代碼
useEffect(() => {
transpileAsync(code).catch(console.error)
}, [code, scope])
function transpileAsync(newCode: string) {
// - transformCode may be synchronous or asynchronous.
// - transformCode may throw an exception or return a
rejected promise, e.g.
// if newCode is invalid and cannot be transformed.
// - Not using async-await to since it requires targeting
ES 2017 or
// importing regenerator-runtime...
try {
// 這裏對比 react-live 簡化了部分代碼,只爲能更簡單看懂主流程,
react-live 編譯前允許使用者轉換處理代碼
const transformResult = newCode
return Promise.resolve(transformResult).then
((transformedCode) => {
// Transpilation arguments
const input = {
code: transformedCode,
scope,
}
// 一定要通過這種方式保存組件,因爲 setState 支持傳入一個
function,但是組件本身又是一個方法,直接通過 setState
(FunctionalElement)
// 會讓 react 以爲你傳入的組件是一個更新 state 的函數
setNode(() => generateNode(input))
})
} catch (e) {
return Promise.resolve()
}
}
return resolveElement(node)
}
export default ReactPreview
渲染 html
上邊我們介紹瞭如何預覽 react 代碼,那我如果我想預覽一些 html 代碼呢?這個相比渲染 jsx 就簡單多了。最簡單方式是使用 dangerouslySetInnerHTML
將 html 文本設置進一個元素,例如 <div dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }}></div>
,但是這種方式侷限性太大,不適用完整的 html 標籤,也沒有 window.onload
等一系列事件 ,所以我考慮使用 iframe 來達成這個需求,我們知道 iframe 需要一個 src url,大家是不是以爲我們必須把代碼轉化爲可訪問 url 纔行?我一開始也是這麼想的,初版使用的 createObjectURL
方式生成了一個 url,不過後邊發現其實有更簡單的方式,那就是使用 srcCode
屬性,直接將代碼傳入就行。。。
import React, { useContext } from 'react'
import { LiveContext } from '../LiveProvider'
const HtmlPreview = () => {
const { code } = useContext(LiveContext)
return code ? (
<iframe
srcCode={code}
frameBorder={0}
allowFullScreen
allow='accelerometer; ambient-light-sensor; camera;
encrypted-media; geolocation; gyroscope; hid; microphone;
midi; payment; usb; vr; xr-spatial-tracking'
/>
) : null
}
export default HtmlPreview
效果預覽
結尾
如上代碼只是對 react-live
的拙略模仿,並且爲了教程清晰有意刪減了很多功能,例如 ErrorBoundry
、noInline
,推薦各位有條件可以直接閱讀源碼,源碼代碼量很少且簡單易懂,偷偷的告訴你,react 官網也在使用它。後邊有機會我將記錄一下如何在 nextjs
中使用 markdown,並且在 markdown 中嵌入 react-live
組件。
參考
-
react-live
-
playground
來源:
https://juejin.cn/post/7110586128113074206
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/A1n2qhAbdqguWSoWl1RDxw