前言
好久没静下来记记近期学到的一些前端集成脚手架的技术知识点了, 上一篇文章 从零实现属于自己的前端脚手架,简单地介绍了利用命令行交互快速创建项目模版的实现;这一篇打算记一下组件文档如何开发,再不记就该忘光了。
组件文档
在前端开发领域,组件库是一种提高开发效率和项目质量的重要手段。一个优秀的组件库可以为开发者提供丰富的功能,降低实现复杂交互和视觉效果的难度。相应的,组件文档是描述组件如何使用的关键,作为码农,肯定看过一些库的文档写得很烂而吐槽吧(有些公司内部组件,组件文档就只有README.md,没有组件示例,新接触的开发者还得去看下源码实现才知道如何调用0.0)。废话过多了...,接下来准备讲讲我瞎捣鼓的组件文档方案(以Vue开发的组件库作为示例)~
组件文档预览效果
以下是对应的README.md文档
# 组件1的使用文档
如何使用组件
<!-- 下面这段代码是组件示例能够展现的关键,下文将讲解如何实现 -->
::: vue examples/component1/demo/index.vue :::
### 功能介绍
### 使用方法
### 使用示例
### 参数
#### props
| 参数 | 说明 | 类型 | 必选 | 可选值 | 默认值 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| ---- | ---- | ---- | ---- | ---- | ---- |
#### 事件
| 事件名称 | 说明 | 回调参数 |
| ---- | ---- | ---- |
| ---- | ---- | - |
想实现上述效果,要实现的内容分为两部分:
- 解析markdown内容,在浏览器加载
- 将示例组件内嵌到markdown内容的指定位置
解析markdown内容
作为一个前端切图仔,这部分内容只能站在巨人的肩膀上开发(缝合怪哈哈)。
需要用到的库包括:
- markdown-it 解析md语法,转义成html语法
- markdown-it-container 解析::: ::: 语法(相当于你可以在md中编写你定义md语法)
- highlight.js 代码高亮
如何将示例组件内嵌到md中
既然我们使用markdown-it库解析md语法,将其转为dom元素,那我们可以想一想,将示例组件也转为dom元素,插到解析完之后的md内容中。
编写一个webpackLoader,以下是思路:
- 解析md文件,将其转为vue-loader能识别的内容,也就是md文件转vue文件,后续需要用到这个文件,暂记为
main.vue
(将解析后的md内容,用<tempalte></tempalte>
包裹) - 解析
::: vue examples/component1/demo/index.vue :::
语法,将vue
后面路径对应的示例组件注册到main.vue
中,这样就可以显示示例组件的预览啦。 - 最后再用普通vue项目加载根文件的方式加载
main.vue
,就能实现组件文档的预览啦(编译也是一样的)
markdown-loader的实现
webpack loader
本质是一个函数,用来处理输入的参数,输出想要的数据。按照上面的思路,函数的逻辑大致如下:
// loader-utils 用于获取loader的配置参数,schema-utils用于校验loader的配置参数
const { getOptions } = require("loader-utils");
const { validate } = require("schema-utils");
const schema = {
type: "object",
properties: {
framework: {
type: "string",
},
options: {
type: "object",
},
},
};
module.export = function() {
const options = getOptions(this);
validate(schema, options);
return new MdParser({
source,
options
}).parse();
}
我们要实现MdParser
类,需要考虑以下几个问题:
- 通过配置参数,可以将md内容处理成
vue
、react
...等文件(支持多语言嘛,虽然我们现在只考虑vue) - md中代码块的高亮配置
- md中示例组件的插入
- md中示例组件代码的展示
MdParser
的实现
class MdParser {
constructor({ source, options = {} }) {
// 原文件信息
this.source = source;
// 选项处理
this.options = Object.assign({ domId: "component-docs" }, options, {
markdownOption: {
// 默认的md解析配置(只做了高亮代码块)
...DEFAULT_MARKDOWN_OPTIONS,
// 自定义md解析配置
...options?.markdownOption,
},
});
// 记录MD插槽路径,也就是示例组件的路径
this.filePaths = [];
// 初始化MD解析器
this.markdown = new MarkdownIt(this.options.markdownOption);
// 初始化MD解析器插件
MdParser.useMarkdownItContainer.apply(this);
}
}
DEFAULT_MARKDOWN_OPTIONS
配置如下(高亮代码):
const DEFAULT_MARKDOWN_OPTIONS = {
html: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return (
'<pre v-pre class="hljs"><code>' +
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
"</code></pre>"
);
} catch (err) {
// ignore
}
}
return ""; // use external default escaping
},
};
md中示例组件的插入
上面说过,解析::: vue examples/component1/demo/index.vue :::
语法,将vue
后面路径对应的示例组件注册到main.vue
中,这样就可以预览示例组件。那么useMarkdownItContainer
方法就是用来处理示例组件路径的。
const NEW_LINE = "\r\n";
class MdParser {
// ...
static useMarkdownItContainer() {
let { framework = "vue" } = this.options;
framework = framework.toLocaleLowerCase();
const mdParserInstance = this;
this.markdown.use(MarkdownItContainer, framework, {
validate(params) {
return params.trim().match(/^vue\s+(.*)$/);
},
// tokens参数值是所有解析::: :::之后的内容,idx是对应当前render要处理的是第几个
render(tokens, idx) {
const str = tokens[idx].info.trim().match(/\s+(.*):::$/);
if (tokens[idx].nesting === 1) {
const pathTemp = str[1].trim();
// 拿到示例组件的路径
const filePath = path.resolve("./", pathTemp);
if (!fs.existsSync(filePath)) {
console.log("示例组件路径错误:", str[1]);
return "\n";
} else {
// 这里为了兼容可能不同路径下会有文件名相同,直接将路径+文件名定义为后面要注册的组件名称
/** e.g:
* examples/demo1/index.vue
* examples/demo2/index.vue
* 若直接取文件名当做组件名导入和注册,会报错
* 所以最后注册组件时,导入的形式是这样的
* import examples_demo1_index from 'xxx'
*/
const name = pathTemp.replace(/\//g, "_").split(".")[0];
// 过滤重名组件,避免重复注册
if (!that.filePaths.find((item) => item.name === name)) {
that.filePaths.push({
// 这里多增加转义\,是因为我们输出的是字符串文本,
// 只不过内容格式为vue文件格式,没转义的话,最后给到vue-loader,\\会变成\。
// 我们要确保输出的路径最终被消费的时候是跟输入进来的一样是\\。
path: filePath.replace(/\\/g, "\\\\"),
name: switchCompName(name),
});
}
return `<CodePanel code="${enCodeStr(
filePath
)}"><${name}/> ${NEW_LINE}
<template v-slot:code>
${addCode(filePath, that.options.markdownOption)}
</template> ${NEW_LINE}
</CodePanel> ${NEW_LINE}
`;
}
} else {
return "";
}
}
}
}
}
如果留心的话,会发现useMarkdownItContainer
方法返回的字符串中,用到了<CodePanel></CodePanel>
、enCodeStr
、addCode
,这些是干什么的呢?
CodePanel
这是实现了一个简单的组件展示、代码预览的功能组件,类似elementUI的组件示例(::: ::: 的思路就是来源于element plus的组件文档编译)。 思路很简单:
提供一个插槽,展示示例组件,将示例组件的代码通过props.code传进来,点击复制按钮,拷贝到剪切板上。
<template>
<div class="panel-container">
<div class="component-main">
<slot>示例组件的插槽</slot>
</div>
<div class="code-main">
<div class="options">
<i class="option-item" title="复制源代吗" @click="copyCode">
<svg
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
width="1.2em"
height="1.2em"
data-v-65a7fb6c=""
>
<path
fill="currentColor"
d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z"
></path></svg
></i>
<i class="option-item" title="查看源代吗" @click="switchShowCodePanel">
<svg
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
width="1.2em"
height="1.2em"
data-v-65a7fb6c=""
>
<path
fill="currentColor"
d="m23 12l-7.071 7.071l-1.414-1.414L20.172 12l-5.657-5.657l1.414-1.414L23 12zM3.828 12l5.657 5.657l-1.414 1.414L1 12l7.071-7.071l1.414 1.414L3.828 12z"
></path>
</svg>
</i>
</div>
<div
ref="transBox"
:class="{ 'transition-box': true, 'hide-box': isShowCodePanel }"
>
<slot name="code"></slot>
<div class="code-button" @click="isShowCodePanel = false">
<i style="width: 18px; height: 18px; margin-right: 8px"
><svg
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
data-v-65a7fb6c=""
>
<path
fill="currentColor"
d="M512 320 192 704h639.936z"
></path></svg
></i>
收起源代码
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "CodePanel",
props: {
code: {
type: String,
default: " ",
},
},
data() {
return {
isShowCodePanel: false,
};
},
methods: {
copyCode() {
const input = document.createElement("textarea");
input.value = decodeURI(this.code);
console.log(decodeURI(this.code));
input.style.transform = "translateX(-1000px)";
document.body.appendChild(input);
input.select();
document.execCommand("Copy");
document.body.removeChild(input);
},
switchShowCodePanel() {
this.isShowCodePanel = !this.isShowCodePanel;
this.$refs.transBox.style.setProperty("--max-height", 0 + "px");
this.$nextTick(() => {
const height = this.$refs.transBox.scrollHeight;
this.$refs.transBox.style.setProperty("--max-height", height + "px");
});
},
},
};
</script>
<style scoped>
.panel-container .component-main {
padding: 10px;
border: 1px solid #ddd;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.panel-container .code-main {
padding: 10px;
border: 1px solid #ddd;
border-top: transparent;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
overflow: hidden;
}
.options {
padding: 0 10px;
text-align: right;
}
.options .option-item {
cursor: pointer;
margin: 0 4px;
}
.options .option-item:active {
color: #409eff;
}
.transition-box {
transform-origin: center top;
transition: max-height 0.3s ease;
will-change: max-height;
overflow: hidden;
max-height: 0;
}
.hide-box {
max-height: var(--max-height);
}
.code-button {
display: flex;
justify-content: center;
align-items: center;
margin-top: 8px;
padding-top: 8px;
cursor: pointer;
border-top: 1px solid #ddd;
}
.code-button:active {
color: #409eff;
}
</style>
enCodeStr
和 addCode
addCode
将示例组件的内容转为代码块enCodeStr
将示例组件的内容转为字符串
const addCode = (path, markdownOption) => {
const fileStr = fs.readFileSync(path).toString();
const md = new MarkdownIt(markdownOption);
return md.render(`
\`\`\`html
${fileStr}
\`\`\`
`);
};
const enCodeStr = (path) => {
const fileStr = fs.readFileSync(path).toString();
return encodeURI(fileStr);
};
到这里,我们已经完成了代码高亮、示例组件的展示与代码内容的复制。接下来就是实现最重要的一部分,将输入的md内容转为vue格式内容。
MD解析方法:parse
上面讲过,md内容解析成vue
格式的内容,然后示例组件注册成vue组件,最后输出vue
格式的内容。
那我们就需要处理生成template,script,style
三块内容。
class MdParser {
// ...
// 解析MD
parse() {
// 定义结果对象,包括template,script,style三个部分
let result =
typeof this.options.process === "function"
? this.options.process(this.source)
: {
template: this.source,
script: "",
style: ""
};
// md内容解析
const html = this.markdown.render(result.template);
// 将packages组件注册为全局组件(怎么方便怎么来),这是为了解决某些组件相互引用又没注册(仁者见仁,这不是必要的)
定义二维数组,用来存import 和 Vue.component()
const globalUseComps = [[], []];
const pwd = process.cwd();
const compDirPath = path.resolve(pwd, "packages");
if (fs.statSync(compDirPath).isDirectory) {
const compDirs = fs.readdirSync(compDirPath);
compDirs.forEach((item) => {
const cuttentItem = path.resolve(compDirPath, item, "index.vue");
if (fs.existsSync(cuttentItem)) {
const componentName = switchCompName(item);
globalUseComps[0].push(
`import ${componentName} from '${cuttentItem.replace(
/\\/g,
"\\"
)}';`
);
globalUseComps[1].push(
`Vue.component('${componentName}', ${componentName});`
);
}
});
} else {
throw new Error(`没找到${compDirPath}路径`);
}
// 默认解析MD之后需要动态生成的script
const scriptStr = `
import Vue from 'vue';
${globalUseComps[0].join(" ")}
${globalUseComps[1].join(" ")}
import CodePanel from '${path
.resolve(__dirname, "./code-panel.vue")
.replace(/\\/g, "\\\\")}';
${this.filePaths
.map((item) => `import ${item.name} from '${item.path}'`)
.join(";")}
export default {
components: {
CodePanel,
${this.filePaths.map((item) => item.name).join(",")}
}
}
`;
// 预留自定义处理函数
if (typeof this.options.process === "function") {
result.script = `<script>
${result.script || ""}
${scriptStr}
</script>
`;
result.style = `<style>${
result.style ||
`
pre {
padding: 10px;
background: #DDD;
}
`
}</style>`;
} else {
result = {
template: this.source,
script: scriptStr,
style: `<style>
pre {
padding: 10px;
background: #DDD;
}
</style>`,
};
}
// 判断使用的框架
const { framework = "vue" } = this.options;
let fileContent = `${framework}暂未实现`;
switch(framework) {
case 'vue':
vueFile = `
<template>
// id 是留给文档站点用的,以后有时间再另起一篇讲讲
<div id="${this.options.domId || "component-docs"}" class="markdown-body">
${html}
</div>
</template>
${result.script}
${result.style}
`;
break;
default:
break;
}
return fileContent;
}
}
到这里,整个markdown-loader 算是基本完成了,再在组件库项目vue.config.js
配置文件加上:
module.exports = {
pages: {
index: "src/readme/index.js",
}
chainWebpack: (config) => {
config.entry("index").clear().add(mainPath);
delete config.entry.app;
config.module
.rule("markdown")
.test(/.md$/)
.use("vue-loader")
.loader("vue-loader")
.end()
.use("markdown-loader")
.loader("markdown-loader的路径")
.options({
framework: "vue",
})
.end();
};
}
// src/readme/index.js
import Vue from 'vue'
import './style.css'
import 'highlight.js/styles/github.css';
import md from '手动修改要预览的组件readme路径'
new Vue({
render: h => h(md),
}).$mount('#component-docs')
最后预览这一步,目前很麻烦,需要手动修改路径;接下来会在前端集成脚手架之组件文档预览与编译(二)
中讲解:
- 如何高效预览组件
- 如何结合vue-cli,集成配置封装成命令行指令
结语
摸爬滚打切图仔~ 写得不好不对的地方,请大家批评指正。
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!代办报建
本公司承接江浙沪报建代办施工许可证。
联系人:张经理,18321657689(微信同号)。
14条评论
东方不败还是灭绝师太啊?http://k17g7.shenzsnytl.com/2024/5.html
支持一下!http://tiqr.cqyiyou.net/test/081284773.html
好帖子!http://test.cqyiyou.net/test/
十分赞同楼主!http://48w1zi.bszhonyigc.com
这位作者的文笔极其出色,用词精准、贴切,能够形象地传达出他的思想和情感。http://99up.7cotton.com
楼主写的很经典!http://ip35r7.momei365.com
收藏了,很不错的内容!http://1hcex1.qmcxsd.com
楼主主机很热情啊!http://2tuo.ycsy11.com
不是惊喜,是惊吓!http://28kqr.chlhq.com
顶顶更健康!http://www.guangcexing.net/voddetail/TwAhtxSjph.html
支持一下,下面的保持队形!http://www.lw400.com/
顶顶更健康!https://www.skypeis.com/
有机会找楼主好好聊聊!http://tflbazaar.com/html/48c98998962.html
经典,收藏了!http://v26mk.shzxdw.com
发表评论