Element UI源码中学到的技巧

8,813 阅读1分钟

本文是azuo和萌妹俩的第三篇创作,内容创作@azuo😄,精神支持@大头萌妹😂

导语Element UI 是世界级最优秀的UI框架之一。这个优秀的框架有哪些我们能学习的优点呢?这篇文章将分享作者在查看这个框架仓库源码中认为值得参考的技巧,建议配合element源码食用更佳。

技巧一:组件脚手架

脚手架在创建新组件的应用:规范代码目录,减少搬砖工作量 ,脚手架源码实现:build/bin/new.js

 # 执行命令,参数说明    
 # componentname 组件名 必填   
 # chineseName 组将中文名 选填 ,不填默认取值componentname  
 # node build/bin/new.js componentname [chineseName] 命令说明
 
 # 在element项目下执行
 node build/bin/new.js helloworld 示例

脚手架处理结果:

  • 1、组件样式处理:

    1.1 生成组件样式 packages/theme-chalk/src/${componentname}.scss

    1.2 样式入口文件packages/theme-chalk/src/index.scss 导入改组件样式

  • 2、组件代码处理:

    2.1 生成组件代码文件 :packages/componentname/index.jspackages/{componentname}/index.js和packages/{componentname}/src/main.vue

    2.2 新增组件的路径信息导入到 components.json ,该文件是json对象,存放了组件的名字和组件入口路径

  • 3、生成组件文档:

    3.1 生成examlpes/docs/{i18n}/component.md, 其中,i18n=['en-US','es','fr-FR','zh-CN']

    3.2 将新增的组件文档的标题和路径添加到 examples/nav.config.json,该文件是Element UI的组件文档的目录,保存了组件文档的标题和路由

  • 4、生成单元测试:

    4.1 生成单元测试文件:test/unit/specs/component.spec.js

  • 5、生成组件接口定义:

    5.1 生成组件的描述文件:types/component.d.ts

    5.2在types/element-ui.d.ts新增新组件的接口定义

一个优秀的组件,除了需要要把组件的代码写好,还有有单元测试、文档说明,最好,也有有接口定义(这编译器就有友好的使用提示),Element的组件起步就已经配齐了😄

技巧二:用代码来生成代码

源码入口文件生成:Element UI 目前一共有80个组件,如果要导出这80个组件,那么引入、导出和声明Vue组件的代码都要写240次,而且,组件的增删都要去修改入口文件。为了减少这部分工作量,基于components.json来生成入口文件的组件引入和导出。

components.json内容如下:

image.png

入口文件有三处代码(引入,导出和声名组件)都要重复了80次,下面以引入代码语句作为说明示例:

引入代码示例

/* Automatically generated by './build/bin/build-entry.js' */

import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
import Autocomplete from '../packages/autocomplete/index.js';
import Dropdown from '../packages/dropdown/index.js';
import DropdownMenu from '../packages/dropdown-menu/index.js';
import DropdownItem from '../packages/dropdown-item/index.js';
import Menu from '../packages/menu/index.js';
/**省略 70多个组件导入**/
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';

生成上述代码,主要逻辑:读入components.json作为数据,然后模版拼接起来,具体实现如下:


var Components = require('../../components.json');
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
var endOfLine = require('os').EOL;

// import 语句语法模版
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';

var ComponentNames = Object.keys(Components);
var includeComponentTemplate = [];

ComponentNames.forEach(name => {
  var componentName = uppercamelcase(name);
  includeComponentTemplate.push(
      // 单条import生成语句
      render(IMPORT_TEMPLATE, {
        name: componentName,
        package: name
      })
  );
});

console.info(includeComponentTemplate.join(endOfLine))

生成代码的实现:build/bin/build-entry.js

代码位置:src/index.js

技巧三:用md去写组件文档和示例

十分优雅的文档和示例书写方式:文档和示例统一写到markdown文件中,在通过编写md-loader,先转成html文件,再转成vue的组件,再渲染,这种做法笔者认为是太有意思。

先看一下 Element UI 的文档页面的框架:

image.png

路由组件逻辑(详细代码):

// 截取组件文档的路由代码片段
const LOAD_DOCS_MAP = {
  'zh-CN': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/zh-CN${path}.md`)),
    'zh-CN');
  },
  'en-US': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/en-US${path}.md`)),
    'en-US');
  },
  'es': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/es${path}.md`)),
    'es');
  },
  'fr-FR': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/fr-FR${path}.md`)),
    'fr-FR');
  }
};

