文章已收录到 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 模式的字符串转换为 CompName
var uppercamelcase = require('uppercamelcase');
var path = require('path');
var endOfLine = require('os').EOL;
// 输入门路 /src/index.js
var 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.js
fs.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);
// 所有组件包的根底门路,/packages
var 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.json
const 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.json
const 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.json
const 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.json
const 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');
// 组件名称,比方 city
const componentname = process.argv[2];
// 组件的中文名称
const chineseName = process.argv[3] || componentname;
// 将组件名称转换为大驼峰模式,city => City
const ComponentName = uppercamelcase(componentname);
// 组件包目录,/packages/city
const 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 i18n
watcher.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 dev
if test -n "$(git status --porcelain)"; then
echo 'Unclean working tree. Commit or stash changes first.' >&2;
exit 128;
fi
if ! git fetch --quiet 2>/dev/null; then
echo 'There was a problem fetching your branch. Run `git fetch` to see more...' >&2;
exit 128;
fi
if test "0" != "$(git rev-list --count --left-only @'{u}'...HEAD)"; then
echo 'Remote history differ. Please pull changes.' >&2;
exit 128;
fi
echo 'No conflicts.' >&2;
/build/release.sh
脚本实现了以下工作:
- 合并 dev 分支到 master、
- 批改款式包和组件库的版本号
- 公布款式包和组件库
- 提交 master 和 dev 分支到近程仓库
该脚本在公布组件库时能够应用,特地是其中主动更改版本号的性能(每次 publish 时都忘改版本号)。这里提交代码到近程仓库的日志很简略,更具体的提交日志时通过更新日志文件 CHANGELOG.{lang}.md
提供的。
#!/usr/bin/env sh
set -e
# 合并 dev 分支到 master
# 编译打包
# 批改款式包和组件库的版本号
# 公布款式包和组件库
# 提交 master 和 dev 分支到近程仓库
# 合并 dev 分支到 master
git checkout master
git merge dev
# 版本抉择 cli
VERSION=`npx select-version-cli`
# 是否确认以后版本信息
read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r
echo # (optional) move to a new line
if [[ $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
fi
fi
/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.js
和 play/index.vue
,示例我的项目,比方你想看一个 element 中某个组件的成果,特地是组件按需加载时的显示成果,能够在 play/index.vue
中引入应用,应用 npm run dev:play
命令启动我的项目,也是在 /build/webpack.demo.js
中通过环境变量来配置的。
// play.js
import 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.js
export 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/fonts function 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。
发表回复