写一个自己的组建库,先从element UI 组件库入手分析(一)

组件库需求背景

  • 提高开发效率
  • 统一整体 UI风格
  • 快速上手,提高了代码的可维护性

组件库设计

在设计组件库时,需要转化自己的角色。如果你从来没有组件库的开发经验,可以先思考自己在使用组件库时有哪些好用的功能,并记录使用组件库的一些痛点。在开发组件库时,你的角色从原来的一个使用者变成设计者,此时就要逆向考虑如何实现上述的好功能并且完善或解决一些痛点。

  • 自动化打包流程-区分模块
  • 多语言
  • 组件文档示例
  • 版本修改记录
  • 自动化创建组件
  • 按需加载
    • babel-plugin-component
  • 单元测试

开发组件库和使用脚手架开发项目有所不同,其中最大的区别就是组件库有很多的自动化打包流程,这句话该如何理解呢?

当我们在开发项目时,使用 vuereact 的脚手架就能满足日常的打包(npm run devnpm run build),这些工具脚手架都帮我们内置好的,用户直接使用即可。但是组件库不同,这些功能都需要你作为开发者时一一实现的,其他方面亦是如此。接下来我们分析 element UI 的设计风格,看从中能学习到什么。

element UI 组件库

这里采用的 elementUI版本为v2.15.12,下载源码打开后我们第一个切入点是先看看它的目录结构。

目录作用:

  • element ui 使用 build 文件夹用来存放工程化的内容以及打包工具的配置文件;
  • examples 存放 Element UI 组件示例;
  • packages 存放组件源码,也就是我们后面重点要分析的目标;
  • src 存放入口文件以及辅助文件;
  • test 存放单元测试文件,合格的单元测试是一个成熟的开源项目必备的;
  • types 存放声明文件,方便引入使用 typescript 的项目中;
  • .travis.yml 持续集成(CI)的配置文件,提交代码时,可根据该文件执行对应的脚本;
  • CHANFELOG 更新日志,Element UI 准备了 4 个不同的语言版本;
  • components.json 配置文件,用于标注组件的文件路径,方便 webpack 打包;
  • element_log.svg Element UI 的图标,使用svg格式,大大减少图片大小;
  • FAQ.md Element UI 开发者对常见问题的解答
  • LICENSE 开源许可证;
  • Makefile 这个文件定义了一系列的规则来制定文件变异操作。可以使用工程化编译工具 --------- make 命令进行操作,例如 make new 自动创建组件目录结构,这些目录包含测试代码、入口文件、文档,实现一套自动化流程。

重点目录分析

packages


从 packages 文件夹分析,这里面的每一个子包都有自己的源代码和一个入口文件,都没有 package.json 文件,所以每个子包不能单独发布,因此可知 element ui 不是一个使用多包管理的组件库。

src

重点关注 src/index.js ,它是项目的入口文件:

js 复制代码
/* Automatically generated by './build/bin/build-entry.js' */

import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
// ...
// 引入组件


const components = [
  Pagination,
  Dialog,
  // ...
  // 组件名称
];

const install = function(Vue, opts = {}) {
  // 国际化配置
  locale.use(opts.locale);
  locale.i18n(opts.i18n);
  
  // 批量注册全局组件
  components.forEach(component => {
    Vue.component(component.name, component);
  });
   
  // 注册全局指令
  Vue.use(InfiniteScroll);
  Vue.use(Loading.directive);
    
  // 设置全局尺寸
  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  };

  // 在 vue 原型上挂载方法
  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;

};

/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  version: '2.15.12',
  locale: locale.use,
  i18n: locale.i18n,
  install,
  CollapseTransition,
  // ... 
  // 导出组件
};

使用 Vue.use 方法调用插件时,会自动调用 install 函数,所以只需要在 install 函数中批量全局注册各种指令、组件、挂载全局方法即可。

