文章已收录到 github,欢送 Watch 和 Star。

简介

具体解说了 ElementUI 的源码架构,为下一步基于 ElementUI 打造团队本人的组件库打好松软的根底。

如何疾速为团队打造本人的组件库?

组件库是古代前端畛域中不可短少的一项基建。它能够进步代码的复用性、可维护性,进步团队的生产效率,更好的服务于将来。

那么如何为团队打造本人的组件库呢? 最现实的计划是借用社区的能力,去裁剪一个优良的开源库,只保留你须要的货色,比方它的架构、工程化和文档能力,以及局部根底组件,在裁剪的过程中你可能会发现它的一些问题,而后在你的组件库中去优化并解决。

Element 源码架构

因为团队的技术栈是 Vue,所以抉择基于 element 进行二次开发,在开始前先对 element 框架源码进行具体的刨析,为打造组件库做常识储备。element 框架源码由工程化、官网、组件库、测试和类型申明这 5 局部组成。

工程化

element 的架构是真的优良,通过大量的脚本实现优良的工程化,致力于让组件库的开发者专一于事件自身。比方增加新组件时,一键生成组件所有文件并实现这些文件根本构造的编写和相干的引入配置,总共波及 13 个文件的增加和改变,而你只需实现组件定义这件事即可。element 的工程化由 5 局部组成:build 目录下的工程化配置和脚本、eslint、travis ci、Makefile、package.json 的 scripts。

build

build 目录寄存工程化相干配置和脚本。比方 /build/bin 目录下的 JS 脚本让组件库开发者专一于组件的开发,除此之外不须要管其余任何事件;build/md-loader 是官网组件页面依据 markdown 实现组件 demo + 文档 的要害;还有比方继续集成、webpack 配置等,接下来就具体介绍这些配置和脚本。

/build/bin/build-entry.js

组件配置文件(components.json)联合字符串模版库,主动生成 /src/index.js 文件,防止每次新增组件时手动在 /src/index.js 中引入和导出组件。

/** * 生成 /src/index.js *  1、主动导入组件库所有组件 *  2、定义全量注册组件库组件的 install 办法 *  3、导出版本、install、各个组件 *///  key 为包名、门路为值var Components = require('../../components.json');var fs = require('fs');// 模版库var render = require('json-templater/string');// 负责将 comp-name 模式的字符串转换为 CompNamevar uppercamelcase = require('uppercamelcase');var path = require('path');var endOfLine = require('os').EOL;// 输入门路 /src/index.jsvar OUTPUT_PATH = path.join(__dirname, '../../src/index.js');// 导入模版,import CompName from '../packages/comp-name/index.js'var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';// ' CompName'var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';// /src/index.js 的模版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;// 失去所有的包名,[comp-name1, comp-name2]var ComponentNames = Object.keys(Components);// 寄存所有的 import 语句var includeComponentTemplate = [];// 组件名数组var installTemplate = [];// 组件名数组var listTemplate = [];// 遍历所有的包名ComponentNames.forEach(name => {  // 将连字符格局的包名转换成大驼峰模式,就是组件名,比方 form-item =》 FormItem  var componentName = uppercamelcase(name);  // 替换导入语句中的模版变量,生成导入语句,import FromItem from '../packages/form-item/index.js'  includeComponentTemplate.push(render(IMPORT_TEMPLATE, {    name: componentName,    package: name  }));  // 这些组件从 components 数组中剔除,不须要全局注册,采纳挂载到原型链的形式,在模版字符串的 install 办法中有写  if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) {    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {      name: componentName,      component: name    }));  }  // 将所有的组件放到 listTemplates,最初导出  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)});// 将就绪的模版写入 /src/index.jsfs.writeFileSync(OUTPUT_PATH, template);console.log('[build entry] DONE:', OUTPUT_PATH);
/build/bin/build-locale.js

通过 babel 将 ES Module 格调的所有翻译文件(/src/locale/lang)转译成 UMD 格调。

/** * 通过 babel 将 ES Module 格调的翻译文件转译成 UMD 格调 */var fs = require('fs');var save = require('file-save');var resolve = require('path').resolve;var basename = require('path').basename;// 翻译文件目录,这些文件用于官网var localePath = resolve(__dirname, '../../src/locale/lang');// 失去目录下的所有翻译文件var fileList = fs.readdirSync(localePath);// 转换函数var transform = function(filename, name, cb) {  require('babel-core').transformFile(resolve(localePath, filename), {    plugins: [      'add-module-exports',      ['transform-es2015-modules-umd', {loose: true}]    ],    moduleId: name  }, cb);};// 遍历所有文件fileList  // 只解决 js 文件,其实目录下不存在非 js 文件  .filter(function(file) {    return /\.js$/.test(file);  })  .forEach(function(file) {    var name = basename(file, '.js');    // 调用转换函数,将转换后的代码写入到 lib/umd/locale 目录下    transform(file, name, function(err, result) {      if (err) {        console.error(err);      } else {        var code = result.code;        code = code          .replace('define(\'', 'define(\'element/locale/')          .replace('global.', 'global.ELEMENT.lang = global.ELEMENT.lang || {}; \n    global.ELEMENT.lang.');        save(resolve(__dirname, '../../lib/umd/locale', file)).write(code);        console.log(file);      }    });  });
/build/bin/gen-cssfile.js

主动在 /packages/theme-chalk/src/index.scss|css 中引入各个组件包的款式,在全量注册组件库时须要用到这个款式文件,即 import 'packages/theme-chalk/src/index.scss

/** * 主动在 /packages/theme-chalk/src/index.scss|css 中引入各个组件包的款式 * 在全量注册组件库时须要用到该款式文件,即 import 'packages/theme-chalk/src/index.scss */var fs = require('fs');var path = require('path');var Components = require('../../components.json');var themes = [  'theme-chalk'];// 失去所有的包名Components = Object.keys(Components);// 所有组件包的根底门路,/packagesvar basepath = path.resolve(__dirname, '../../packages/');// 判断指定文件是否存在function fileExists(filePath) {  try {    return fs.statSync(filePath).isFile();  } catch (err) {    return false;  }}// 遍历所有组件包,生成引入所有组件包款式的 import 语句,而后主动生成 packages/theme-chalk/src/index.scss|css 文件themes.forEach((theme) => {  // 是否是 scss,element-ui 默认应用 scss 编写款式  var isSCSS = theme !== 'theme-default';  // 导入根底款式文件 @import "./base.scss|css";\n  var indexContent = isSCSS ? '@import "./base.scss";\n' : '@import "./base.css";\n';  // 遍历所有组件包,并生成 @import "./comp-package.scss|css";\n  Components.forEach(function(key) {    // 跳过这三个组件包    if (['icon', 'option', 'option-group'].indexOf(key) > -1) return;    // comp-package.scss|css    var fileName = key + (isSCSS ? '.scss' : '.css');    // 导入语句,@import "./comp-package.scss|css";\n    indexContent += '@import "./' + fileName + '";\n';    // 如果该组件包的款式文件不存在,比方 /packages/form-item/theme-chalk/src/form-item.scss 不存在,则认为其被脱漏了,创立该文件    var filePath = path.resolve(basepath, theme, 'src', fileName);    if (!fileExists(filePath)) {      fs.writeFileSync(filePath, '', 'utf8');      console.log(theme, ' 创立脱漏的 ', fileName, ' 文件');    }  });  // 生成 /packages/theme-chalk/src/index.scss|css,负责引入所有组件包的款式  fs.writeFileSync(path.resolve(basepath, theme, 'src', isSCSS ? 'index.scss' : 'index.css'), indexContent);});
/build/bin/i18n.js

依据模版(/examples/pages/template)生成四种语言的官网页面的 .vue 文件。

'use strict';var fs = require('fs');var path = require('path');// 官网页面翻译配置,内置了四种语言var langConfig = require('../../examples/i18n/page.json');// 遍历所有语言langConfig.forEach(lang => {  // 创立 /examples/pages/{lang},比方: /examples/pages/zh-CN  try {    fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));  } catch (e) {    fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));  }  // 遍历所有的页面,依据 page.tpl 主动生成对应语言的 .vue 文件  Object.keys(lang.pages).forEach(page => {    // 比方 /examples/pages/template/index.tpl    var templatePath = path.resolve(__dirname, `../../examples/pages/template/${ page }.tpl`);    // /examples/pages/zh-CN/index.vue    var outputPath = path.resolve(__dirname, `../../examples/pages/${ lang.lang }/${ page }.vue`);    // 读取模版文件    var content = fs.readFileSync(templatePath, 'utf8');    // 读取 index 页面的所有键值对的配置    var pairs = lang.pages[page];    // 遍历这些键值对,通过正则匹配的形式替换掉模版中对应的 key    Object.keys(pairs).forEach(key => {      content = content.replace(new RegExp(`<%=\\s*${ key }\\s*>`, 'g'), pairs[key]);    });    // 将替换后的内容写入 vue 文件    fs.writeFileSync(outputPath, content);  });});
/build/bin/iconInit.js

依据 icon.scss 款式文件中的选择器,通过正则匹配的形式,匹配出所有的 icon 名称,而后将这些 icon 名组成数组,将数组写入到 /examples/icon.json 文件中,该文件在官网的 icon 图标页用来主动生成所有的 icon 图标。

'use strict';/** * 依据 icon.scss 款式文件中的选择器,通过正则匹配的形式,匹配出所有的 icon 名称, * 而后将所有 icon 名组成的数组写入到 /examples/icon.json 文件中 * 该文件在官网的 icon 图标页用来主动生成所有的 icon 图标 */var postcss = require('postcss');var fs = require('fs');var path = require('path');// icon.scss 文件内容var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');// 失去款式节点var nodes = postcss.parse(fontFile).nodes;var classList = [];// 遍历所有的款式节点nodes.forEach((node) => {  // 从选择器中匹配出 icon 名称,比方 el-icon-add,匹配失去 add  var selector = node.selector || '';  var reg = new RegExp(/\.el-icon-([^:]+):before/);  var arr = selector.match(reg);  // 将 icon 名称写入数组,  if (arr && arr[1]) {    classList.push(arr[1]);  }});classList.reverse(); // 心愿按 css 文件程序倒序排列// 将 icon 名组成的数组写入 /examples/icon.json 文件fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList), () => {});
/build/bin/new-lang.js

为组件库增加新语言,比方 fr(法语),别离为波及到的文件(components.json、page.json、route.json、nav.config.json、docs)设置该语言的相干配置,具体的配置项默认为英语,你只须要在相应的文件中将这些英文配置项翻译为对应的语言即可。

'use strict';/** * 为组件库增加新语言,比方 fr(法语) *  别离为波及到的文件(components.json、page.json、route.json、nav.config.json、docs)设置该语言的相干配置 *  具体的配置项默认为英语,你只须要在相应的文件中将这些英文配置项翻译为对应的语言即可 */console.log();process.on('exit', () => {  console.log();});if (!process.argv[2]) {  console.error('[language] is required!');  process.exit(1);}var fs = require('fs');const path = require('path');const fileSave = require('file-save');const lang = process.argv[2];// const configPath = path.resolve(__dirname, '../../examples/i18n', lang);// 增加到 components.jsonconst componentFile = require('../../examples/i18n/component.json');if (componentFile.some(item => item.lang === lang)) {  console.error(`${lang} already exists.`);  process.exit(1);}let componentNew = Object.assign({}, componentFile.filter(item => item.lang === 'en-US')[0], { lang });componentFile.push(componentNew);fileSave(path.join(__dirname, '../../examples/i18n/component.json'))  .write(JSON.stringify(componentFile, null, '  '), 'utf8')  .end('\n');// 增加到 page.jsonconst pageFile = require('../../examples/i18n/page.json');// 新语言的默认配置为英语,你只须要去 page.json 中将该语言配置中的应为翻译为该语言即可let pageNew = Object.assign({}, pageFile.filter(item => item.lang === 'en-US')[0], { lang });pageFile.push(pageNew);fileSave(path.join(__dirname, '../../examples/i18n/page.json'))  .write(JSON.stringify(pageFile, null, '  '), 'utf8')  .end('\n');// 增加到 route.jsonconst routeFile = require('../../examples/i18n/route.json');routeFile.push({ lang });fileSave(path.join(__dirname, '../../examples/i18n/route.json'))  .write(JSON.stringify(routeFile, null, '  '), 'utf8')  .end('\n');// 增加到 nav.config.jsonconst navFile = require('../../examples/nav.config.json');navFile[lang] = navFile['en-US'];fileSave(path.join(__dirname, '../../examples/nav.config.json'))  .write(JSON.stringify(navFile, null, '  '), 'utf8')  .end('\n');// docs 下新建对应文件夹try {  fs.statSync(path.resolve(__dirname, `../../examples/docs/${ lang }`));} catch (e) {  fs.mkdirSync(path.resolve(__dirname, `../../examples/docs/${ lang }`));}console.log('DONE!');
/build/bin/new.js

为组件库增加新组件时会应用该脚本,一键生成组件所有文件并实现这些文件根本构造的编写和相干的引入配置,总共波及 13 个文件的增加和改变,比方:make new city 城市列表。该脚本的存在,让你为组件库开发新组件时,只需专一于组件代码的编写即可,其它的一律不必管。

'use strict';/** * 增加新组件 *  比方:make new city 城市列表 *  1、在 /packages 目录下新建组件目录,并实现目录构造的创立 *  2、创立组件文档,/examples/docs/{lang}/city.md *  3、创立组件单元测试文件,/test/unit/specs/city.spec.js *  4、创立组件款式文件,/packages/theme-chalk/src/city.scss *  5、创立组件类型申明文件,/types/city.d.ts *  6、配置 *      在 /components.json 文件中配置组件信息 *      在 /examples/nav.config.json 中增加该组件的路由配置 *      在 /packages/theme-chalk/src/index.scss 文件中主动引入该组件的款式文件 *      将类型申明文件在 /types/element-ui.d.ts 中主动引入 *  总之,该脚本的存在,让你只需专一于编写你的组件代码,其它的一律不必管 */console.log();process.on('exit', () => {  console.log();});if (!process.argv[2]) {  console.error('[组件名]必填 - Please enter new component name');  process.exit(1);}const path = require('path');const fs = require('fs');const fileSave = require('file-save');const uppercamelcase = require('uppercamelcase');// 组件名称,比方 cityconst componentname = process.argv[2];// 组件的中文名称const chineseName = process.argv[3] || componentname;// 将组件名称转换为大驼峰模式,city => Cityconst ComponentName = uppercamelcase(componentname);// 组件包目录,/packages/cityconst PackagePath = path.resolve(__dirname, '../../packages', componentname);// 须要增加的文件列表和文件内容的根本构造const Files = [  // /packages/city/index.js  {    filename: 'index.js',    // 文件内容,引入组件,定义组件静态方法 install 用来注册组件,而后导出组件    content: `import ${ComponentName} from './src/main';/* istanbul ignore next */${ComponentName}.install = function(Vue) {  Vue.component(${ComponentName}.name, ${ComponentName});};export default ${ComponentName};`  },  // 定义组件的根本构造,/packages/city/src/main.vue  {    filename: 'src/main.vue',    // 文件内容,sfc    content: `<template>  <div class="el-${componentname}"></div></template><script>export default {  name: 'El${ComponentName}'};</script>`  },  // 四种语言的文档,/examples/docs/{lang}/city.md,并设置文件题目  {    filename: path.join('../../examples/docs/zh-CN', `${componentname}.md`),    content: `## ${ComponentName} ${chineseName}`  },  {    filename: path.join('../../examples/docs/en-US', `${componentname}.md`),    content: `## ${ComponentName}`  },  {    filename: path.join('../../examples/docs/es', `${componentname}.md`),    content: `## ${ComponentName}`  },  {    filename: path.join('../../examples/docs/fr-FR', `${componentname}.md`),    content: `## ${ComponentName}`  },  // 组件测试文件,/test/unit/specs/city.spec.js  {    filename: path.join('../../test/unit/specs', `${componentname}.spec.js`),    // 文件内容,给出测试文件的根本构造    content: `import { createTest, destroyVM } from '../util';import ${ComponentName} from 'packages/${componentname}';describe('${ComponentName}', () => {  let vm;  afterEach(() => {    destroyVM(vm);  });  it('create', () => {    vm = createTest(${ComponentName}, true);    expect(vm.$el).to.exist;  });});`  },  // 组件款式文件,/packages/theme-chalk/src/city.scss  {    filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`),    // 文件根本构造    content: `@import "mixins/mixins";@import "common/var";@include b(${componentname}) {}`  },  // 组件类型申明文件  {    filename: path.join('../../types', `${componentname}.d.ts`),    // 类型申明文件根本构造    content: `import { ElementUIComponent } from './component'/** ${ComponentName} Component */export declare class El${ComponentName} extends ElementUIComponent {}`  }];// 将组件增加到 components.json,{ City: './packages/city/index.js' }const componentsFile = require('../../components.json');if (componentsFile[componentname]) {  console.error(`${componentname} 已存在.`);  process.exit(1);}componentsFile[componentname] = `./packages/${componentname}/index.js`;fileSave(path.join(__dirname, '../../components.json'))  .write(JSON.stringify(componentsFile, null, '  '), 'utf8')  .end('\n');// 将组件款式文件在 index.scss 中引入const sassPath = path.join(__dirname, '../../packages/theme-chalk/src/index.scss');const sassImportText = `${fs.readFileSync(sassPath)}@import "./${componentname}.scss";`;fileSave(sassPath)  .write(sassImportText, 'utf8')  .end('\n');// 将组件的类型申明文件在 element-ui.d.ts 中引入const elementTsPath = path.join(__dirname, '../../types/element-ui.d.ts');let elementTsText = `${fs.readFileSync(elementTsPath)}/** ${ComponentName} Component */export class ${ComponentName} extends El${ComponentName} {}`;const index = elementTsText.indexOf('export') - 1;const importString = `import { El${ComponentName} } from './${componentname}'`;elementTsText = elementTsText.slice(0, index) + importString + '\n' + elementTsText.slice(index);fileSave(elementTsPath)  .write(elementTsText, 'utf8')  .end('\n');// 遍历 Files 数组,创立列出的所有文件并写入文件内容Files.forEach(file => {  fileSave(path.join(PackagePath, file.filename))    .write(file.content, 'utf8')    .end('\n');});// 在 nav.config.json 中增加新组件对应的路由配置const navConfigFile = require('../../examples/nav.config.json');// 遍历配置中的各个语言,在所有语言配置中都减少该组件的路由配置Object.keys(navConfigFile).forEach(lang => {  let groups = navConfigFile[lang][4].groups;  groups[groups.length - 1].list.push({    path: `/${componentname}`,    title: lang === 'zh-CN' && componentname !== chineseName      ? `${ComponentName} ${chineseName}`      : ComponentName  });});fileSave(path.join(__dirname, '../../examples/nav.config.json'))  .write(JSON.stringify(navConfigFile, null, '  '), 'utf8')  .end('\n');console.log('DONE!');

这里有个毛病就是,新建组件时不会主动从新生成 /src/index.js,也就是说不会将新生成的组件主动在组件库入口中引入。这也简略,只须要配置下 Makefile 即可,将 new 命令改成 node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS)) && npm run build:file 即可。

/build/bin/template.js

监听 /examples/pages/template 目录下的所有模版文件,当模版文件产生扭转时主动执行 npm run i18n,即执行 i18n.js 脚本,从新生成四种语言的 .vue 文件。

/** * 监听 /examples/pages/template 目录下的所有模版文件,当模版文件产生扭转时主动执行 npm run i18n, * 即执行 i18n.js 脚本,从新生成四种语言的 .vue 文件 */const path = require('path');// 监听目录const templates = path.resolve(process.cwd(), './examples/pages/template');// 负责监听的库const chokidar = require('chokidar');// 监听模板目录let watcher = chokidar.watch([templates]);// 当目录下的文件产生扭转时,主动执行 npm run i18nwatcher.on('ready', function() {  watcher    .on('change', function() {      exec('npm run i18n');    });});// 负责执行命令function exec(cmd) {  return require('child_process').execSync(cmd).toString().trim();}
/build/bin/version.js

依据 /package.json 文件,主动生成 /examples/version.json,用于记录组件库的版本信息,这些版本洗洗在官网组件页面的头部导航栏会用到。

/** * 依据 package.json 主动生成 /examples/version.json,用于记录组件库的版本信息 * 这些版本信息在官网组件页面的头部导航栏会用到 */var fs = require('fs');var path = require('path');var version = process.env.VERSION || require('../../package.json').version;var content = { '1.4.13': '1.4', '2.0.11': '2.0', '2.1.0': '2.1', '2.2.2': '2.2', '2.3.9': '2.3', '2.4.11': '2.4', '2.5.4': '2.5', '2.6.3': '2.6', '2.7.2': '2.7', '2.8.2': '2.8', '2.9.2': '2.9', '2.10.1': '2.10', '2.11.1': '2.11', '2.12.0': '2.12', '2.13.2': '2.13', '2.14.1': '2.14' };if (!content[version]) content[version] = '2.15';fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));
/build/md-loader

它是一个 loader,官网组件页面的 组件 demo + 文档的模式一大半的功绩都是源自于它。

能够在 /examples/route.config.js 中看到 registerRoute 办法生成组件页面的路由配置时,应用 loadDocs 办法加载/examples/docs/{lang}/comp.md 。留神,这里加载的 markdown 文档,而不是平时常见的 vue 文件,然而却能想 vue 文件一样在页面上渲染成一个 Vue 组件,这是怎么做到的呢?

咱们晓得,webpack 的理念是所有资源都能够 require,只需配置相应的 loader 即可。在 /build/webpack.demo.js 文件中的 module.rules 下能够看到对 markdow(.md) 规定的解决,先通过 md-loader 解决 markdown 文件,从中解析出 vue 代码,而后交给 vue-loader,最终生成 sfc(vue 单文件组件)渲染到页面。这就能看到组件页面的文档 + 组件 demo 展现成果。

{  test: /\.md$/,  use: [    {      loader: 'vue-loader',      options: {        compilerOptions: {          preserveWhitespace: false        }      }    },    {      loader: path.resolve(__dirname, './md-loader/index.js')    }  ]}

如果对 loader 的具体实现感兴趣能够自行深刻浏览。

/build/config.js

webpack 的公共配置,比方 externals、alias 等。通过 externals 的配置解决了组件库局部代码的冗余问题,比方组件和组件库公共模块的代码,然而组件款式冗余问题没有失去解决;alias 别名配置为开发组件库提供了不便。

/** * webpack 公共配置,比方 externals、alias */var path = require('path');var fs = require('fs');var nodeExternals = require('webpack-node-externals');var Components = require('../components.json');var utilsList = fs.readdirSync(path.resolve(__dirname, '../src/utils'));var mixinsList = fs.readdirSync(path.resolve(__dirname, '../src/mixins'));var transitionList = fs.readdirSync(path.resolve(__dirname, '../src/transitions'));/** * externals 解决组件依赖其它组件并按需引入时代码冗余的问题 *     比方 Table 组件依赖 Checkbox 组件,在我的项目中如果我同时引入 Table 和 Checkbox 时,会不会产生冗余代码 *     如果没有以下内容的的话,会,这时候你会看到有两份 Checkbox 组件代码。 *     包含 locale、utils、mixins、transitions 这些公共内容,也会呈现冗余代码 *     但有了 externals 的设置,就会将通知 webpack 不须要将这些 import 的包打包到 bundle 中,运行时再从内部去 *     获取这些扩大依赖。这样就能够在打包后 /lib/tables.js 中看到编译后的 table.js 对 Checkbox 组件的依赖引入: *     module.exports = require("element-ui/lib/checkbox") *     这么解决之后就不会呈现冗余的 JS 代码,然而对于 CSS 局部,element-ui 并未解决冗余状况。 *     能够看到 /lib/theme-chalk/table.css 和 /lib/theme-chalk/checkbox.css 中都有 Checkbox 组件的款式 */var externals = {};Object.keys(Components).forEach(function(key) {  externals[`element-ui/packages/${key}`] = `element-ui/lib/${key}`;});externals['element-ui/src/locale'] = 'element-ui/lib/locale';utilsList.forEach(function(file) {  file = path.basename(file, '.js');  externals[`element-ui/src/utils/${file}`] = `element-ui/lib/utils/${file}`;});mixinsList.forEach(function(file) {  file = path.basename(file, '.js');  externals[`element-ui/src/mixins/${file}`] = `element-ui/lib/mixins/${file}`;});transitionList.forEach(function(file) {  file = path.basename(file, '.js');  externals[`element-ui/src/transitions/${file}`] = `element-ui/lib/transitions/${file}`;});externals = [Object.assign({  vue: 'vue'}, externals), nodeExternals()];exports.externals = externals;// 设置别名,方便使用exports.alias = {  main: path.resolve(__dirname, '../src'),  packages: path.resolve(__dirname, '../packages'),  examples: path.resolve(__dirname, '../examples'),  'element-ui': path.resolve(__dirname, '../')};exports.vue = {  root: 'Vue',  commonjs: 'vue',  commonjs2: 'vue',  amd: 'vue'};exports.jsexclude = /node_modules|utils\/popper\.js|utils\/date\.js/;
/build/deploy-ci.sh

和 travis ci 联合应用的继续集成脚本,这个脚本在 .travis.yml 文件中被执行,代码被提交到 github 仓库当前会主动被 Tavis CI 执行,ci 会主动找我的项目中的 .travis.yml 文件,并执行外面的命令。但这个咱们可能用不到,个别团队外部都会有本人的继续集成计划。

/build/git-release.sh

这里次要是和近程的 dev 分支做 diff 而后合并。

#!/usr/bin/env sh# 这里次要是和近程的 dev 分支做 diff 而后合并git checkout devif test -n "$(git status --porcelain)"; then  echo 'Unclean working tree. Commit or stash changes first.' >&2;  exit 128;fiif ! git fetch --quiet 2>/dev/null; then  echo 'There was a problem fetching your branch. Run `git fetch` to see more...' >&2;  exit 128;fiif test "0" != "$(git rev-list --count --left-only @'{u}'...HEAD)"; then  echo 'Remote history differ. Please pull changes.' >&2;  exit 128;fiecho 'No conflicts.' >&2;
/build/release.sh

脚本实现了以下工作:

  • 合并 dev 分支到 master、
  • 批改款式包和组件库的版本号
  • 公布款式包和组件库
  • 提交 master 和 dev 分支到近程仓库

该脚本在公布组件库时能够应用,特地是其中主动更改版本号的性能(每次 publish 时都忘改版本号)。这里提交代码到近程仓库的日志很简略,更具体的提交日志时通过更新日志文件 CHANGELOG.{lang}.md 提供的。

#!/usr/bin/env shset -e# 合并 dev 分支到 master# 编译打包# 批改款式包和组件库的版本号# 公布款式包和组件库# 提交 master 和 dev 分支到近程仓库# 合并 dev 分支到 mastergit checkout mastergit merge dev# 版本抉择 cliVERSION=`npx select-version-cli`# 是否确认以后版本信息read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -recho    # (optional) move to a new lineif [[ $REPLY =~ ^[Yy]$ ]]then  echo "Releasing $VERSION ..."  # build,编译打包  VERSION=$VERSION npm run dist  # ssr test  node test/ssr/require.test.js              # publish theme  echo "Releasing theme-chalk $VERSION ..."  cd packages/theme-chalk  # 更改主题包的版本信息  npm version $VERSION --message "[release] $VERSION"  # 公布主题  if [[ $VERSION =~ "beta" ]]  then    npm publish --tag beta  else    npm publish  fi  cd ../..  # commit  git add -A  git commit -m "[build] $VERSION"  # 更改组件库的版本信息  npm version $VERSION --message "[release] $VERSION"  # publish,将 master 推到近程仓库  git push eleme master  git push eleme refs/tags/v$VERSION  git checkout dev  git rebase master  git push eleme dev  # 公布组件库  if [[ $VERSION =~ "beta" ]]  then    npm publish --tag beta  else    npm publish  fifi
/build/webpack.xx.js
  • webpack.common.js,构建 commonjs2 标准的包,会打一个全量的包
  • webpack.component.js,构建 commonjs2 标准的包,反对按需加载

    反对按需加载的重点在于 entry 和 ouput 的配置,将每个组件打成独自的包
  • webpack.conf.js,构建 UMD 标准的包,会打一个全量的包
  • webpack.demo.js,官网我的项目的 webpack 配置
  • webpack.extension.js,主题编辑器的 chorme 插件我的项目的 webpack 配置,我的项目在 extension 目录下
  • webpack.test.js,这个文件没什么用,不过看命名,应该是想用于测试项目的 webpack 配置,不过当初测试用的是 karma 框架

eslint

