ElementUI源码系列三 - 学习gen-cssfile.js文件之自动创建组件的.scss文件与生成index.scss文件内容

1,197 阅读11分钟

写在开头

经过前两篇文章的学习,我们算是已经是把项目的基础架构给立起来了,支棱起来了xd。 (✪ω✪)

那这章我们来学啥呢?这章只有一个目标,就是学习在 element-ui 源码内某个文件的内容,它的目录:element-dev/build/bin/gen-cssfile.js

image.png

bin 目录是一个存放可直接运行文件的目录,里面有很多能够帮助我们提升效率的操作,有很多知识值得我们去学习,接下来会抽几个文件来介绍它们,来看看 ElementUI 团队在开发项目时是如何释放双手、高效率的去工作的。

这其中会涉及挺多 Node 知识,希望你最好能够具备一些 Node 方面的基础知识;不过,不会也没有关系啦,小编会仔细的写上注释,读懂应该是不难的哈,遇上不懂的也欢迎你给我留言评论哦。

057B70A9.gif

分割线组件实现 - Divider

在开始进入本章主题前,回顾一下我们现在的项目,里面只有一个 button 组件,有点寒酸了(ー_ー),为了后面更好的演示效果,我们为项目再添加一个组件 - 分割线组件(Divider)

之所以选这个组件,完全是因为它比较简单,还有就是整个源码系列的文章,重点内容并不是组件的具体实现细节,因为里面大部分组件拉出来单独讲,都能讲个长篇大论,比如小编之前写过的一篇 如何做出一个与ElementUI一样高质量的Toast组件? ,虽然没什么热度,但是组件还是非常好用的。( ̄▽ ̄)

那废话不多说,我们先来想一下,为项目新添加一个组件,我们要做些什么?我总结了如下这三步:

第一步 - 创建组件目录结构

创建分割线组件目录,分别由 divider/index.jsdivider/src/main.vue 文件组成。

image.png

index.js 文件还是一样做一个引入,并给按需引入留个入口。

// index.js
import Divider from './src/main.vue'; // 后缀不可省略
/* istanbul ignore next */
Divider.install = function(Vue) {
  Vue.component(Divider.name, Divider);
};
export default Divider;

main.vue 文件,这里比较简单,直接就是把源码给搬过来了。

<template functional>
  <div v-bind="data.attrs" v-on="listeners"
    :class="[data.staticClass, 'el-divider', `el-divider--${props.direction}`]" >
    <div v-if="slots().default && props.direction !== 'vertical'"
      :class="['el-divider__text', `is-${props.contentPosition}`]">
      <slot />
    </div>
  </div>
</template>

<script>
export default {
  name: "ElDivider",
  props: {
    // 分割线方向
    direction: {
      type: String,
      default: "horizontal",
      validator(val) {
        return ["horizontal", "vertical"].indexOf(val) !== -1;
      },
    },
    // 分割线文案的位置
    contentPosition: {
      type: String,
      default: "center",
      validator(val) {
        return ["left", "center", "right"].indexOf(val) !== -1;
      },
    },
  },
};
</script>

比较难的可能是关于函数式组件知识点:

  • functional函数式组件,这是一种 Vue 对组件的优化策略。近年来应该能常常听到所谓的组件化理念,它强调页面由一小块一小块组件构成,分而治之,这确实是个好东西;Vue 的组件化同样继承该理念,但是任何事物都具体两面性,利与弊,组件化带我们走出了复杂繁琐,但也带来了额外的“开销”。 我们知道,Vue 中每个组件就是一个实例,一个实例“开销”是很大的,比如,实例身上挂载了大量的实例方法,大量的响应数据等等,这都需要消耗资源的。如果任何页面功能都去分割成组件,即会有大量的组件实例,这消耗是恐怖的。为此,Vue 推出了函数式组件,对于简单的功能,没有任何状态管理的组件,我们可以把它定义成函数式组件,它并不会产生组件实例,也就是没有所谓的 this,它是无状态的,它能够有效的降低一些不必要的组件开销。(讲得有点多了,其实只要记住第一句就行了-.-)

  • v-bind="data.attrs":在普通组件中,没有被定义为 propattribute 会自动添加到组件的根元素上,将已有的同名 attribute 进行替换或与其进行智能合并。

    简单来说,就像下面的例子,我们给子组件添加了一个 id,正常普通组件,这个 id 会被加在子组件的根元素上;但是函数式组件不会,会忽略,函数式组件要求你显式定义该行为,也就是给子组件添加 v-bind="data.attrs"

    // parent.vue
    <template>
      <div>
        <child id="yd"></child>
      </div>
    </template>
    
  • v-on="listeners":它与 v-bind="data.attrs" 差不多一个意思,但是前者针对事件方法,后者针对的是属性。

  • slots().default:我们平常使用的默认插槽为 this.$slots.default,但是函数式组件规定的插槽对象是一个函数。

    image.png