注意到,路由加载的组件不 vue 组件,而是,一个 markdown 文件。这个过程,是在 webpack 打包过程中自定义一个 loader 来实现:markdown 转成 vue 来实现的。

详细实现:build/md-loader/index.js

接下来,将详细分享文档如何实现组件演示的效果:

image.png

第一步:扩展了markdown的container格式:demo

:::demo ${content}:::

实现代码:

const md = require('markdown-it')();
const fs = require('fs');
const path = require('path');
const mdContainer = require('markdown-it-container');

module.exports = md => {
  md.use(mdContainer, 'demo', {
    validate(params) {
      return params.trim().match(/^demo\s*(.*)$/);
    },
    render(tokens, idx) {
      const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
      if (tokens[idx].nesting === 1) {
        const description = m && m.length > 1 ? m[1] : '';
        const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';
        return `<demo-block>
        ${description ? `<div>${md.render(description)}</div>` : ''}
        <!--element-demo: ${content}:element-demo-->
        `;
      }
      return '</demo-block>';
    }
  });

  md.use(mdContainer, 'tip');
  md.use(mdContainer, 'warning');
};

const inputPath = path.resolve(__dirname, './alert.md');  
const outputPath = path.resolve(__dirname, './alert.html');  

const input = fs.readFileSync(inputPath, {encoding: 'utf8'});  
const ounput = md.render(input);  
fs.writeFileSync(outputPath, ounput, {encoding: 'utf8'});}  

执行结果:

image.png

提取注释内容转成组件的实例实现

第二步:将html转成ComponentDoc.vue

 <template>    
   <section class="content element-doc">        
    <h2>Alert 警告</h2>        
    <p>用于页面中展示重要的提示信息。</p>        
    <h3>自定义关闭按钮</h3>        
    <p>自定义关闭按钮为文字或其他符号。</p>  
    
    <!--示例展示 这个步是最关键-->    
    <demo-block>            
        <template name="source">              
        <element-demo0 />            
        </template>            
        <template name="default"></template>            
        <slot name="highlight"></slot>        
    </demo-block>  
    
  </section>
 </template>  

组件:<demo-block>,源码位置:examples/components/demo-block.vue

DemoBlockComponent效果展示如下图:

image.png

第三步: 示例效果展示

  • 第一种,组件仅仅只有模板,没有其他属性,就跟描述内容插槽一样,直接以插槽透传就行;
  • 第二种,是组件有script内容,怎么处理呢?如下图

image.png

组件代码是调用vue-template-compiler模块生成的,参照:build/md-loader/util.js:L30

技巧四:icon组件示例

Element UI 提供了280个icon,人工搬砖,写文档,又得加不少班呀。对于有追求的程序员,当然要有想法了。处理技巧:使用postcss模块解析icons的样式文件,提取出el-icon-XXX的className,将所有icon的className组装成数组中,保存到examples/icon.json

通过样式class提取icon名字,代码实现如下:

'use strict';
var postcss = require('postcss');
var fs = require('fs');
var path = require('path');
var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');
// 借助postcss解析css文件
var nodes = postcss.parse(fontFile).nodes;
var classList = [];

// 通过正则获取icon的name
nodes.forEach((node) => {  
    var selector = node.selector || '';  
    var reg = new RegExp(/\.el-icon-([^:]+):before/);  
    var arr = selector.match(reg);  
    if (arr && arr[1]) {    
    classList.push(arr[1]);  
}});
classList.reverse(); 
// 希望按 css 文件顺序倒序排列
console.info(classList.length);

fs.writeFile(
     path.resolve(__dirname, '../../examples/icon.json'), 
     JSON.stringify(classList)
 );  

技巧三种中是将icons的icons.md生成的ComponentDoc.vue组件,无法编写代码来传入icons数组,那就直接注入 Vue中原型链中

 // 文档入口引入
 import Vue from 'vue'
 import icon from './icon.json';
 Vue.prototype.$icon = icon;

icon文档书写

图标集合
<ul class="icon-list">
  <!--直接从原型链获取全部icon名字-->
  <li v-for="name in $icon" :key="name">
    <span>
      <i :class="'el-icon-' + name"></i>
      <span class="icon-name">{{'el-icon-' + name}}</span>
    </span>
  </li>
</ul>

效果:element.eleme.io/#/zh-CN/com…

image.png

一下子解决这么多的重复工作😄

技巧五:文档多语言

文档多语言,使用脚本生成每中语言,都单独生成一个vue模板,和技巧二类似,详细参考:build/bin/i18n.js