element 通过 eslint 来保障代码格调的一致性,还专门编写了 elemefe 作为 eslint 的扩大规定配置。为了保障官网我的项目的品质,在 /build/webpack.demo.js 中配置了 eslint-loader 规定,在我的项目启动时强制查看代码品质。然而 element 在代码品质管制这块儿做的还是不够,比方:代码主动格式化能力太弱、只保障了 /src、/test、/packages、/build 目录下的代码品质,对于官网我的项目做的不够,特地是 文档格局的限度。这里倡议大家再集成一个 prettier 专门去做格局限度,让 eslint 专一于代码语法的限度,能够参考 搭建本人的 typescript 我的项目 + 开发本人的脚手架工具 ts-cli 中的 代码品质 局部去配置。

travis ci

travis ci 联合脚本的形式来实现继续集成的工作,不过这个可能对于外部我的项目用不上,因为 travis ci 只能用于 github,外部个别应用 gitlab,也有配套的继续集成

Makefile

make 命令的配置文件,写过 C、C++ 的同学应该比拟相熟。

执行 make 命令能够看到具体的帮忙信息。比方:执行 make install 装包、make dev 启动本地开发环境、make new comp-name 中文名 新建组件等。应用 make 命令相较于 npm run xx 更不便、清晰、简略,不过其外部也是依赖于 npm run xx 来实现真正的工作,相当于为了更好的开发体验,将泛滥 npm run cmd 提供了一层封装。

package.json -> scripts

elemnt 编写了很多 npm scripts,这些 script 联合 /build 中的泛滥脚本实现通过脚本来主动实现大量反复的体力劳动,比人工靠谱且效率更高,这个设计我感觉是 element 中最值得大家学习的中央,能够将这样的设计利用到本人的我的项目中,助力业务提效。