Element UI 的入口文件有以下两点值得我们学习:

  • 初始化时,提供选项用于配置全局属性,大大方便了组件的使用
  • 自动化生成入口文件
自动化生成入口文件

首先看入口文件的第一句话:

js 复制代码
/* Automatically generated by './build/bin/build-entry.js' */

解释:该文件是由 ./build/bin/build-entry.js 生成的,那么我们找到该文件查看一番。

js 复制代码
var Components = require('../../components.json');
var fs = require('fs');
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
var path = require('path');
var endOfLine = require('os').EOL;

// 输出地址
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
// 导入模版
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
// 安装组件模板
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
// 模板
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */

{{include}}
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';

const components = [
{{install}},
  CollapseTransition
];

const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n);

  components.forEach(component => {
    Vue.component(component.name, component);
  });

  Vue.use(InfiniteScroll);
  Vue.use(Loading.directive);

  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  };

  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;

};

/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  version: '{{version}}',
  locale: locale.use,
  i18n: locale.i18n,
  install,
  CollapseTransition,
  Loading,
{{list}}
};
`;

delete Components.font;

var ComponentNames = Object.keys(Components);

var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];

// 根据 components.json 文件批量生成模板所需的参数
ComponentNames.forEach(name => {
  var componentName = uppercamelcase(name);

  includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
    name: componentName,
    package: name
  }));

  if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) {
    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
      name: componentName,
      component: name
    }));
  }

  if (componentName !== 'Loading') listTemplate.push(`  ${componentName}`);
});

// 传入模版参数
var template = render(MAIN_TEMPLATE, {
  include: includeComponentTemplate.join(endOfLine),
  install: installTemplate.join(',' + endOfLine),
  version: process.env.VERSION || require('../../package.json').version,
  list: listTemplate.join(',' + endOfLine)
});

// 生成入口文件
fs.writeFileSync(OUTPUT_PATH, template);
console.log('[build entry] DONE:', OUTPUT_PATH);

build-entry.js使用了json-templater生成入口文件,并且引入 components.json 批量生成了组件引入、注册的代码。没有自动化生成入口文件之前,当我们每添加或者删除一个组件时,就需要在入口文件中进行多处修改,使用自动化生成入口文件后,我们只修改一处就可以了。

另外,components.js 文件也是自动化生成的,这又该从哪里追源寻根呢? 这个问题我目前还有找到答案,先顺着下文的思路慢慢解析吧。

packages.json

接下来,我们把注意力集中到 packages.json 中,当你不了解一个项目时,从这里入手是能获取到信息量最多的最好途径!

脚本
js 复制代码
// packages.json
"scripts": {
    "bootstrap": "yarn || npm i",
    "build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
    "build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
    "build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
    "build:umd": "node build/bin/build-locale.js",
    "clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
    "deploy:build": "npm run build:file && cross-env NODE_ENV=production webpack --config build/webpack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME",
    "deploy:extension": "cross-env NODE_ENV=production webpack --config build/webpack.extension.js",
    "dev:extension": "rimraf examples/extension/dist && cross-env NODE_ENV=development webpack --watch --config build/webpack.extension.js",
    "dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
    "dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js",
    "dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme",
    "i18n": "node build/bin/i18n.js",
    "lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",
    "pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js",
    "test": "npm run lint && npm run build:theme && cross-env CI_ENV=/dev/ BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
    "test:watch": "npm run build:theme && cross-env BABEL_ENV=test karma start test/unit/karma.conf.js"
  },

首先,来分析 scripts 中的 dev 命令,他由三个串行一个并行命令组成,在终端中执行npm run dev会发生什么?

第一步它会执行 npm run bootstrap,用来安装项目所需的依赖;

js 复制代码
"bootstrap": "yarn || npm i"

第二步执行 npm run build:file,在以上的脚本中可以看到,build:file又对应着 4 个并行命令,使用node运行了4个文件,这几个文件都可以在 build / bin 目录下找到,看看具体做了什么操作。

js 复制代码
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
  • node build / bin / iconInit.js
    • 利用 node.js 批量处理 icont 图标,倒序写入 examples/icon.json 中;
  • node build / bin / build-entry.js
    • 从 components.json 中拿到每个组件的名字,组件名 push 进一个数组中;
    • 遍历数组,通过模版引擎生成入口文件,也就是上文提到过的 src / index.js;
  • node build / bin / i18n.js
    • 运行多语言库,生成不同的语言的.vue文件,供用户切换语言时使用
  • node build / bin / version.js
    • 形成版本列表,切换不同版本的组件库

第三步在编译环节将 NODE_ENV 配置为 development,webpack-dev-server --config build/webpack.demo.js 意味着将要启动一个 webpack 服务,config 配置文件指向 build/webpack.demo.js,那么我们来看看 webpack.demo.js 中做了什么事情:

js 复制代码
// webpack.demo.js
const webpackConfig = {
  // ... 
  // 使用 ./examples/entry.js 作为入口文件
  entry: isProd ? {
    docs: './examples/entry.js'
  } : (isPlay ? './examples/play.js' : './examples/entry.js'),
  // 使用 /examples/element-ui/ 作为出口文件
  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'
  },
  // ...
  // 项目可以在 8085 端口进行预览
  devServer: {
    host: '0.0.0.0',
    port: 8085,
    publicPath: '/',
    // ...
    }
  },
 // 各种 loader 配置
  module: {
    rules: [
      // ...
      // 这个 md 的loader很重要
      {
        test: /\.md$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              compilerOptions: {
                preserveWhitespace: false
              }
            }
          },
          {
            loader: path.resolve(__dirname, './md-loader/index.js')
          }
        ]
      },
      {
        test: /\.(svg|otf|ttf|woff2?|eot|gif|png|jpe?g)(\?\S*)?$/,
        loader: 'url-loader',
        // todo: 这种写法有待调整
        query: {
          limit: 10000,
          name: path.posix.join('static', '[name].[hash:7].[ext]')
        }
      }
    ]
  },
  plugins: []
};

上面的代码中有两个重要的文件,分别是入口文件和出口文件,我们先来看一下 /examples/entry.js,点进去大致可以看出来这个文档的编码风格和一个普通的 vue 项目的入口文件很相似。由此不难分析,整个 examples文件可以看做是个独立的 vue 项目。webpack对入口文件进行编译打包构建,就相当于将整个examples的 vue项目启动了,并且可以在 8085 端口进行预览。

第四步执行 node build/bin/template.js,主要功能是监测 examples/pages/template文件下的模版信息,如果该模板发生改变,则重新执行一遍 npm run i18n,重新编译多语言组件。

js 复制代码
const path = require('path');
const templates = path.resolve(process.cwd(), './examples/pages/template');

const chokidar = require('chokidar');
let watcher = chokidar.watch([templates]);

watcher.on('ready', function() {
  watcher
    .on('change', function() {
      exec('npm run i18n');
    });
});

function exec(cmd) {
  return require('child_process').execSync(cmd).toString().trim();
}

分析到这里,我们可以总结一下npm run dev做了什么,首先做一系列的准备工作,初始化项目包,处理图标以及多语言,准备好后使用 webpack 将项目启动在 8085 端口,最后再监听国际化模版信息,当模板发生改变时,及时更新多语言组件页面。

这一篇分析到这里就结束啦,本文仅作为学习笔记,有需要补充以及更正的地方,欢迎大家评论区指正!

参考链接

# ElementUI 源码简析------源码结构篇

element源码学习一(package.json)

相关推荐
涔溪35 分钟前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇1 小时前
ES6进阶知识一
前端·ecmascript·es6
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR1 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式
帅帅哥的兜兜1 小时前
CSS:导航栏三角箭头
javascript·css3
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss