北京网站设计公司地址,去哪儿网站排名怎么做,怎么开通公司网站,保定seo排名收费需求分析当我们去实现一个组件库的时候#xff0c;并不会一上来就撸码#xff0c;而是把它当做产品一样#xff0c;思考一下我们的组件库的需求。那么对于 element-ui#xff0c;除了基于 Vue.js 技术栈开发组件#xff0c;它还有哪些方面的需求呢。丰富的 feature#x… 需求分析当我们去实现一个组件库的时候并不会一上来就撸码而是把它当做产品一样思考一下我们的组件库的需求。那么对于 element-ui除了基于 Vue.js 技术栈开发组件它还有哪些方面的需求呢。丰富的 feature丰富的组件自定义主题国际化。文档 demo提供友好的文档和 demo维护成本小支持多语言。安装 引入支持 npm 方式和 cdn 方式并支持按需引入。工程化开发测试构建部署持续集成。需求有了接下来就需要去思考如何去实现本文会依据 element-ui 2.11.1 版本的源码来分析这些需求是如何实现的。当然element-ui 早期一定不是这样子的我们分析的这个版本已经是经过它多次迭代优化后的如果你想去了解它的发展历程可以去 GitHub 搜索它的历史版本。丰富的 feature丰富的组件组件库最核心的还是组件先来看一下 element-ui 组件的设计原则一致、反馈、效率、可控。具体的解释在官网有我就不多贴了在 element-ui 开发团队背后有一个强大的设计团队这也得益于 element-ui 的创始人 sofish 在公司的话语权和地位争取到这么好的资源。所以 element-ui 组件的外型、配色、交互都做的非常不错。作为一个基础组件库还有一个很重要的方面就是组件种类丰富。element-ui 官方目前有 55 个组件分成了 6 大类分别是基础组件、表单类组件、数据类组件、提示类组件、导航类组件和其它类型组件。这些丰富的基础组件能很好地满足大部分 PC 端 to B 业务开发需求。开发这么多组件需要大量的时间和精力所以这里要非常感谢 element-ui 团队为我们提供了这些基础组件我们基于它们做二次开发节约了非常多的时间。element-ui 的组件源码在 packages 目录里维护而并不在 src 目录中。这么做并不是为了要采用 monorepo我也并没有找到 lerna 包管理工具这么做的目的我猜测是为了让每个组件可以单独打包支持按需引入。但实际上想达到这个目的也并不一定需要这么去组织维护代码我更推荐把组件库中的组件代码放在 src/components 目录中维护然后通过修改 webpack 配置脚本也可以做到每个组件单独打包以及支持按需引入源码放在 src 目录总是更合理的。自定义主题element-ui 的一大特色是支持自定义主题你可以使用在线主题编辑器可以修改定制 Element 所有全局和组件的 Design Tokens并可以方便地实时预览样式改变后的视觉。同时它还可以基于新的定制样式生成完整的样式文件包供直接下载使用那么它是如何做到这点的呢element-ui 组件的样式、公共样式都在 packages/theme-chalk 文件中并且它是可以独立发布的。element-ui 组件样式中的颜色、字体、线条等等样式都是通过变量的方式引入的在 packages/theme-chalk/src/common/var.scss 中我们可以看到这些变量的定义这样就给做多主题提供了方便因为我只要修改这些变量就可以实现组件的主题改变。了解了基本原理做在线替换主题也并不是难事了我并不会详细去讲在线定制主题前端交互部分感兴趣的同学可以自己去看源码都在 examples 目录中我这里只说一下本质的原理。想要做到在线换肤并且实时预览需要借助 server 的帮助比如主题可以通过一个配置去维护用户做一系列操作后会生成新的主题配置把这个配置通过接口提交的方式告诉 server然后 server 会根据这个配置做返回生成新的 CSS(具体的实施的方案未开源大致会做一些变量替换然后编译)新的 CSS 的样式就会覆盖默认的样式达到了切换主题的目的。我们可以在主题编辑页面打开网络面板可以看到有 2 个 xhr 请求如图其中updateVarible 是一个 POST 请求他会把你修改的的主题配置提交到后端 server提交的数据你可以自己去查看它的 Request Payload这个 POST 请求会返回一段 CSS 文本然后会动态插入到 head 标签的底部来覆盖默认样式你可以通过审查元素看到 head 底部会动态插入一个 id 为 chalk-style 的标签。下图就是该请求返回的样式文本 相关代码在 examples/components/theme/loader/index.vue 中。 onAction() { this.triggertProgressBar(true); const time new Date(); updateVars(this.userConfig) .then(res { this.applyStyle(res, time); }) .catch(err { this.onError(err); }) .then(() { this.triggertProgressBar(false); }); }, applyStyle(res, time) { if (time this.lastApply) return; this.updateDocs(() { updateDomHeadStyle(chalk-style, res); }); this.lastApply time; }onAction 函数中的 updateVars 就是去发送 POST 请求而 applyStyle 函数就是去修改和覆盖默认样式updateDocs 函数会去更新默认主题颜色updateDomHeadStyle 样式会添加或者修改 id 为 chalk-style 的 style 标签目的就是覆盖默认样式应用新主题样式。updateVars 请求在页面加载的时候会发起在你修改完主题配置后也会发起。再来看一下 getVarible 请求它是一个 GET 请求返回的内容是主题配置页面右侧配置面板的数据源如下图所示主题配置面板根据该数据源生成并且当你去编辑其中一项的时候又会发起 updateVars POST 请求把更新的配置提交然后后端会返回新的 CSS 并在前端生效。另外用户修改的配置还利用了 localStorage 在本地保存了一份这样用户每次编辑都可以保存一份主题下次也可以继续基于某个主题继续编辑。不过这么实现多主题也并非完美为了编译加速element-ui 把样式部分单独抽离出单独的文件这样给开发组件的同学带来很大的不便当你去编写组件的样式的时候需要在多个文件中来回切换而且这样也不符合组件就近管理的原则。但是如果把样式写在组件中server 端去编译生成单独样式文件的时间就会增长(需要从组件中提取 CSS)所以这是一个需要权衡的问题。国际化说到 Vue 的国际化方案大家很容易会联想到 vue-i18n 方案element-ui 并未引入 vue-i18n不过它是可以很好地与 vue-i18n 兼容的。所有的国际化方案都会用到语言包语言包通常会返回一个 JSON 格式的数据element-ui 组件库的语言包在 src/locale/lang 目录下以英语语言包为例export default { el: { colorpicker: { confirm: OK, clear: Clear } // ... }}在 packages/color-picker/src/components/picker-dropdown.vue 中我们在模板部分可以看到这个语言包的使用el-buttonsizeminitypetextclassel-color-dropdown__link-btn click$emit(clear) {{ t(el.colorpicker.clear) }}el-buttonel-buttonplainsizeminiclassel-color-dropdown__btn clickconfirmValue {{ t(el.colorpicker.confirm) }}el-button模板中用到的 t 函数它定义在 src/mixins/locale.js 中import { t } from element-ui/src/locale;export default { methods: { t(...args) { return t.apply(this, args); } }};实际上是在 src/locale/index.js 中定义的 t 函数export const t function(path, options) { let value i18nHandler.apply(this, arguments); if (value ! null value ! undefined) return value; const array path.split(.); let current lang; for (let i 0, j array.length; i const property array[i]; value current[property]; if (i j - 1) return format(value, options); if (!value) return ; current value; } return ;};这个函数是根据传入的 path 路径比如我们例子中的 el.colorpicker.confirm从语言包中找到对应的文案。其中 i18nHandler 是一个 i18n 的处理函数这块逻辑就是用来兼容外部的 i18n 方案如 vue-i18n。let i18nHandler function() { const vuei18n Object.getPrototypeOf(this || Vue).$t; if (typeof vuei18n function !!Vue.locale) { if (!merged) { merged true; Vue.locale( Vue.config.lang, deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true }) ); } return vuei18n.apply(this, arguments); }};export const i18n function(fn) { i18nHandler fn || i18nHandler;};export const use function(l) { lang l || lang;};可以看到 i18nHandler 默认会尝试去找 Vue 原型中的 $t 函数这是 vue-i185.x 的实现会在 Vue 的原型上挂载 $t 方法。另外它也暴露了 i18n 方法可以外部传入其它的 i18n 方法覆盖 i18nHandler。如果没有外部提供的 i18n 方法那么就直接找到当前的语言包 let current lang;接下来的逻辑就是从这个语言包对象中读到对应的字符串值当然如果字符串需要格式化则调用 format 函数这块逻辑同学们感兴趣可以自己看。因此在使用对应的语言包的时候一定要注册import lang from element-ui/lib/locale/lang/enimport locale from element-ui/lib/locale// 设置语言locale.use(lang)这样就注册了英文语言包在模板中就可以正常使用并找到对应的语言了。如果你要开发一个国际化项目在运行时才能知道用户的语言可以考虑使用异步动态加载的方式在渲染页面前先获取语言包另外也可以考虑做缓存优化不过这个话题延伸起来就有点多了未来我可能会单开一个主题去分享业务如何做国际化。文档 demo作为一个优秀的开源组件库友好的文档和 demo 是必不可少的它也能帮你招揽到不少用户。作为一个组件库的开发者和维护者也希望用最小的成本来维护文档和 demo。element-ui 的文档和 demo 是融为一体的我们打开它的文档可以看到文档不仅介绍了每个组件的使用方式还展示了组件的各种示例并且还可以清楚地看到每个示例的源码对用户而言非常友好。那么 element-ui 内部是如何去编写这些 demo 和文档的呢实际上每个组件的文档和 demo 都是通过一个单独的 .md 文件生成的那么它又是如何做到这点的呢element-ui 的 demo 源码都在 examples 目录中维护当我们在 element-ui 工程下运行 npm run dev 的时候会启动它的开发调试模式并且运行官方文档和 demo。看一下 npm scriptsscripts: { 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, dev: npm run bootstrap npm run build:file cross-env NODE_ENVdevelopment webpack-dev-server --config build/webpack.demo.js node build/bin/template.js,}我们省略了其它的 scripts重点看 dev 和相关的几个命令其中 bootstrap 的作用是安装依赖build:file 的作用是运行 build 目录下几个命令包括对 icon、entry、i18n、version 等初始化。在执行完 bootstrap 和 build:file 后通过 webpack-dev-server 运行 build/webpack.demo.js这个是重点我们来看一下这个 webpack 的配置文件。const webpackConfig { mode: process.env.NODE_ENV, entry: isProd ? { docs: ./examples/entry.js, element-ui: ./src/index.js } : (isPlay ? ./examples/play.js : ./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: config.alias, modules: [node_modules] }, devServer: { host: 0.0.0.0, port: 8085, publicPath: /, hot: true }, module: { rules: [ { test: /\.vue$/, loader: vue-loader, options: { compilerOptions: { preserveWhitespace: false } } }, { test: /\.md$/, use: [ { loader: vue-loader, options: { compilerOptions: { preserveWhitespace: false } } }, { loader: path.resolve(__dirname, ./md-loader/index.js) } ] } ] }};由于整个配置文件内容比较长我只保留了重点的部分重点看一下 entry 和 module 下的 rules。element-ui 官网本质上就是一个用 vue 开发的应用当我们运行 npm run dev 的时候入口文件是 examples 目录下的 entry.jsimport Vue from vue;import entry from ./app;import VueRouter from vue-router;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;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);入口文件做的事情很简单全引入的方式注册了 element-ui 组件库注册了一些官网用到的组件注册了路由以及路由的全局钩子函数。这里我们要重点关注路由部分路由的配置都在 examples/route.config.js 中import navConfig from ./nav.config;import langs from ./i18n/route;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);};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);};const registerRoute (navConfig) { let route []; Object.keys(navConfig).forEach((lang, index) { let navs navConfig[lang]; route.push({ path: /${ lang }/component, redirect: /${ lang }/component/installation, component: load(lang, component), 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 { addRoute(nav, lang, index); } }); }); function addRoute(page, lang, index) { 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 }; 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;这个路由配置文件提供了指南、组件、主题、资源等多个路由页面的配置并且支持了多语言我们重点关注一下组件路由是如何生成的它主要通过 registerRoute(navConfig) 方法生成。其中 navConfig 读取的是 examples/nav.config.json 文件这个配置文件太长我就不贴了它包括了多个语言的配置维护了左侧组件导航菜单路径映射关系。registerRoute 函数内部就是遍历 navConfig根据它内部元素的数据结构生成路由配置如果数据中有 children 则生成子路由。我们知道 Vue Router 的本质是根据不同的 URL path 组件映射到对应的路由组件对于每一个组件的路由都是通过 addRoute(nav, lang, index) 方法生成的该方法内部又调用了 loadDocs(lang, page.path) 获取到对应的路由组件。const loadDocs function(lang, path) { return LOAD_DOCS_MAP[lang](path);};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); }};以中文为例我们获取到某个 path 下的路由组件就是一个工厂函数它对应加载的组件路径是 exmaples/docs/zh-CN/${path}.md。这里要注意的是和我们普通的异步组件加载方式不同这里加载的居然是一个 .md 文件而并非一个 .vue 文件但却能和 .vue 文件一样能渲染成一个 Vue 组件这是如何做到的呢我们知道webpack 的理念是一切资源都可以 require只要配置了对应的 loader。回到 build/webpack.demo.js我们发现对于 .md 文件我们配置了相应的 loader { test: /\.md$/, use: [ { loader: vue-loader, options: { compilerOptions: { preserveWhitespace: false } } }, { loader: path.resolve(__dirname, ./md-loader/index.js) } ] }对于 .md 文件这里 use 数组中配置了 2 项它们执行顺序是逆序的也就是先执行 md-loader再执行 vue-loadermd-loader 的代码在 build/md-loader/index.js 中const { stripScript, stripTemplate, genInlineComponentText} require(./util);const md require(./config);module.exports function(source) { const content md.render(source); const startTag ; const endTagLen endTag.length; let componenetsString ; let id 0; // demo 的 id let output []; // 输出的内容 let start 0; // 字符串开始位置 let commentStart content.indexOf(startTag); let commentEnd content.indexOf(endTag, commentStart startTagLen); while (commentStart ! -1 commentEnd ! -1) { output.push(content.slice(start, commentStart)); const commentContent content.slice(commentStart startTagLen, commentEnd); const html stripTemplate(commentContent); const script stripScript(commentContent); let demoComponentContent genInlineComponentText(html, script); const demoComponentName element-demo${id}; output.push(${demoComponentName} /); componenetsString ${JSON.stringify(demoComponentName)}: ${demoComponentContent},; // 重新计算下一次的位置 id; start commentEnd endTagLen; commentStart content.indexOf(startTag, start); commentEnd content.indexOf(endTag, commentStart startTagLen); } // 仅允许在 demo 不存在时才可以在 Markdown 中写 script 标签 // todo: 优化这段逻辑 let pageScript ; if (componenetsString) { pageScript ; } else if (content.indexOf() .length; pageScript content.slice(0, start); } output.push(content.slice(start)); return ${output.join()}${pageScript} ;};webpack loader 的原理很简单输入是文件的原始内容返回的是经过 loader 处理后的内容。对于 md-loader输入的是 .md 文档输出的则是一个 Vue SFC 格式的字符串这样它的输出就可以作为下一个 vue-loader 的输入做处理了。我们来简单看一下 md-loader 中间处理过程。首先执行了 md.render(source) 对 md 文档解析提取文档中 :::demo {content} ::: 内容分别生成一些 Vue 的模板字符串然后再从这个模板字符串中循环查找 包裹的内容从中提取模板字符串到 output 中提取 script 到 componenetsString 中然后构造 pageScript最后返回的内容就是 return ${output.join()}${pageScript} ;最终生成的字符串满足我们通常编写的 .vue SFC 格式它会作为下一个 vue-loader 的输入所以这样我们就相当于通过加载一个 .md 格式的文件的方式加载了 Vue 组件。这里面还有很多和 .md 文件解析的细节如果你对最终生成的 output 和 pageScript 代码是什么感兴趣建议你自己调试一番。element-ui 这种文档和 demo 的实现方式是非常巧妙的大大减少了 demo 和文档的维护成本并且对于用户来说也非常友好如果你也为自己的库构建文档不妨参考它的实现。安装 引入通常 JS 库都会支持 npm 和 CDN 2 种安装方式element-ui 也不例外。先说一下 CDN 的安装方式实际上 element-ui 会把所有组件打包生成一份 CSS 和 JS官方也提供了例子link relstylesheet hrefhttps://unpkg.com/element-ui/lib/theme-chalk/index.cssscript srchttps://unpkg.com/element-ui/lib/index.jsscriptCDN 安装方式有它的好处不需要构建工具开箱即用但缺点也很明显全量引入了所有组件体积非常大。由于大部分人在开发 Vue 项目都是基于 vue-cli 脚手架初始化项目的所以更推荐使用 npm 方式安装。npm i element-ui -S说到 npm 安装不得不提 element-ui 提供的 2 种组件引入方式完整引入和部分引入。支持完整引入非常容易把所有组件打包成一份 CSS 和 JS并且在 package.json 中配置 main: lib/element-ui.common.js这样当用户执行 import ElementUI from element-ui 的时候就可以完整引入了组件的 JS 代码了。正如我们之前说的element-ui 会单独发布 CSS所以你还需要 import element-ui/lib/theme-chalk/index.css。完整引入的好处是方便只需要 2 行代码就可以完整地使用 element-ui 所有的组件但缺点也很明显引入的组件包体积很大通常一个项目也用不到所有的组件会有资源浪费。因此最佳实践就是按需引入import Vue from vueimport { Button } from element-uiVue.component(Button.name, Button)大部分人这么用的时候会觉得理所当然不知道大家有没有想过为什么这种引入方式可以实现按需引入呢要搞清楚这个问题就要搞清楚 import { Button } from element-ui 这个背后都做了什么。其实官网已经有答案了在使用按需引入的时候要借助 babel-plugin-component 这个 webpack 插件并且配置 .babelrc{ presets: [[es2015, { modules: false }]], plugins: [ [ component, { libraryName: element-ui, styleLibraryName: theme-chalk } ] ]}实际上它是把 import { Button } from element-ui 转换成var button require(element-ui/lib/button)require(element-ui/lib/theme-chalk/button.css)这样我们就精准地引入了对应 lib 下的 Button 组件的 JS 和 CSS 代码了也就实现了按需引入 Button 组件。element-ui 这种按需引入的方式虽然方便但背后却要解决几个问题由于我们支持每个组件可以单独引入那么如果产生了组件依赖并且同时按需引入的时候代码冗余问题怎么解决。举个例子在 element-ui 中Table 组件依赖了 CheckBox 组件那么当我同时引入了 Table 组件和 CheckBox 组件的时候会不会产生代码冗余呢import { Table, CheckBox } from element-ui如果你不做任何处理的话答案是会你最终引入的包会有 2 份 CheckBox 的代码。那么 element-ui 是怎么解决这个问题的呢实际上只是部分解决了它的 webpack 配置文件中配置了 externals在 build/config.js 中我们可以看到这些具体的配置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()];externals 可以防止将这些 import 的包打包到 bundle 中并在运行时再去从外部获取这些扩展依赖。我们来看一下打包后的 lib/table.js我们可以看到编译后的 table.js 对 CheckBox 组件的依赖引入module.exports require(element-ui/lib/checkbox);这么处理的话就不会打包生成 2 份 CheckBox JS 部分的代码了但是对于 CSS 部分element-ui 并未处理冗余情况可以看到 lib/theme-chalk/checkbox.css 和 lib/theme-chalk/table.css 中都会有 CheckBox 组件的 CSS 样式。其实要解决按需引入的 JS 和 CSS 的冗余问题并非难事可以用后编译的思想即依赖包提供源码而编译交给应用处理这样不仅不会有组件冗余代码甚至连编译的冗余代码都不会有实际上我们基于 element-ui fork 的组件库 zoom-ui 就应用了后编译技术之前在滴滴搞的开源组件库cube-ui 组件库也是这么玩的。更多后编译相关介绍可以参考滴滴团队在掘金发布的 《webpack 应用编译优化之路》。工程化前端对于工程化的要求越来越高element-ui 作为一个组件库它在工程化方面做了哪些事情呢首先是开发阶段为了保证大家代码风格的一致性使用了 ESLint甚至专门写了 eslint-config-elemefe 作为 ESint 的扩展规则配置为了方便本地开发调试借助了 webpack 并配置了 Hot Reload利用模块化开发的思想把组件依赖的一些公共模块放在了 src 目录并依据功能拆分出 directives、locale、mixins、transitions、utils 等模块。其次是测试方面使用了 karma 测试框架为每一个组件编写了单元测试并且利用 Travis CI 集成了测试。接着是构建方面element-ui 编写了很多 npm scripts以 dist 这个 script 为例 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它内部会依次执行多个命令最终会生成 lib 目录和打包后的文件。我并不打算介绍所有的命令感兴趣同学可自行研究这里我想介绍一下 build:file 这个 script 做的事情build:file: node build/bin/iconInit.js node build/bin/build-entry.js node build/bin/i18n.js node build/bin/version.js,这里会依次执行 build/bin 目录下的一些 Node 脚本对 icon、entry、i18n、version 等做了一系列的初始化工作它们的内容都是根据一些规则做文件的 IO这么做的好处就是完全通过工具的手段自动化生成文件比人工靠谱且效率更高这波操作非常值得我们学习和应用。最后是部署通过 pub 这个 npm script 完成 pub: npm run bootstrap sh build/git-release.sh sh build/release.sh node build/bin/gen-indices.js sh build/deploy-faas.sh主要是通过运行一系列的 bash 脚本实现了代码的提交、合并、版本管理、npm 发布、官网发布等让整个发布流程自动化完成脚本具体内容感兴趣的同学可自行查看。总结至此element-ui 的组件库的整体设计介绍完毕可以看到除了这些丰富的组件背后还有很完整的一套解决方案很多经验都值得我们学习和借鉴不完美的地方也值得我们去思考其中有很多技术细节可以深入挖掘。把不会的东西学会了那么你就进步了如果你觉得这类文章有帮助也欢迎把它推荐给你身边的小伙伴。下一篇预告 Element-UI 技术揭秘(3)— Layout 布局组件的设计与实现。在这里留下你的爪印喔~