前端项目中那些让人头疼的使用示例踩坑总结
组件使用示例的那些坑
最近在重构一个组件库,碰到了一些使用示例的问题。说实话,这玩意儿看着简单,真要做好的话还是挺麻烦的,特别是要考虑各种边界情况和用户使用习惯。
最开始的想法很简单,就是在组件旁边加几个示例代码,然后让用户直接复制粘贴就能用。结果现实给了我一记响亮的耳光。
最开始的傻瓜做法
最开始我是这么做的:
<div class="component-demo">
<h3>基本用法</h3>
<div class="demo-code">
<pre><code><button class="btn">点击按钮</button></code></pre>
</div>
<div class="demo-result">
<button class="btn">点击按钮</button>
</div>
</div>
看起来还不错,对吧?但是问题很快就来了。用户复制代码的时候经常把外面那层 div 也复制进去了,而且代码格式也不太友好。更要命的是,如果组件需要引入 CSS 文件或者 JS 文件,这些依赖信息怎么展示给用户?
折腾了半天发现,这种静态的展示方式根本不够用。用户需要的是完整的可运行示例,包括样式、脚本、以及相关的配置说明。
动态渲染踩的坑
后来我想用动态渲染的方式,把代码字符串传进去,然后实时渲染出效果:
function renderDemo(codeString) {
const container = document.createElement('div');
container.innerHTML = codeString;
// 这里有个大坑,innerHTML 执行 script 标签会被忽略
const scripts = container.querySelectorAll('script');
scripts.forEach(script => {
const newScript = document.createElement('script');
newScript.textContent = script.textContent;
document.head.appendChild(newScript);
});
return container;
}
这里我踩了个坑,就是 innerHTML 会忽略 script 标签,需要用其他方式来执行。而且还有安全问题,用户输入的代码可能包含恶意脚本。虽然在组件库场景下风险不大,但还是得考虑 XSS 防护。
后来试了下 iframe 方案,确实比较安全,但是 iframe 的样式同步和通信又是一堆问题。组件库更新了样式,iframe 里的也要跟着更新,这事儿搞起来很复杂。
最终方案:沙箱环境 + 模拟导入
最后我还是用了 iframe + postMessage 的方案,这样比较干净,不会污染主页面的样式和脚本:
class DemoSandbox {
constructor(container, codeConfig) {
this.container = container;
this.codeConfig = codeConfig;
this.iframe = null;
this.createIframe();
}
createIframe() {
this.iframe = document.createElement('iframe');
this.iframe.style.width = '100%';
this.iframe.style.height = '300px';
this.iframe.style.border = '1px solid #e5e5e5';
this.iframe.style.borderRadius = '4px';
// iframe 内容生成
const iframeContent = this.generateIframeContent();
this.iframe.srcdoc = iframeContent;
this.container.appendChild(this.iframe);
// 监听 iframe 加载完成
this.iframe.onload = () => {
this.sendCodeToIframe();
};
}
generateIframeContent() {
return
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Component Demo</title>
<link rel="stylesheet" href="https://jztheme.com/styles/components.css">
<style>
body { margin: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
</style>
</head>
<body>
<div id="app"></div>
<script src="https://jztheme.com/libs/vue.min.js"></script>
<script>
window.addEventListener('message', function(event) {
if (event.data.type === 'RENDER_DEMO') {
const code = event.data.payload;
eval(code); // 注意:这里用 eval 是为了演示,实际生产中要更谨慎
}
});
</script>
</body>
</html>
;
}
sendCodeToIframe() {
const code = this.buildDemoCode();
this.iframe.contentWindow.postMessage({
type: 'RENDER_DEMO',
payload: code
}, '*');
}
buildDemoCode() {
const { template, style, script, imports } = this.codeConfig;
// 构建完整的 Vue 示例代码
return
new Vue({
el: '#app',
template: ${template},
${script ? script : ''}
});
;
}
}
这个方案的好处是隔离性好,缺点是要处理很多跨域和通信的问题。不过考虑到安全性和用户体验,这点复杂度还是值得的。
代码高亮和编辑功能
光有展示还不够,我还想让用户能够直接编辑代码看效果。这里我又遇到了一堆坑,主要是语法解析和错误处理的问题。
我用的是 Prism.js 来做代码高亮,但是要在运行时解析用户修改的代码并重新渲染,这就涉及到 AST 解析。本来想用 @babel/parser,但感觉太重了,就用了简单的正则匹配。
// 简单的代码块解析器
function parseDemoCode(sourceCode) {
const templateMatch = sourceCode.match(/<template>([sS]*?)</template>/);
const scriptMatch = sourceCode.match(/<script>([sS]*?)</script>/);
const styleMatch = sourceCode.match(/<style>([sS]*?)</style>/);
return {
template: templateMatch ? templateMatch[1].trim() : '',
script: scriptMatch ? scriptMatch[1].trim() : '',
style: styleMatch ? styleMatch[1].trim() : ''
};
}
// 代码编辑器
function setupCodeEditor(container, initialCode) {
const editorContainer = document.createElement('div');
editorContainer.className = 'code-editor';
const textarea = document.createElement('textarea');
textarea.value = initialCode;
textarea.className = 'editor-input';
textarea.addEventListener('input', debounce(() => {
try {
const parsedCode = parseDemoCode(textarea.value);
updateSandbox(parsedCode);
} catch (error) {
console.warn('代码解析失败:', error.message);
}
}, 500));
editorContainer.appendChild(textarea);
container.appendChild(editorContainer);
// 初始化语法高亮
Prism.highlightElement(textarea);
}
这里的 debounce 函数很重要,不然每次按键都会触发重渲染,性能受不了。还有就是错误处理,用户的代码可能会有问题,不能让整个页面挂掉。
依赖管理是个麻烦事
组件通常会有各种依赖,比如 CSS 类库、JS 库等等。怎么把这些信息准确地告诉用户?我一开始想直接在代码示例里显示完整的 import 语句,但这样会让代码变得很乱。
后来我搞了一个依赖面板,专门显示当前示例需要的所有依赖:
function createDependencyPanel(dependencies) {
const panel = document.createElement('div');
panel.className = 'dependency-panel';
const title = document.createElement('h4');
title.textContent = '所需依赖';
panel.appendChild(title);
const ul = document.createElement('ul');
dependencies.forEach(dep => {
const li = document.createElement('li');
li.innerHTML =
<code>${dep.name}</code>
<span class="version">${dep.version}</span>
<button onclick="copyToClipboard('${dep.installCmd}')">复制安装命令</button>
;
ul.appendChild(li);
});
panel.appendChild(ul);
return panel;
}
这样用户一眼就能看到需要安装什么包,比在代码里找要方便多了。不过依赖版本管理还是个问题,有时候新版本的库会有 breaking change,这时候示例代码就需要维护。
响应式和主题切换
还有个问题是响应式展示。有些组件在不同尺寸下表现不一样,用户需要能调整预览区域的大小。我也加了主题切换功能,毕竟现在暗色模式挺流行的。
这个倒是相对简单,主要是在 iframe 里加了一些响应式的控制按钮:
.preview-controls {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.preview-size-btn {
padding: 5px 10px;
border: 1px solid #ddd;
background: #f5f5f5;
cursor: pointer;
}
.preview-size-btn.active {
background: #007cba;
color: white;
}
JavaScript 里面监听按钮点击,然后调整 iframe 尺寸:
function setupResponsiveControls(iframe) {
const sizes = [
{ name: 'Mobile', width: '375px', height: '667px' },
{ name: 'Tablet', width: '768px', height: '1024px' },
{ name: 'Desktop', width: '100%', height: '400px' }
];
sizes.forEach(size => {
const btn = document.createElement('button');
btn.textContent = size.name;
btn.onclick = () => {
iframe.style.width = size.width;
iframe.style.height = size.height;
};
document.querySelector('.preview-controls').appendChild(btn);
});
}
整体来说,这事儿比我想象的复杂多了。不仅要考虑展示效果,还要考虑安全性、性能、用户体验各个方面。折腾了差不多一周才搞出个能用的版本。
当然还有一些细节问题没解决,比如错误边界处理还可以更好,代码编辑器的语法检查也可以更智能。但这套方案基本能满足日常需求了,至少用户可以直接复制示例代码并且能正常运行。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

暂无评论