(转)ElementUI的构建流程
Element的目录结构
先看看目录结构,从目录结构入手,一步步进行分解。
├─build // 构建相关的脚本和配置 ├─examples // 用于展示Element组件的demo ├─lib // 构建后生成的文件,发布到npm包 ├─packages // 组件代码 ├─src // 引入组件的入口文件 ├─test // 测试代码 ├─Makefile // 构建文件 ├─components.json // 组件列表 └─package.json
有哪些构建命令
刚打开的时候看到了一个Makefile文件,如果学过c/c++的同学对这个东西应该不陌生。makefile可以说是比较早出现在UNIX 系统中的工程化工具,通过一个简单的make XXX来执行一系列的编译和链接操作。不懂makefile文件的可以看这篇文章了解下:前端入门->makefile。
default: help install: npm install new: node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS)) dev: npm run dev deploy: @npm run deploy dist: install npm run dist pub: npm run pub help: @echo "make 命令使用说明" @echo "make install --- 安装依赖" @echo "make new <component-name> [中文名] --- 创建新组件 package. 例如 'make new button 按钮'" @echo "make dev --- 开发模式" @echo "make dist --- 编译项目,生成目标文件" @echo "make deploy --- 部署 demo" @echo "make pub --- 发布到 npm 上" @echo "make new-lang <lang> --- 为网站添加新语言. 例如 'make new-lang fr'"
开发模式与构建入口文件
这里我们只挑选几个重要的看看。首先看到make install,使用的是npm进行依赖安装,但是Element实际上是使用yarn进行依赖管理,所以如果你要在本地进行Element开发的话,最好使用yarn进行依赖安装。在官方的贡献指南也有提到。