第二步 - 创建组件样式文件

第二步,我们需要给新组件创建一个 .scss 文件,注意文件位置。

image.png

divider.scss 文件内容还是老样子,我们直接从 element-ui 源码中搬过来:

@import "common/var";
@import "mixins/mixins";

@include b(divider) {
  background-color: $--border-color-base;
  position: relative;
  @include m(horizontal) {
    display: block;
    height: 1px;
    width: 100%;
    margin: 24px 0;
  }
  @include m(vertical) {
    display: inline-block;
    width: 1px;
    height: 1em;
    margin: 0 8px;
    vertical-align: middle;
    position: relative;
  }
  @include e(text) {
    position: absolute;
    background-color: $--color-white;
    padding: 0 20px;
    font-weight: 500;
    color: $--color-text-primary;
    font-size: 14px;
    @include when(left) {
      left: 20px;
      transform: translateY(-50%);
    }
    @include when(center) {
      left: 50%;
      transform: translateX(-50%) translateY(-50%);
    }
    @include when(right) {
      right: 20px;
      transform: translateY(-50%);
    }
  }
}

这里它还使用了一些其他文件的内容,我们也对应去给它补全,要不等等打包环节可能会报错,这里你可以直接拷贝小编的样式添加到对应文件即可。

// minxins.scss
@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }
  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}
