写在开头
在这一篇文章中 ElementUI源码系列三 - 学习gen-cssfile.js文件之自动创建组件的.scss文件与生成index.scss文件内容 我们讲过添加一个新组件要经历以下三个步骤:
- 第一步 - 创建组件目录结构
- 第二步 - 创建组件样式文件
- 第三步 - 总入口文件引入组件
经过前两篇文章的学习,我们已经实现了第一、第二步骤的自动化处理,只需由一行命令(npm run new xxx
)即可完成操作。这一章节我们来搞定第三步骤的自动化处理,下面接着来开启愉快的旅途吧。
课前预备
build-entry.js
首先,我们先把 build-entry.js
文件创建处理,在 build/bin
目录下:
老样子,给这个脚本文件配置命令:
"scripts": {
"dev": "webpack-dev-server --config build/webpack.common.js",
"build": "webpack --config build/webpack.common.js",
"build:theme": "gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
"gen": "node build/bin/gen-cssfile.js",
"new": "node build/bin/new.js",
"build:entry": "node build/bin/build-entry.js"
},
不知不觉,我们已经拥有了六条命令,希望你能记住它们各自的作用,后面我们会把它们进行一些合并。不要觉得多,这才哪到哪,element-ui 源码中的命令直接是一整坨,干懵你。(ㄒoㄒ)
json-templater模块
json-templater 模块是一个字符串模板生成器,它能帮助我们更加快速、便捷的生成好字符串模板。
下载该包:npm install json-templater@1.0.4 -D
在项目根目录下创建 test.js
文件,基本使用:
// test.js
var render = require('json-templater/string');
var strs = `
姓名:{{name}}
年龄:{{age}}
爱好:{{hobby.coding}}、{{hobby.writing}}
其他:{{others}}
`;
var template = render(strs, {
name: '橙某人',
age: 18,
hobby: {
coding: '写代码',
writing: '写文章',
},
others: ['var a = 1;', 'var b = 2;', 'var c = 3;'].join('\n') // 数组变字符串并加换行符
});
console.log(template)
生成总入口index.js文件
推导index.js文件字符串模板
在编写 build-entry.js
内容之前,我们先来观察一下我们现有的项目总入口 src/index.js
文件:
import Button from '../packages/button/index.js';
import Divider from '../packages/divider/index.js';
const components = [
Button, Divider
];
const install = (Vue) => {
components.forEach(component => {
Vue.component(component.name, component)
})
}
export default {
install,
Button,
Divider
}
因为要自动生成该文件内容,那么我们需要先推导出该文件的字符串模块,后面才好去动态生成文件内容。
我们根据 json-templater
模块的内容来做第一次推导:
// build-entry.js
var render = require('json-templater/string');
var strs = `
{{include}}
const components = [
{{list}}
];
const install = function(Vue, opts = {}) {
components.forEach(component => {
Vue.component(component.name, component);
});
};
export default {
install,
{{list}}
};
`;
var template = render(strs, {
include: [
'import Button from \'../packages/button/index.js\';';
'import Divider from \'../packages/divider/index.js\';';
].join('\n'),
list: ['Button', 'Divider'].join(',' + '\n'),
});
是不是很简单,应该不难吧?第二次推导:
// build-entry.js
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
var strs = `
{{include}}
const components = [
{{list}}
];
const install = function(Vue, opts = {}) {
components.forEach(component => {
Vue.component(component.name, component);
});
};
export default {
install,
{{list}}
};
`;
// 定义相关变量
var ComponentNames = ['button', 'divider'];
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
var includeComponentTemplate = [];
var listTemplate = [];
// 循环收集动态数据
ComponentNames.forEach(name => {
var componentName = uppercamelcase(name); // ['Button', 'Divider']
includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
name: componentName,
package: name
}));
listTemplate.push(` ${componentName}`); // [' Button', ' Divider']
})
var template = render(strs, {
include: includeComponentTemplate.join('\n'),
list: listTemplate.join(',' + '\n'),
});
第二次推导结果应该也不算难,我们做了一些变量的定义,用循环的形式来收集相应的动态数据。现在生成的模板还不是最终想要的,再优化优化:
// build-entry.js
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
var endOfLine = require('os').EOL;
var strs = `
{{include}}
const components = [
{{list}}
];
const install = function(Vue, opts = {}) {
components.forEach(component => {
Vue.component(component.name, component);
});
};
export default {
install,
{{list}}
};
`;
// 引入 `components.json`
var Components = require('../../components.json')
var ComponentNames = Object.keys(Components);
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
var includeComponentTemplate = [];
var listTemplate = [];
ComponentNames.forEach(name => {
var componentName = uppercamelcase(name);
includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
name: componentName,
package: name
}));
listTemplate.push(` ${componentName}`);
})
var template = render(strs, {
include: includeComponentTemplate.join(endOfLine), // 替换换行符
list: listTemplate.join(',' + endOfLine),
});
也没做多少改动,就是引入了 var endOfLine = require('os').EOL;
,替换了所有的换行符。因为换行符不同系统需要兼容,os.EOL
属性是一个常量,返回当前操作系统的换行符,Windows系统是\r\n
,其他系统是\n
。
另一个改动就是引入 components.json
文件,根据这个文件来生成 index.js
文件内容。
os
模块是Node
环境的内置模块,和path
、fs
等模块一样,并不需要单独下载,更多详情可以点我。
生成index.js文件内容
上面推导出最终的模板,也就是 index.js
的内容了,最后我们把它写入文件即可。
// build-entry.js
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
var endOfLine = require('os').EOL;
var strs = `
{{include}}
const components = [
{{list}}
];
const install = function(Vue, opts = {}) {
components.forEach(component => {
Vue.component(component.name, component);
});
};
export default {
install,
{{list}}
};
`;
var Components = require('../../components.json')
var ComponentNames = Object.keys(Components);
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
var includeComponentTemplate = [];
var listTemplate = [];
ComponentNames.forEach(name => {
var componentName = uppercamelcase(name);
includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
name: componentName,
package: name
}));
listTemplate.push(` ${componentName}`);
})
var template = render(strs, {
include: includeComponentTemplate.join(endOfLine),
list: listTemplate.join(',' + endOfLine),
});
// 写入文件
var path = require('path');
var fs = require('fs');
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
fs.writeFileSync(OUTPUT_PATH, template);
最终我们执行 npm run build:entry
命令,index.js
文件内容就自动生成,是不是很棒!
到这里,我们就基本是实现了自动化创建组件的过程了,只需要两步:
- 执行
npm run new xxx
命令创建组件- 执行
npm run build:entry
引入组件就能搞定了。(✪ω✪)
build-entry.js 完整源码
下面我们丢一下 ElementUI
源码中 build-entry.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 = [];
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);
往期内容
- ElementUI源码系列一 - 从零搭建项目架构,项目准备、项目打包、项目测试流程
- ElementUI源码系列二 - 引入scss,用gulp把scss转成css并补全、压缩,用cp-cli移动目录、文件
- ElementUI源码系列三 - 学习gen-cssfile.js文件之自动创建组件的.scss文件与生成index.scss文件内容
- ElementUI源码系列四 - 学习new.js文件之自动创建组件目录结构与生成components.json文件内容
- ElementUI源码系列五 - 学习build-entry.js文件之自动生成总入口文件index.js内容
- ElementUI源码系列六 - 小结
- ElementUI源码系列七 - 组件按需引入
- ElementUI源码系列八 - 搭建element-ui官方文档之项目的基本框架
- ElementUI源码系列九 - 搭建element-ui官方文档之md文件渲染到页面、demo-block组件的实现
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。