笔者在阅读源码方面属于小白,希望从最简单的element-ui源码开始深入浅出,也希望我的学习源码的过程能给大家一些帮助和指导,对我所写的不正确的地方,欢迎评论
.github文件夹里面是关于GitHub的配置文件,比如issue的编辑模板build文件夹里面包含了脚手架,各环境下的打包逻辑,其中还有一个element自定义的md-loader,一会我细说examples文件夹里存放了使用element-ui组件制作的各种例子,它们就是我们在官网上看到例子packages文件夹存放的就是开发者们呕心沥血制作的UI组件,也是element的灵魂与核心src文件夹是element-ui的入口,通过这个入口将UI组件进行拆分和导出test文件夹是测试UI框架的鲁棒性的,使用的karma测试框架types文件夹是为了兼容ts的导入方式,UI组件的声明文件
上面我说到一个md-loader,这个东西挺有意思的,属于自定义的webpack的loader,它被用在webpack.demo.js配置中,该配置中会以examples文件夹作为入口,将docs文件夹下的md文件通过md-loader转换成vue模板语法的字符串再通过vue-loader最后输出到指定element-ui文件夹中
const isProd = process.env.NODE_ENV === 'production';
const isPlay = !!process.env.PLAY_ENV;
const webpackConfig = {
mode: process.env.NODE_ENV,
entry: isProd ? {
docs: './examples/entry.js'
} : (isPlay ? './examples/play.js' : './examples/entry.js'),
output: {
path: path.resolve(process.cwd(), './examples/element-ui/'),
publicPath: process.env.CI_ENV || '',
filename: '[name].[hash:7].js',
chunkFilename: isProd ? '[name].[hash:7].js' : '[name].js'
},
module: {
rules: [
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false // 是否去掉元素之间空格
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './examples/index.tpl',
filename: './index.html',
favicon: './examples/favicon.ico'
})
]
}
module.exports = webpackConfig;
md-loader
md-loader主要的作用是让webpack识别md文件,并将md文件转换成vue模板语法的格式,它是怎么做到的呢,看我慢慢分析!
我们从入口文件开始说,这也是md-loader实现语法转换的主要逻辑,我贴一下源码
const {
stripScript,
stripTemplate,
genInlineComponentText
} = require('./util');
const md = require('./config');
module.exports = function(source) {
const content = md.render(source);
const startTag = '<!--element-demo:';
const startTagLen = startTag.length;
const endTag = ':element-demo-->';
const endTagLen = endTag.length;
let componenetsString = '';
let id = 0; // demo 的 id
let output = []; // 输出的内容
let start = 0; // 字符串开始位置
let commentStart = content.indexOf(startTag);
let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
while (commentStart !== -1 && commentEnd !== -1) {
output.push(content.slice(start, commentStart));
const commentContent = content.slice(commentStart + startTagLen, commentEnd);
const html = stripTemplate(commentContent);
const script = stripScript(commentContent);
let demoComponentContent = genInlineComponentText(html, script);
const demoComponentName = `element-demo${id}`;
output.push(`<template slot="source"><${demoComponentName} /></template>`);
componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`;
// 重新计算下一次的位置
id++;
start = commentEnd + endTagLen;
commentStart = content.indexOf(startTag, start);
commentEnd = content.indexOf(endTag, commentStart + startTagLen);
}
// 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
// todo: 优化这段逻辑
let pageScript = '';
if (componenetsString) {
pageScript = `<script>
export default {
name: 'component-doc',
components: {
${componenetsString}
}
}
</script>`;
} else if (content.indexOf('<script>') === 0) { // 硬编码,有待改善
start = content.indexOf('</script>') + '</script>'.length;
pageScript = content.slice(0, start);
}
output.push(content.slice(start));
return `
<template>
<section class="content element-doc">
${output.join('')}
</section>
</template>
${pageScript}
`;
};
不管是哪个webpack的loader都需要默认暴露一个函数提供给webpack,element-ui也不能例外,函数可以接受三个参数,源文件内容,sourcemap对象以及meta元信息,一般处理文件内容只需要文件内容就可以,由于我们解析的md格式的文件内容,所以source就是element的markdown文件,它通过md变量的render方法解析成了html文件,这里我们再说一下./config.js文件里大致的逻辑
它主要使用了markdown-it-container插件和改写fence的默认渲染规则,拦截fence code blocks并对相关内容进行处理 什么是fence code blocks,个人理解为特殊的格式,便于解析和处理,像下面这样
:::demo description
``` info
content
```
:::
./containers.js文件
./fence.js文件
遇到fence之后,对fence里的内容进行处理,分别将description放于default默认插槽,content放入highlight插槽中,content就是官网上例子上我们看到的代码,还有一个source插槽用于显示组件渲染之后的样子,例如下面这个样子
我们从上图已经知道description和highlight插槽在何处插入的了,那页面上的source是什么时候放进去的呢,其实它就在入口进行判断的,你回去看源代码,逻辑是这样的
首先分析content的位置,在container插件拦截的时候对content进行了包装,把它变成了
<!--element-demo:content:element-demo-->
所以element是通过indexOf来进行判断并找到content的开始结束索引的
当存在上方格式的content时,将会把content拆分出来并且通过stripTemplate和stripScript将template和script分离保存,再调用genInlineComponentText将content编译成模板对象,像下面这样
这个demoComponentContent会以键值对的方式追加到componenetsString上,而键我们是以累加的形式去添加的,最后以vue的components声明为组件,像下面这样
在这个过程中,output把拆分的每一个content块都收集起来了,通过join合并起来输出给下一个vue-loader将vue模板语法转换成render函数,就是我们在element-ui官网上看到的那些组件页面了
就写到这里吧,大家退下吧