{  // 装包  "bootstrap": "yarn || npm i",  // 通过JS脚本,主动生成以下文件:生成 examples/icon.json 文件 && 生成 src/index.js 文件 && 生成四种语言的官网的 .vue 文件 && 生成 examples/version.json 文件,蕴含了组件库的版本信息  "build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",  // 构建主题款式:在 index.scss 中主动引入各个组件的款式文件 && 通过 gulp 将 scss 文件编译成 css 并输入到 lib 目录 && 拷贝根底款式 theme-chalk 到 lib/theme-chalk  "build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",  // 通过 babel 编译 src 目录,而后将编译后的文件输入到 lib 目录,疏忽 /src/index.js  "build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",  // 将 ES Module 格调的翻译文件编译成 UMD 格调  "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",  // 启动组件库的本地开发环境。执行 build:file,自动化生成一些文件 && 启动 example 我的项目,即官网 && 监听 examples/pages/template 目录下所有模版文件的变动,如果扭转了则从新生成 .vue",  "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",  // 组件测试项目,在 examples/play/index.vue 中能够引入组件库任意组件,也能够间接应用 dev 启动的我的项目,在文档中应用组件  "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",  // 生成四种语言的官网的 .vue 文件  "i18n": "node build/bin/i18n.js",  // lint,保障我的项目代码品质  "lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",  // 装包 && 合并近程仓库的 dev 分支 && 合并 dev 分支到 master、打包编译、批改款式包和组件库的版本号、公布款式包和组件库、提交代码到近程仓库。应用时注掉最初一个脚本,那个脚本有问题  "pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js",  // 生成测试报告,不论是 test 还是 test:watch,生成一次测试报告耗时太长了  "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"}

官网

element 的官网是和组件库在一个仓库内,官网的所有货色都放在 /examples 目录下,就是一个 vue 我的项目。

entry.js

官网我的项目的入口,在这里全量引入组件库,及其款式。

// 官网我的项目的入口,就是一个一般的 vue 我的项目import Vue from 'vue';import entry from './app';import VueRouter from 'vue-router';// 引入组件库,main 是别名,在 /build/config.js 中有配置import Element from 'main/index.js';import hljs from 'highlight.js';// 路由配置import routes from './route.config';// 官网我的项目的一些组件import demoBlock from './components/demo-block';import MainFooter from './components/footer';import MainHeader from './components/header';import SideNav from './components/side-nav';import FooterNav from './components/footer-nav';import title from './i18n/title';// 组件库款式import 'packages/theme-chalk/src/index.scss';import './demo-styles/index.scss';import './assets/styles/common.css';import './assets/styles/fonts/style.css';// 将 icon 信息挂载到 Vue 原型链上,在 markdown 文档中被应用,在官网的 icon 图标 页面展现出所有的 icon 图标import icon from './icon.json';Vue.use(Element);Vue.use(VueRouter);Vue.component('demo-block', demoBlock);Vue.component('main-footer', MainFooter);Vue.component('main-header', MainHeader);Vue.component('side-nav', SideNav);Vue.component('footer-nav', FooterNav);const globalEle = new Vue({  data: { $isEle: false } // 是否 ele 用户});Vue.mixin({  computed: {    $isEle: {      get: () => (globalEle.$data.$isEle),      set: (data) => {globalEle.$data.$isEle = data;}    }  }});Vue.prototype.$icon = icon; // Icon 列表页用const router = new VueRouter({  mode: 'hash',  base: __dirname,  routes});router.afterEach(route => {  // https://github.com/highlightjs/highlight.js/issues/909#issuecomment-131686186  Vue.nextTick(() => {    const blocks = document.querySelectorAll('pre code:not(.hljs)');    Array.prototype.forEach.call(blocks, hljs.highlightBlock);  });  const data = title[route.meta.lang];  for (let val in data) {    if (new RegExp('^' + val, 'g').test(route.name)) {      document.title = data[val];      return;    }  }  document.title = 'Element';  ga('send', 'event', 'PageView', route.name);});new Vue({ // eslint-disable-line  ...entry,  router}).$mount('#app');

nav.config.json

官网组件页面的侧边导航栏配置,肯定要理解该 json 文件的构造,才能看懂 route.config.js 文件中生成组件页面所有路由的代码。

route.config.js

依据路由配置主动生成官网我的项目的路由配置。

// 依据路由配置主动生成官网我的项目的路由import navConfig from './nav.config';// 反对的所有语言import langs from './i18n/route';// 加载官网各个页面的 .vue 文件const LOAD_MAP = {  'zh-CN': name => {    return r => require.ensure([], () =>      r(require(`./pages/zh-CN/${name}.vue`)),    'zh-CN');  },  'en-US': name => {    return r => require.ensure([], () =>      r(require(`./pages/en-US/${name}.vue`)),    'en-US');  },  'es': name => {    return r => require.ensure([], () =>      r(require(`./pages/es/${name}.vue`)),    'es');  },  'fr-FR': name => {    return r => require.ensure([], () =>      r(require(`./pages/fr-FR/${name}.vue`)),    'fr-FR');  }};const load = function(lang, path) {  return LOAD_MAP[lang](path);};// 加载官网组件页面各个组件的 markdown 文件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');  }};const loadDocs = function(lang, path) {  return LOAD_DOCS_MAP[lang](path);};// 增加组件页的各个路由配置,以下这段代码要看懂必须明确 nav.config.json 文件的构造const registerRoute = (navConfig) => {  let route = [];  // 遍历配置,生成四种语言的组件路由配置  Object.keys(navConfig).forEach((lang, index) => {    // 指定语言的配置,比方 lang = zh-CN,navs 就是所有配置项都是中文写的    let navs = navConfig[lang];    // 组件页面 lang 语言的路由配置    route.push({      // 比方: /zh-CN/component      path: `/${ lang }/component`,      redirect: `/${ lang }/component/installation`,      // 加载组件页的 component.vue      component: load(lang, 'component'),      // 组件页的所有子路由,即各个组件,放这里,最初的路由就是 /zh-CN/component/comp-path      children: []    });    // 遍历指定语言的所有配置项    navs.forEach(nav => {      if (nav.href) return;      if (nav.groups) {        // 该项为组件        nav.groups.forEach(group => {          group.list.forEach(nav => {            addRoute(nav, lang, index);          });        });      } else if (nav.children) {        // 该项为开发指南        nav.children.forEach(nav => {          addRoute(nav, lang, index);        });      } else {        // 其它,比方更新日志、Element React、Element Angular        addRoute(nav, lang, index);      }    });  });  // 生成子路由配置,并填充到 children 中  function addRoute(page, lang, index) {    // 依据 path 决定是加载 vue 文件还是加载 markdown 文件    const component = page.path === '/changelog'      ? load(lang, 'changelog')      : loadDocs(lang, page.path);    let child = {      path: page.path.slice(1),      meta: {        title: page.title || page.name,        description: page.description,        lang      },      name: 'component-' + lang + (page.title || page.name),      component: component.default || component    };    // 将子路由增加在下面的 children 中    route[index].children.push(child);  }  return route;};// 失去组件页面所有侧边栏的路由配置let route = registerRoute(navConfig);const generateMiscRoutes = function(lang) {  let guideRoute = {    path: `/${ lang }/guide`, // 指南    redirect: `/${ lang }/guide/design`,    component: load(lang, 'guide'),    children: [{      path: 'design', // 设计准则      name: 'guide-design' + lang,      meta: { lang },      component: load(lang, 'design')    }, {      path: 'nav', // 导航      name: 'guide-nav' + lang,      meta: { lang },      component: load(lang, 'nav')    }]  };  let themeRoute = {    path: `/${ lang }/theme`,    component: load(lang, 'theme-nav'),    children: [      {        path: '/', // 主题治理        name: 'theme' + lang,        meta: { lang },        component: load(lang, 'theme')      },      {        path: 'preview', // 主题预览编辑        name: 'theme-preview-' + lang,        meta: { lang },        component: load(lang, 'theme-preview')      }]  };  let resourceRoute = {    path: `/${ lang }/resource`, // 资源    meta: { lang },    name: 'resource' + lang,    component: load(lang, 'resource')  };  let indexRoute = {    path: `/${ lang }`, // 首页    meta: { lang },    name: 'home' + lang,    component: load(lang, 'index')  };  return [guideRoute, resourceRoute, themeRoute, indexRoute];};langs.forEach(lang => {  route = route.concat(generateMiscRoutes(lang.lang));});route.push({  path: '/play',  name: 'play',  component: require('./play/index.vue')});let userLanguage = localStorage.getItem('ELEMENT_LANGUAGE') || window.navigator.language || 'en-US';let defaultPath = '/en-US';if (userLanguage.indexOf('zh-') !== -1) {  defaultPath = '/zh-CN';} else if (userLanguage.indexOf('es') !== -1) {  defaultPath = '/es';} else if (userLanguage.indexOf('fr') !== -1) {  defaultPath = '/fr-FR';}route = route.concat([{  path: '/',  redirect: defaultPath}, {  path: '*',  redirect: defaultPath}]);export default route;

play

包含 play.jsplay/index.vue,示例我的项目,比方你想看一个 element 中某个组件的成果,特地是组件按需加载时的显示成果,能够在 play/index.vue 中引入应用,应用 npm run dev:play 命令启动我的项目,也是在 /build/webpack.demo.js 中通过环境变量来配置的。

// play.jsimport Vue from 'vue';// 全量引入组件库和其款式import Element from 'main/index.js';import 'packages/theme-chalk/src/index.scss';import App from './play/index.vue';Vue.use(Element);new Vue({ // eslint-disable-line  render: h => h(App)}).$mount('#app');
<!-- play/index.vue --><template>  <div style="margin: 20px;">    <el-input v-model="input" placeholder="请输出内容"></el-input>  </div></template><script>  export default {    data() {      return {        input: 'Hello Element UI!'      };    }  };</script>

pages

官网的各个页面都在这里,通过 i18n.js 脚本 联合 pages/template 目录下的各个模版文件主动在 pages 目录下生成四种语言的 .vue 文件,这些 vue 文件会在 route.config.js 中被加载。

i18n

官网页面的翻译配置文件都在这里。

  • component.json,组件页面的翻译配置
  • page.json,其它页面的一些翻译配置,比方首页、设计页等
  • route.json,语言配置,示意组件库目前都反对那些语言
  • theme-editor.json,主题编辑器页面的翻译配置
  • title.json,官网各个页面在 tab 标签中显示的 title 信息

extension

主题编辑器的 chrome 插件我的项目。

dom

定义了 dom 款式操作方法,包含判断是否存在指定的款式、增加款式、移除款式、切换款式。

// dom/class.jsexport const hasClass = function(obj, cls) {  return obj.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));};export const addClass = function(obj, cls) {  if (!hasClass(obj, cls)) obj.className += ' ' + cls;};export const removeClass = function(obj, cls) {  if (hasClass(obj, cls)) {    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');    obj.className = obj.className.replace(reg, ' ');  }};export const toggleClass = function(obj, cls) {  if (hasClass(obj, cls)) {    removeClass(obj, cls);  } else {    addClass(obj, cls);  }};

docs

组件文档目录,默认提供了四种语言的文档,目录构造为:docs/{lang}/comp-name.md。这些文档在组件页面加载(在 route.config.js 中有配置),先交给 md-loader 解决,提取其中的 vue 代码,而后交给 vue-loader 去解决,最初渲染到页面造成组件 demo + 文档。

demo-style

组件页面中显示的 组件 demo 的排版款式,和组件本身的款式无关,就像你业务代码中给组件定义排版款式一样。因为组件在有些场景下间接显示成果不好,所以就须要通过肯定的排版,比方 button 页面、icon 页面等。

components

官网我的项目寄存一些全局组件的目录。

assets

官网我的项目的动态资源目录

组件库

element 组件库由两局部组成:/src/packages

src

利用模块化的开发思维,把组件依赖的一些公共模块放在 /src 目录下,并根据性能拆分出以下模块:

  • utils,定义了一些工具办法
  • transitions,动画
  • mixins,全局混入的一些办法
  • locale,国际化性能以及各种语言的 局部组件 的翻译文件
  • directives,指令

/src/index.js 是通过脚本 /build/bin/build-entry.js 脚本主动生成,是组件库的入口。负责主动导入组件库的所有组件、定义全量注册组件库组件的 install 办法,而后导出版本信息、install 和 各个组件。

/* 通过 './build/bin/build-entry.js' 文件主动生成 */// 引入所有组件import Pagination from '../packages/pagination/index.js';import Dialog from '../packages/dialog/index.js';// ...// 组件数组,有些组件没在外面,这些组件不须要通过 Vue.use 或者 Vue.component 的形式注册,间接挂载到 Vue 原型链上const components = [  Pagination,  Dialog,  // ...]// 定义 install 办法,负责全量引入组件库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 原型链上挂点货色  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;};// 通过 CDN 引入组件库时,走上面这段代码,全量注册组件库if (typeof window !== 'undefined' && window.Vue) {  install(window.Vue);}// 导出版本信息、install 办法、各个组件export default {  version: '2.15.0',  locale: locale.use,  i18n: locale.i18n,  install,  CollapseTransition,  Loading,  // ...}

为了缩小篇幅,只贴出文件的一部分,但足以阐明所有。

/packages

element 将组件全副都放在了 /packages 目录下,每个组件以目录为单位,目录构造以及其中的根本代码是通过脚本 /build/bin/new.js 主动生成的。目录构造为:

  • package-name,连字符模式的包名

    • index.js,组件的 install 办法,示意组件是以 Vue 插件的模式存在
    • src,组件的源码目录

      • main.vue 组件的根本构造曾经就绪

比方新建的 city 组件的目录及文件是这样的:

  • city

    • index.js

      import City from './src/main';/* istanbul ignore next */City.install = function(Vue) {  Vue.component(City.name, City);};export default City;
    • src

      • main.vue

        <template>  <div class="el-city"></div></template><script>export default {  name: 'ElCity'};</script>

其实 /packages 目录下除了组件之外,还有一个非凡的目录 theme-chalk,它是组件库的款式目录,所有组件的款式代码都在这里,element 的组件文件中没有定义款式。theme-chalk 目录也是一个我的项目,通过 gulp 打包,并反对独立公布,其目录构造是这样的:

  • theme-chalk

    • src,组件款式的源码目录

      • index.scss,引入目录下所有的款式文件
      • comp.scss,组件款式文件,比方:button.scss
      • other,比方:字体、公共款式、变量、办法等
    • .gitignore
    • gulpfile.js

      'use strict';// gulp 配置文件const { series, src, dest } = require('gulp');const sass = require('gulp-sass');const autoprefixer = require('gulp-autoprefixer');const cssmin = require('gulp-cssmin');// 将 scss 编译成 css 并压缩,最初输入到 ./lib 目录下function compile() {  return src('./src/*.scss')    .pipe(sass.sync())    .pipe(autoprefixer({      browsers: ['ie > 9', 'last 2 versions'],      cascade: false    }))    .pipe(cssmin())    .pipe(dest('./lib'));}// 拷贝 ./src/fonts 到 ./lib/fontsfunction copyfont() {  return src('./src/fonts/**')    .pipe(cssmin())    .pipe(dest('./lib/fonts'));}exports.build = series(compile, copyfont);
    • package.json
    • README.md

测试

组件库的测试项目,应用 karma 框架

类型申明

每个组件的类型申明文件,TS 我的项目应用组件库时有更好的代码提醒。

完结

到这里 element 的源码架构剖析就完结了,倡议读者参照文章,亲自去浏览框架源码并增加正文,这样了解会更深,也更利于后续工作的发展。下一篇将具体解说 基于 Element 为团队打造组件库 的过程。

链接

  • Element 源码架构 思维导图版
  • Element 源码架构 视频版,关注微信公众号,回复: "Element 源码架构视频版" 获取
  • 组件库专栏

    • 如何疾速为团队打造本人的组件库(下)—— 基于 element-ui 为团队打造本人的组件库
  • github

文章已收录到 github,欢送 Watch 和 Star。