ElementUI 是一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。
0、前言
老规矩,带着问题看源码:
- 组件全量引入和按需引入是如何做的?
- 主题是如何实现定制的?
- 国际化是如何实现的?
- 怎样支持CDN引入和基于webpack的两种开发模式?
- 开发组件时,组件MD文档是如何处理的?
1、目录结构
- 基本结构
build:存放构建相关的 shell 脚本和 js 脚本
examples:Element 官方网站前端代码
packages:组件库代码
src:官方网站的入口文件和一些公用代码,如utils,mixins,directives,transitions等
test:单元测试代码
types:类型定义文件(typescript)
注意这里没有最终编译生成的文件夹 lib,源码都这样,得运行脚本来构建lib - package.json
// 待发布的npm包由哪些目录组成"files": ["lib","src","packages","types"],// npm 包的入口"main": "lib/element-ui.common.js",// 类型定义入口"typings": "types/index.d.ts","scripts": {"bootstrap": "yarn || npm i","build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js","build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk","build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js","build:umd": "node build/bin/build-locale.js","clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage","deploy:build": "npm run build:file && cross-env NODE_ENV=production webpack --config build/webpack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME","dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js","dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js","dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme","i18n": "node build/bin/i18n.js","lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet","pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js && sh build/deploy-faas.sh","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"},
2、构建脚本分析
2.1、npm run dev
- npm run build:file 并行执行以下四个js脚本
1、node build/bin/iconInit.js
通过 postcss 解析 icon.scss ,筛选出类名并最终导出到 icon.json 文件
// node build/bin/iconInit.js'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]);}});classList.reverse(); // 希望按 css 文件顺序倒序排列fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList), () => {});// 效果:// icon.scss 部分.el-icon-platform-eleme:before {content: "\e7ca";}// 生成的 icon.json['platform-eleme']
至于生成的 icon.json 有啥用先不管。
2、node build/bin/build-entry.js
构建 src/index.js 这个文件,这个文件可能随着组件的增加删除会经常变动,故用脚本来产生
var Components = require('../../components.json'); // 所有可用组件的映射表(组件名=>组件定义)var fs = require('fs');var render = require('json-templater/string'); // 模板渲染工具var uppercamelcase = require('uppercamelcase'); // 转驼峰 a-bc =>ABcvar 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(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'].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);
缺点:components.json需要自行维护,不够自动化
3、node build/bin/i18n.js
以 i18n/page.json 作为数据,以 pages/templates 作为模版来生成 pages 目录下的多语言版本。官方网站支持多语言版本就是这么来的
'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 }`));}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);});});
4、node build/bin/version.js
记录 Element 版本号到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.7.2' };if (!content[version]) content[version] = '2.8';fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));
- webpack-dev-server —config build/webpack.demo.js 与 node build/bin/template.js 并行执行
1、node build/bin/template.js
监听 examples/pages/template 下文件的变化并运行 npm run i18n 重新生成多语言版本的 pages
const path = require('path');const templates = path.resolve(process.cwd(), './examples/pages/template');const chokidar = require('chokidar'); // 专门用于文件监控的库let watcher = chokidar.watch([templates]);watcher.on('ready', function() {watcher.on('change', function() {exec('npm run i18n');});});function exec(cmd) {return require('child_process').execSync(cmd).toString().trim();}
2、build/webpack.demo.js
这个就是正式启动本地开发模式了,内容就不说了
2.2、分析 npm run dist
- npm run clean && npm run build:file && npm run lint
同上,略过 - webpack —config build/webpack.conf.js
构建入口为src/index.js ; 出口为 lib/index.js 用于打出UMD格式的包,供CDN方式引入
<!-- 引入样式 --><link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"><!-- 引入组件库 --><script src="https://unpkg.com/element-ui/lib/index.js"></script>
这里 index.css 的生成请看 npm run build:theme 的分析
- npm run build:theme
1、node build/bin/gen-cssfile
产生 index.scss / index.css 文件,这个文件引入了所有组件的 scss/css 文件
2、gulp build —gulpfile packages/theme-chalk/gulpfile.js
编译 scss 文件为 css 文件,包括各组件的 css 文件和一个总的 css 文件
3、cp-cli packages/theme-chalk/lib lib/theme-chalk
复制 packages/theme-chalk/lib 至 lib/theme-chalk - webpack —config build/webpack.component.js
构建入口为 components.json ; 出口为 lib/[name].js 用于将 packages 中的所有组件单独打出一个 js 文件用于做按需加载 - webpack —config build/webpack.common.js
构建入口为src/index.js ; 出口为 lib/element-ui.common.js 用于打出commonjs格式的包,用以完全导入方式使用,产生的 element-ui.common.js 也是 package.json 的 main 入口 - npm run build:utils
将 src 目录下除 index.js 外的所有文件 Babel 编译到 lib 目录下。算是除了组件库以外,额外提供了一些小工具供开发者使用,如:
import { kebabCase } from 'element-ui/src/utils/util';
- npm run build:umd
将 src/locale/lang 下的ES6格式的文件转为UMD格式,放在 lib/umd/locale。用于CDN方式加载。
3、小结&收获
小结
回答下开头的问题:
- 组件全量引入和按需引入是如何做的?
如果是 cdn 方式来加载,则只能全量引入。如果是用 webpack 这种工程方式引入,则两种方式都可以,其中按需引入借助了 babel-plugin-component
// .babelrc{"presets": [["es2015", { "modules": false }]],"plugins": [["component",{"libraryName": "element-ui","styleLibraryName": "theme-chalk"}]]}// 上述配置会转换以下代码import { Button } from 'element-ui';// 转为import Button from 'element-ui/lib/button.js'import Button from 'element-ui/lib/theme-chalk/button.css'
- 主题是如何实现定制的?
有两种主要方式:1、如果使用scss,则是通过修改 scss 变量来实现主题定制;2、如果使用css,则手动引入定制好的css文件来替换默认的css文件 - 组件国际化是如何实现的?
将组件中的使用的文本抽离出来,然后用各种不同的语言去填充即可实现。难点在于怎样提供多语言版本的文件 - 怎样支持CDN引入和基于webpack的两种开发模式?
一套源码打两套格式的包,一种umd格式,一种 commonjs2 格式。 - 开发组件时,组件MD文档是如何处理的?
ElementUI 开发了一个 md-loader 来把 .md 文档封装成 .vue 组件,实现了组件文档的渲染
收获
- postcss.parse 可以将 scss 文件内容处理成 js 对象,再通过 postcss.stringify 转回 scss 文件。放便对scss文件做批处理
- 可通过 require(‘child_process’).execSync(cmd).toString().trim() 来获取 shell 脚本执行的结果
- cross-env 设置环境变量可屏蔽 mac 和 window 系统的差异
- commonjs , commonjs2 区别(一个用 exports导出,一个用module.exports,所以我们平时用的都是commonjs2)
commonjs: exports['MyLibrary'] = entry_returncommonjs2: module.exports = entry_return