@mixin e($element) {
  $E: $element !global;
  $selector: &;
  $currentSelector: "";
  @each $unit in $element {
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
  }
  @if hitAllSpecialNestRule($selector) {
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}
// var.scss
$--color-text-primary: #303133 !default;

上面的 scss 样式并不是我们关注的重点,你可以全部照抄即可。如果你感兴趣可以学学 scss 的语法或者等下面打包生成它的 .css 文件,对照着看,应该是分分钟就能秒懂٩(๑❛ᴗ❛๑)۶。

写完组件的样式文件,还需要在总样式文件中引入:

// index.scss
@import "./button.scss";
@import "./divider.scss";

做完后,你可以先测试一下样式的使用是否正常,可以去执行 npm run build:theme 命令,看看 theme-chalk/lib 目录是否正常生成 divider.css 文件,index.css 文件内容是否正确。

image.png

第三步 - 总入口文件引入组件

做完第一、第二步骤,组件的准备工作也就做完了,最后一步就是需要引入这个组件,需要我们在项目根目录下的 src/index.js 中引入:

// src/index.js
import Button from '../packages/button/index.js';
import Divider from '../packages/divider/index.js';

const components = [
  Button, Divider
];
const install = (Vue) => {
  components.forEach(component => {
    Vue.component(component.name, component)
  })
}
export default {
  install,
  Button,
  Divider
}

最后,老样子,测试一下组件,测试步骤:

原本项目:
npm run build:theme
npm run build
npm pack
测试项目:
npm install 本地放置tgz包的全路径
npm run serve (重启项目)

测试代码:

<div>
  <span>头上一片晴天,心中一个想念</span>
  <el-divider content-position="left">少年包青天</el-divider>
  <span>饿了别叫妈, 叫饿了么</span>
  <el-divider></el-divider>
  <span>为了无法计算的价值</span>
  <el-divider content-position="right">阿里云</el-divider>
</div>

gen-cssfile.js

实现完分割线组件后,我们先来思考一个问题,为项目添加一个新组件,每次都需要经历上述的三个步骤,那么 ElementUI 有大量的组件,ElementUI 团队每次都是这样一个一个文件、目录去创建的吗?答案是否定的。

可以先提前告诉你最终答案,上述三个步骤都是能够被自动化处理的,因为它们都具备一定的相似性,而自动化处理它们的脚本都放在 ElementUI 源码下的 build/bin 目录下,我们本章学习的重点 "gen-cssfile.js" 文件也是其中之一,而最终我们只需通过一条条简单的命令去执行这些脚本即可完成操作。

本章节会先带你实现第二步骤自动创建组件的 .scss 样式文件与生成总样式文件 index.scss 的内容,第一、第三步骤将会在后续的文章讲解,敬请期待。

首先,先来把 build/bin/gen-cssfile.js 这个文件创建出来:

image.png

再给这个脚本文件在 package.json 文件中配置一条命令:

"scripts": {
  "gen": "node build/bin/gen-cssfile.js"
},

自动创建组件的.scss文件

要实现创建一个文件的过程,需要用到 Node 方面的知识,我们主要会用到这个 APIfs.writeFileSync('文件路径', '文件内容', '文件编码/错误回调'); ,它会把内容写入到一个文件中,而且当所给的文件路径不存在时,会自动创建文件。

我们先来看看下面的内容:

// gen-cssfile.js
var fs = require('fs');
fs.writeFileSync('./packages/theme-chalk/src/loading.scss', 
                `.el-loading{background-color: red}`, 'utf-8');

执行命令: npm run gen

可以看到对应路径下会有文件生成和内容:

image.png

了解了这个简单的小知识后,我们就可以直接来看看原 ElementUI 源码中 gen-cssfile.js 文件的内容了。

不过在那之前,我们得先在项目根目录下创建一个 components.json 文件,它的内容如下:

// `components.json`
{
  "button": "./packages/button/index.js",
  "divider": "./packages/divider/index.js",
  "loading": "./packages/loading/index.js",
  "alert": "./packages/alert/index.js"
}

那么这个文件又是干嘛用的呢?从上面的小知识可以知道,使用 fs.writeFileSync('./packages/theme-chalk/src/loading.scss'); 创建一个 .scss 文件,会用到一个文件路径,因为项目会有大量的组件,每个组件都需要创建一个 .scss 文件,所以我们在一个单独文件里面先对所有组件进行宏观定义,后面我们只需要引入这个文件,然后一个循环即可搞定,components.json 也相关于组件的配置文件了。

下面我们再来看看 gen-cssfile.js 的具体内容:

var fs = require('fs');
var path = require('path');
var Components = require('../../components.json'); // 引入组件的配置文件
Components = Object.keys(Components); // Components = ['button', 'divider', 'loading', 'alert']

/**文件类型:
 * 'theme-chalk': 表示自动生成的是 .scss 文件; 
 * 'theme-default': 表示自动生成的是 .css 文件.
 */
var themes = ['theme-chalk'];  
// 绝对路径: 定位到你项目的packages目录下
var basepath = path.resolve(__dirname, '../../packages/');

// 一个判断文件是否存在的工具函数
function fileExists(filePath) {
  try {
    return fs.statSync(filePath).isFile();
  } catch (err) {
    return false;
  }
}

// 重点: gen-cssfile.js文件执行, 只会跑这里的内容
themes.forEach((theme) => {
  // 判断是否为 .scss 文件, true 
  var isSCSS = theme !== 'theme-default';

  Components.forEach(function(key) {
    // 遇到这三种组件直接跳过, 不为其创建 .scss
    if (['icon', 'option', 'option-group'].indexOf(key) > -1) return;
    // 拼接文件名, 名称+后缀
    var fileName = key + (isSCSS ? '.scss' : '.css');
    // 拼接路径: '../../packages/' + 'theme-chalk' + src + 文件名
    var filePath = path.resolve(basepath, theme, 'src', fileName);
    // 只有当组件的 .scss 文件未存在才会进行创建
    if (!fileExists(filePath)) {
      fs.writeFileSync(filePath, '', 'utf8');
      console.log(theme, ' 创建遗漏的 ', fileName, ' 文件');
    }
  });
});

上面代码量不多,小编都写上了注释,就是不知道你能读懂多少成了?(◕ˇ∀ˇ◕) 如果看了注释都不懂的,那么你可以不断执行 npm run gen 命令慢慢调试理解。

我们执行 npm run gen 命令,可以发现,缺的组件样式文件会帮我们自动创建了,并且控制台也有相应的提示。

image.png

image.png

自动生成总样式index.scss文件

上面我们实现了自动创建组件的 .scss 样式文件,但这还没完,自动创建的样式文件还需要在总样式文件 index.scss 中引入才能生效。

所以我们还要再改改 gen-cssfile.js 文件:

...

themes.forEach((theme) => {
  var isSCSS = theme !== 'theme-default';
  // index.scss文件开头第一句默认引入base.scss文件, base.scss文件为图标或者动画样式
  var indexContent = isSCSS ? '@import "./base.scss";\n' : '@import "./base.css";\n';

  Components.forEach(function(key) {
    if (['icon', 'option', 'option-group'].indexOf(key) > -1) return;
    var fileName = key + (isSCSS ? '.scss' : '.css');
    // 组件在 index.scss 中引入
    indexContent += '@import "./' + fileName + '";\n';
    var filePath = path.resolve(basepath, theme, 'src', fileName);
    if (!fileExists(filePath)) {
      fs.writeFileSync(filePath, '', 'utf8');
      console.log(theme, ' 创建遗漏的 ', fileName, ' 文件');
    }
  });
  // 把 indexContent 内容写入 index.scss 中, 每次都进行覆盖
  fs.writeFileSync(path.resolve(basepath, theme, 'src', isSCSS ? 'index.scss' : 'index.css'), indexContent);
});

一共加了三行代码,然后再次执行 npm run gen 命令,可以发现,自动创建的样式文件也在总样式文件中实现了自动引入,大功告成。当然我们再手动给补创建一个 base.scss 文件,防止打包出现报错。

image.png

到此,我们就基本实现了组件样式文件的自动化创建与引入了,你可以重新在 components.json 中定义一个新组件,再走整个测试过程自己玩玩。

不过,这或许看起来并不能提高多大的效率啊?这原因嘛?当然是因为还没讲完!!!继续学习后续的文章,你就能切身体会了,先留个期待吧。

0740324C.jpg




往期内容




至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。