element组件库源码 — 官网打包原理

element 源码解读

1
npm run dev

运行 命令详情

1
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"

更新依赖 npm run bootstrap

更新项目依赖

1
yarn || npm i

文件预处理 npm run build:file

处理文件

1
node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js

让我们看看每个部分分别作了什么

处理icon node build/bin/iconInit.js

通过读取icon.scss文件初始化icon列表,生成文件输出到example/icon.json

☁️ 通过postcss解析文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'use strict';

var postcss = require('postcss');
var fs = require('fs');
var path = require('path');
var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');
var nodes = postcss.parse(fontFile).nodes;
var classList = [];

nodes.forEach((node) => {
var selector = node.selector || '';
var reg = new RegExp(/\.el-icon-([^:]+):before/);
var arr = selector.match(reg);

if (arr && arr[1]) {
classList.push(arr[1]);
}
});

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

生成入口文件 node build/bin/build-entry.js

生成src/index.js文件

  • components.json文件(文件中保存所有组件,key为组件名称,value为组件文件路径)
  • 定义一个index.js文件内容的基本模板
  • 读取components.json文件,动态根据模板生成index.js文件,其中包含所有组件的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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 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.map(component => {
Vue.component(component.name, component); //注册为全局组件
});

Vue.use(Loading.directive); // 安装插件

Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
}; // 定义$ELEMENT 遍历保存全局变量

// 注册全局方法
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);
}

module.exports = {
version: '{{version}}',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
Loading,
{{list}}
};