同时在package.json文件中有个bootstrap命令就是使用yarn来安装依赖。
"bootstrap": "yarn || npm i",
安装完依赖之后,就可以进行开发了,运行npm run dev,可以通过webpack-dev-sever在本地运行Element官网的demo。
"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 " "build:file": " node build/bin/iconInit.js & // 解析icon.scss,将所有小图标的name存入examples/icon.json node build/bin/build-entry.js & // 根据components.json,生成入口文件 node build/bin/i18n.js & // 根据examples/i18n/page.json和模板,生成不同语言的demo node build/bin/version.js // 生成examples/versions.json,键值对,各个大版本号对应的最新版本 "
在通过webpack-dev-server运行demo时,有个前置条件,就是通过npm run build:file生成目标文件。这里主要看下node build/bin/build-entry.js,这个脚本用于生成Element的入口js。先是读取根目录的components.json,这个json文件维护着Element的所有的组件名,键为组件名,值为组件源码的入口文件;然后遍历键值,将所有组件进行import,对外暴露install方法,把所有import的组件通过Vue.component(name, component)方式注册为全局组件,并且把一些弹窗类的组件挂载到Vue的原型链上。具体代码如下(ps:对代码进行一些精简,具体逻辑不变):
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 includeComponentTemplate = []; var installTemplate = []; var listTemplate = []; Object.keys(Components).forEach(name => { var componentName = uppercamelcase(name); //将组件名转为驼峰 var componetPath = Components[name] includeComponentTemplate.push(`import ${componentName} from '.${componetPath}';`); // 这几个特殊组件不能直接注册成全局组件,需要挂载到Vue的原型链上 if (['Loading', 'MessageBox', 'Notification', 'Message'].indexOf(componentName) === -1) { installTemplate.push(` ${componentName}`); } if (componentName !== 'Loading') listTemplate.push(` ${componentName}`); }); var template = `/* Automatically generated by './build/bin/build-entry.js' */ ${includeComponentTemplate.join(endOfLine)} import locale from 'element-ui/src/locale'; import CollapseTransition from 'element-ui/src/transitions/collapse-transition'; const components = [ ${installTemplate.join(',' + endOfLine)}, 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); } module.exports = { version: '${process.env.VERSION || require('../../package.json').version}', locale: locale.use, i18n: locale.i18n, install, CollapseTransition, Loading, ${listTemplate.join(',' + endOfLine)} }; module.exports.default = module.exports; `; // 写文件 fs.writeFileSync(OUTPUT_PATH, template); console.log('[build entry] DONE:', OUTPUT_PATH);
最后生成的代码如下:
/* Automatically generated by './build/bin/build-entry.js' */ import Button from '../packages/button/index.js'; import Table from '../packages/table/index.js'; import Form from '../packages/form/index.js'; import Row from '../packages/row/index.js'; import Col from '../packages/col/index.js'; // some others Component import locale from 'element-ui/src/locale'; import CollapseTransition from 'element-ui/src/transitions/collapse-transition'; const components = [ Button, Table, Form, Row, Menu, Col, // some others Component ]; 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); } module.exports = { version: '2.4.6', locale: locale.use, i18n: locale.i18n, install, Button, Table, Form, Row, Menu, Col, // some others Component }; module.exports.default = module.exports;
最后有个写法需要注意:module.exports.default = module.exports;,这里是为了兼容ESmodule,因为es6的模块export default xxx,在webpack中最后会变成类似于exports.default = xxx的形式,而import ElementUI from 'element-ui';会变成ElementUI = require('element-ui').default的形式,为了让ESmodule识别这种commonjs的写法,就需要加上default。
exports对外暴露的install方法就是把Element组件注册会全局组件的方法。当我们使用Vue.use时,就会调用对外暴露的install方法。如果我们直接通过script的方式引入vue和Element,检测到Vue为全局变量时,也会调用install方法。
// 使用方式1 <!-- import Vue before Element --> <script src="https://unpkg.com/vue/dist/vue.js"></script> <!-- import JavaScript --> <script src="https://unpkg.com/element-ui/lib/index.js"></script> // 使用方式2 import Vue from 'vue'; import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); // 此时会调用ElementUI.install()
在module.exports对象中,除了暴露install方法外,还把所有组件进行了对外的暴露,方便引入单个组件。
import { Button } from 'element-ui';
Vue.use(Button);
但是如果你有进行按需加载,使用Element官方的babel-plugin-component插件,上面代码会转换成如下形式:
var _button = require('element-ui/lib/button') require('element-ui/lib/theme-chalk/button.css') Vue.use(_button)
那么前面module.exports对外暴露的单组件好像也没什么用。
不过这里使用npm run build:file生成文件的方式是可取的,因为在实际项目中,我们每新增一个组件,只需要修改components.json文件,然后使用npm run build:file重新生成代码就可以了,不需要手动去修改多个文件。
在生成了入口文件的index.js之后就会运行webpack-dev-server。
webpack-dev-server --config build/webpack.demo.js
接下来看下webpack.demo.js的入口文件:
// webpack.demo.js const webpackConfig = { entry: './examples/entry.js', output: { path: path.resolve(process.cwd(), './examples/element-ui/'), publicPath: process.env.CI_ENV || '', filename: '[name].[hash:7].js', chunkFilename: isProd ? '[name].[hash:7].js' : '[name].js' }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { main: path.resolve(__dirname, '../src'), packages: path.resolve(__dirname, '../packages'), examples: path.resolve(__dirname, '../examples'), 'element-ui': path.resolve(__dirname, '../') }, modules: ['node_modules'] } // ... some other config } // examples/entry.js import Vue from 'vue'; import Element from 'main/index.js'; Vue.use(Element);
新建组件
entry.js就是直接引入的之前build:file中生成的index.js的Element的入口文件。因为这篇文章主要讲构建流程,所以不会仔细看demo的源码。下面看看Element如何新建一个组件,在Makefile可以看到使用make new xxx新建一个组件。
new: node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS))
这后面的$(filter-out $@,$(MAKECMDGOALS))就是把命令行输入的参数直接传输给node build/bin/new.js,具体细节这里不展开,还是直接看看build/bin/new.js的具体细节。
// 参数校验 if (!process.argv[2]) { console.error('[组件名]必填 - Please enter new component name'); process.exit(1); } const path = require('path'); const fileSave = require('file-save'); const uppercamelcase = require('uppercamelcase'); // 获取命令行的参数 // e.g. node new.js input 输入框 // process.argv表示命令行的参数数组 // 0是node,1是new.js,2和3就是后面两个参数 const componentname = process.argv[2]; // 组件名 const chineseName = process.argv[3] || componentname; const ComponentName = uppercamelcase(componentname); // 转成驼峰表示 // 组件所在的目录文件 const PackagePath = path.resolve(__dirname, '../../packages', componentname); // 检查components.json中是否已经存在同名组件 const componentsFile = require('../../components.json'); if (componentsFile[componentname]) { console.error(`${componentname} 已存在.`); process.exit(1); } // componentsFile中写入新的组件键值对 componentsFile[componentname] = `./packages/${componentname}/index.js`; fileSave(path.join(__dirname, '../../components.json')) .write(JSON.stringify(componentsFile, null, ' '), 'utf8') .end('\n'); const Files = [ { filename: 'index.js', content: `index.js相关模板` }, { filename: 'src/main.vue', content: `组件相关的模板` }, // 下面三个文件是的对应的中英文api文档 { 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('../../test/unit/specs', `${componentname}.spec.js`), content: `组件相关测试用例的模板` }, { filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`), content: `组件的样式文件` }, { filename: path.join('../../types', `${componentname}.d.ts`), content: `组件的types文件,用于语法提示` } ]; // 生成组件必要的文件 Files.forEach(file => { fileSave(path.join(PackagePath, file.filename)) .write(file.content, 'utf8') .end('\n'); });
这个脚本最终会在components.json写入组件相关的键值对,同时在packages目录创建对应的组件文件,并在packages/theme-chalk/src目录下创建一个样式文件,Element的样式是使用sass进行预编译的,所以生成是.scss文件。大致看下packages目录下生成的文件的模板:
{ filename: 'index.js', content: ` import ${ComponentName} from './src/main'; /* istanbul ignore next */ ${ComponentName}.install = function(Vue) { Vue.component(${ComponentName}.name, ${ComponentName}); }; export default ${ComponentName}; ` }, { filename: 'src/main.vue', content: ` <template> <div class="el-${componentname}"></div> </template> <script> export default { name: 'El${ComponentName}' }; </script> ` }
每个组件都会对外单独暴露一个install方法,因为Element支持按需加载。同时,每个组件名都会加上El前缀。,所以我们使用Element组件时,经常是这样的el-xxx,这符合W3C的自定义HTML标签的规范(小写,并且包含一个短杠)。
打包流程
由于现代前端的复杂环境,代码写好之后并不能直接使用,被拆成模块的代码,需要通过打包工具进行打包成一个单独的js文件。并且由于各种浏览器的兼容性问题,还需要把ES6语法转译为ES5,sass、less等css预编译语言需要经过编译生成浏览器真正能够运行的css文件。所以,当我们通过npm run new component新建一个组件,并通过npm run dev在本地调试好代码后,需要把进行打包操作,才能真正发布到npm上。
这里运行npm run dist进行Element的打包操作,具体命令如下。
"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
"

浙公网安备 33010602011771号