element-ui[2.15.x]源码从开始到放弃(一)-项目结构

216 阅读3分钟

笔者在阅读源码方面属于小白,希望从最简单的element-ui源码开始深入浅出,也希望我的学习源码的过程能给大家一些帮助和指导,对我所写的不正确的地方,欢迎评论

image.png

  • .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文件 image.png

./fence.js文件 image.png

遇到fence之后,对fence里的内容进行处理,分别将description放于default默认插槽,content放入highlight插槽中,content就是官网上例子上我们看到的代码,还有一个source插槽用于显示组件渲染之后的样子,例如下面这个样子

image.png

我们从上图已经知道descriptionhighlight插槽在何处插入的了,那页面上的source是什么时候放进去的呢,其实它就在入口进行判断的,你回去看源代码,逻辑是这样的

首先分析content的位置,在container插件拦截的时候对content进行了包装,把它变成了

<!--element-demo:content:element-demo-->

所以element是通过indexOf来进行判断并找到content的开始结束索引的

image.png

image.png

当存在上方格式的content时,将会把content拆分出来并且通过stripTemplate和stripScript将template和script分离保存,再调用genInlineComponentText将content编译成模板对象,像下面这样

image.png

这个demoComponentContent会以键值对的方式追加到componenetsString上,而键我们是以累加的形式去添加的,最后以vue的components声明为组件,像下面这样

image.png

在这个过程中,output把拆分的每一个content块都收集起来了,通过join合并起来输出给下一个vue-loader将vue模板语法转换成render函数,就是我们在element-ui官网上看到的那些组件页面了

image.png

就写到这里吧,大家退下吧