module.exports.default = module.exports;
`;

delete Components.font;

var ComponentNames = Object.keys(Components); // 获取所有组件名称

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

ComponentNames.forEach(name => { // 遍历处理组件
var componentName = uppercamelcase(name); // 转换为驼峰命名

//IMPORT_TEMPLATE:
includeComponentTemplate.push(
render('import {{name}} from \'../packages/{{package}}/index.js\';', {
name: componentName,
package: name
}));

if (['Loading', 'MessageBox', 'Notification', 'Message'].indexOf(componentName) === -1) {
installTemplate.push(render(' {{name}}', {
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(path.join(__dirname, '../../src/index.js'), template);

console.log('[build entry] DONE:', OUTPUT_PATH);

🍃知识点:

Vue.use(plugin) 安装插件(plugin:如果是对象要包含install方法)使用Vue.use会自动足迹多次调用,保证install函数只会调用一次

使用场景

  • 添加全局方法
  • 添加全局资源,如directive
  • 注入组件选项,Vue.mix
  • 添加示例, Vue.prototype.$myMethod

国际化 node build/bin/i18n.js

关于国际化的处理【此处处理的十几个大的板块的页面】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use strict';

var fs = require('fs');
var path = require('path');
var langConfig = require('../../examples/i18n/page.json'); // 保存国际化各个相关页面的文案

langConfig.forEach(lang => {
// 判断目标文件是否存在,如果不存在新建一个
try {
fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
} catch (e) {
fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
}

// 遍历lang.pages
Object.keys(lang.pages).forEach(page => {
var templatePath = path.resolve(__dirname, `../../examples/pages/template/${ page }.tpl`);
var outputPath = path.resolve(__dirname, `../../examples/pages/${ lang.lang }/${ page }.vue`);
var content = fs.readFileSync(templatePath, 'utf8');
var pairs = lang.pages[page];

// 替换模板文件中的站位符
Object.keys(pairs).forEach(key => {
content = content.replace(new RegExp(`<%=\\s*${ key }\\s*>`, 'g'), pairs[key]);
});
fs.writeFileSync(outputPath, content);
});
});

🍃 知识点

.tpl文件,是template的缩写,用来保存模板文件

版本处理 node build/bin/version.js

1
2
3
4
5
6
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' };
if (!content[version]) content[version] = '2.4';
fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));

运行

webpack-dev-server

运行指令

1
cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js

✨ build/webpack.demo.js 文件

🍃相关知识了解

vue-markdown-loader

https://www.npmjs.com/package/vue-markdown-loader

使用markdown-it 吧.md文件转换为Vue 组件

也可以自定义模块,例如下面🌰

1
2
3
4
> ::: warning
> *here be dragons*
> :::
>

>

将转换为

1
2
3
4
> <div class="warning">
> <em>here be dragons</em>
> </div>
>

markdown-it-anchor

https://github.com/valeriangalliat/markdown-it-anchor

为md文件转换的html添加锚点

markdown-it-container

对自定义模块进行特殊处理

应用

主要看对.md 文件的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const path = require('path');
const webpack = require('webpack');
const md = require('markdown-it')();
const slugify = require('transliteration').slugify;
...
const striptags = require('./strip-tags');
const config = require('./config');
...
function convert(str) {
str = str.replace(/(&#x)(\w{4});/gi, function($0) {
return String.fromCharCode(parseInt(encodeURIComponent($0).replace(/(%26%23x)(\w{4})(%3B)/g, '$2'), 16));
});
return str;
}

function wrap(render) {
return function() {
return render.apply(this, arguments)
.replace('<code v-pre class="', '<code class="hljs ')
.replace('<code>', '<code class="hljs">');
};
}

const webpackConfig = {
...
module: {
rules: [
...
{
test: /\.md$/,
loader: 'vue-markdown-loader', // https://www.npmjs.com/package/vue-markdown-loader
options: {
use: [ // https://github.com/valeriangalliat/markdown-it-anchor
[require('markdown-it-anchor'), {
level: 2, // 在所选级别或所选级别数组上应用锚点的最低级别。
slugify: slugify, // 自定义分段功能。
permalink: true, // 是否在标题旁边添加永久链接。
permalinkBefore: true // 将永久链接放在标题之前。
}],
/** https://www.npmjs.com/package/markdown-it-container
* Params:
name - container name (mandatory)
options:
validate - optional, function to validate tail after opening marker, should return true on success.
render - optional, renderer function for opening/closing tokens.
marker - optional (:), character to use in delimiter.
*/
[require('markdown-it-container'), 'demo', {
// 将.md文件里的demo模块,转换为demo-block组件
validate: function(params) { // 匹配规则
return params.trim().match(/^demo\s*(.*)$/);
},

render: function(tokens, idx) {
var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
if (tokens[idx].nesting === 1) {
var description = (m && m.length > 1) ? m[1] : '';
var content = tokens[idx + 1].content;
var html = convert(striptags.strip(content, ['script', 'style'])).replace(/(<[^>]*)=""(?=.*>)/g, '$1'); // 提取 html 的内容作为参数传给demo-block组件
var script = striptags.fetch(content, 'script');
var style = striptags.fetch(content, 'style');
var jsfiddle = { html: html, script: script, style: style };
var descriptionHTML = description
? md.render(description)
: ''; // 使用markdown-it渲染内容

jsfiddle = md.utils.escapeHtml(JSON.stringify(jsfiddle));

return `<demo-block class="demo-box" :jsfiddle="${jsfiddle}">
<div class="source" slot="source">${html}</div>
${descriptionHTML}
<div class="highlight" slot="highlight">`;
}
return '</div></demo-block>\n';
}
}],
[require('markdown-it-container'), 'tip'],
[require('markdown-it-container'), 'warning']
],
preprocess: function(MarkdownIt, source) {
// 处理 table
MarkdownIt.renderer.rules.table_open = function() {
return '<table class="table">';
};
MarkdownIt.renderer.rules.fence = wrap(MarkdownIt.renderer.rules.fence);
return source;
}
}
},
....
]
},
...
};

.....

module.exports = webpackConfig;

node build/bin/template.js

监听模板文件(‘./examples/pages/template’)如果模板文件变化运行 npm run i18n