漫谈Vue组件库开发

2017年是Vue.js大爆发的一年,React迎来了一个强有力的竞争对手,王者地位受到挑战(撰写此文时github上Vue与React的star数量已逼近)。我们团队这一年有十多个大型项目采用了Vue技术栈,在开发效率、页面性能、可维护性等方面都有不错的收效。 我们希望把这些项目中可复用的功能组件提取出来,给后续项目使用,以减少重复开发,提高效率,同时也为了致敬前端界“出一个框架,造一遍*”的行规, 一个基于Vue 2的移动端UI组件库被提上日程。

组件库的开发过程总的来说还是比较顺利的,这里与大家分享一些问题与思考。


脚手架选择

尽管我们团队的这些Vue技术栈项目的脚手架大都使用的是webpack,在为组件库选择脚手架的时候我们还是在webpack与Rollup中犹豫了一下。


Rollup看起来更适合组件库的开发,它把所有模块构建在一个函数内,执行效率更高,它支持Tree Shaking,只打包需要的代码,输出文件更小(webpack后来也支持了)。但综合考虑之后,我们还是选择了webpack作为打包工具。首先,按照规划,demo演示和文档页面也在这个脚手架中,所以对代码分割、热加载等功能是有需求的,而这方面能力Rollup远不及webpack。另外,这个组件库由多人开发维护,基于现有webpack脚手架开发成本更低、效率更高。选择webpack,让我们可以更专注于造*。


打包

即便选择了webpack作为打包工具,我们也并不希望这个库的使用场景局限在webpack项目中,通过AMD/CMD方式、甚至通过script标签直接引用等场景都应该得到支持。为了达到这个目的,我们需要在webpack配置文件中设置输出格式,需要配置的选项是output.libraryTarget,有以下可选值:


  • "var"(默认值)输出为一个变量


  1. var MyLibrary = _entry_return_;


  • "this" 输出为this的一个属性


  1. this["MyLibrary"] = _entry_return_;


  • "window" 输出为window对象的一个属性


  1. window["MyLibrary"] = _entry_return_;


  • "global" 输出为global对象的一个属性


  1. global["MyLibrary"] = _entry_return_;


  • "commonjs" 输出为exports 的一个属性


  1. exports["MyLibrary"] = _entry_return_;


  • "commonjs2" 以module.exports形式输出


  1. module.exports = _entry_return_;


  • "amd" 输出为AMD模块


  • "umd" 暴露给所有模块定义,允许它和CommonJS/AMD/全局变量一起工作


很显然,我们需要把output.libraryTarget的值设为“umd”,以使我们的库可以工作在各种场景下。


另一个与库打包有关的设置项是output.umdNamedDefine,在output.libraryTarget 设为umd,且output.library 也设置了的情况下,把此选项值设为true,将会为AMD模块命名。


  1. webpackConfig.output = {

  2.    path: path.resolve(__dirname, 'dist'),

  3.    publicPath:"/",

  4.    filename: '[name].js',

  5.    library: 'xxx', //模块名称

  6.    libraryTarget: 'umd', //输出格式

  7.    umdNamedDefine: true //是否把模块名作为AMD输出的命名空间

  8. };


Vue组件库只提供组件,Vue文件自身需要组件库使用者在项目中自行引入,库中无需打包。所以我们可以把Vue加到externals中。


  1. externals: {

  2.    vue: 'vue'

  3. }


这样Vue就不会被打包。不过,有个问题,就是用script标签的形式引用Vue的时候,挂在window上的变量名是“Vue”,而不是我们需要的"vue",因此使用时会报vue未定义的错误。

漫谈Vue组件库开发


还好,webpack的externals配置项支持传入一个对象,可以为不同导出形式指定不同名称。所以下面这种写法可以解决这个问题。

漫谈Vue组件库开发


组件类型

规划中的Vue组件库包含组件(Component)、指令(Directive)和过滤器(Filter)三种类型的存在。


比较特殊的是模态弹窗类(Modal)组件,如Dialog、Toast等等。页面中可能存在很多个Modal,而很多场景下用户的行为只会触发其中一部分,如果把所有可能弹出的Modal(特别是异步的、结构内容复杂的Modal)全部写在页面上,是否妥当?对于多页面应用,每个页面都写一遍或者再封装一层组件是否繁琐而冗余?这个问题在知乎上引发过讨论,尤大(Vue.js作者尤雨溪)本人在参与讨论时给出建议,组件多层嵌套时,应该把Modal放在根组件里,然后在子组件里通过事件触发。在具体应用里,应该这么用,这符合Vue提倡的“状态驱动”。不过在组件库里,我们还是希望提供一种更便捷更通用的方式来使用Modal类型的组件。


参考了Element UI等优秀组件库的做法,我们把Modal类型的组件挂到了Vue.prototype上,使之成为Vue的实例方法,一次安装、全局调用。


  1. this.$dialog(options);


因此,我们的组件库组件类型还包括“实例方法”。


组件CSS作用域

对于一个组件,我们希望它的CSS只作用于当前组件内的元素,所以我们给每个组件的Vue单页面文件的style标签加上了scoped属性。编译后,HTML标签会被自动添加一个随机生成的唯一属性 (比如 data-v-f3f3eg9) ,同时对应的CSS选择器也会增加同名的属性选择器(如.example[data-v-f3f3eg9]),这样组件内的 CSS 便指定了作用域。


  1. <style scoped>

  2. .example {

  3.  color: red;

  4. }

  5. </style>


  6. <template>

  7.  <div class="example">hi</div>

  8. </template>


编译后:


  1. <style>

  2. .example[data-v-f3f3eg9] {

  3.  color: red;

  4. }

  5. </style>


  6. <template>

  7.  <div class="example" data-v-f3f3eg9>hi</div>

  8. </template>


通过scoped属性的确能达到给组件样式设置作用域的目的,基本能避免组件内的样式影响外部,但是它也带来了另外一个问题,就是给外部覆盖内部样式带来了不便。无论组件功能多么通用,接口多么灵活,只要涉及到UI,就难免无法满足所有项目样式需求,所以应该允许在具体的项目中根据需要覆盖组件部分甚至全部样式。而scoped随机生成属性名提高了覆盖样式的难度。


经过权衡,我们在组件里移除了scoped属性,改用class策略来避免组件内样式影响外部。当然,scoped属性也不是没有存在的意义,它更适合在具体应用中使用,对于复用性高的组件来说,不是最佳选择。


按需使用与自定义构建

随着项目推进,组件库里的组件越来越多,目前已超过40个,构建之后的文件也越来越大。如果某个应用只用到了库里的少数几个组件,完全没有必要使用完整的构建包,所以我们需要提供一种按需使用的方式。早期,我们是让用户通过私有npm安装组件库之后,根据应用自身需要直接引用src目录下组件源码的方式来实现按需加载。这种方式有较大局限性,因为引用的源码没有经过编译,需要用户自己去处理组件的依赖关系,ES6/SCSS/Vue模板等编译工作也需要用户在自己的项目里完成,繁琐、易出错,也难以支持webpack外的其他场景。 我们设想提供一种自定义构建的方式,来实现按需打包。首先让用户选择需要哪些组件,然后基于这些信息生成一个个性化的配置文件,再基于这个文件进行构建,最终只打包编译用户指定的这些组件。


那么,通过哪种方式与用户交互,收集用户指令呢?比较友好的方式是通过web,比如在项目主页中提供一个页面,让用户在线选择组件,然后下载构建之后的文件。而根据我们组件库目前的定位,推荐的使用方式是通过私有npm安装,所以我们首先推出的是通过命令行界面(CLI)方式来完成自定义构建。

用户只需要在终端执行命令“npm run custom”,即可得到全部组件的列表,通过键盘选择需要的组件,然后按下回车,脚手架便开始自动完成剩余的个性化构建工作。

漫谈Vue组件库开发


片刻之后,只包含用户所选组件的构建包会出现在dist目录下,文件体积比完整版本小很多。

漫谈Vue组件库开发


这种方式下,所选组件会经历组件库脚手架完整的构建流程,自动处理组件依赖关系,对ES6/SCSS/Vue等语法也进行了编译,构建出的文件也支持AMD/CMD/script标签直接引用等场景,能比较好的满足按需使用的需求。


图标

组件库UI组件难免会包含一些小图标,需要寻找一种合适的方式处理这些图标。


在应用开发中有时会把一些图片转成Base64编码放在代码里,这会使数据量增大30%左右,所以这种方式不适合较大图片。而对于小图标来说,增加的绝对数据量并不大,却能减少一个http请求,也不失为一种优化方案。不过,组件库较普通应用对数据量更为敏感,这种方式不是上策。


另一种处理小图标的经典方案是雪碧图(CSS Sprite),但这种基于精准位置信息的图标引用方式在移动端基于rem的布局中并不是那么受欢迎,因为rem布局自身就难以精确,如果用于组件库也会给按需引用带来一些不便。


对于小图标,在移动端更需要的是矢量方案,天然适配各种像素密度的屏幕。

在组件库中,比较流行的是采用基于CSS3字体(@font-face)的ICON FONT方案,也就是把图标放在一个自定义字体文件中。有很多优点,比如:

  • ICON在字体中是矢量存在,受移动端欢迎

  • 良好的浏览器兼容性,web字体并非CSS3发明,更早之前的浏览器(包括IE6)也都事实上支持,虽有些许差异,终归是有办法兼容的

  • 可通过CSS控制ICON颜色和透明度等样式,甚至可以实现颜色渐变效果


漫谈Vue组件库开发


我们并没有选择ICON FONT方案,我们认为SVG方案更适合移动端组件库:

  • SVG虽在PC端个别古董浏览器中兼容较差,但在移动端兼容良好

  • ICON FONT被认为是文本,所以一些浏览器会对其进行抗锯齿处理,这可能导致图标不那么锐利,清晰度打折扣

  • SVG样式控制比ICON FONT更灵活,甚至可以控制图标各个部分的颜色,实现彩色图标。而这对ICON FONT来说是不可能实现的

  • ICON FONT通常是用伪对象或伪类插入页面,其展示受到“line-height”、“vertical-align”、“letter-spacing”、“word-spacing”及字体相关CSS属性影响,也受到字体字符设计本身影响。而SVG在页面中就是一个标签,更方便控制,语义化也更好

  • 结合symbol元素可以实现所谓“SVG Sprite”,也就是把很多SVG图标整合在一起,通过ID引用指定图标,可以复用。这种方式比CSS Sprite还要方便,因为不需要关心图标具体位置信息


  1. <svg>

  2.    <symbol id="icon1">

  3.        ...

  4.    </symbol>

  5.    <symbol id="icon2">

  6.        ...

  7.    </symbol>

  8.    <symbol id="icon3">

  9.        ...

  10.    </symbol>

  11. </svg>


  12. <svg>

  13.    <use xlink:href="#icon1"></use>

  14. </svg>

  15. <svg>

  16.    <use xlink:href="#icon2"></use>

  17. </svg>

  18. <svg>

  19.    <use xlink:href="#icon3"></use>

  20. </svg>


SVG Sprite也不是必须手动去组合,借助webpack的 svg-sprite-loader 可以轻松实现SVG Sprite的动态生成,图标的按需加载不是梦。


好了,限于篇幅,这次先聊这么多,更多内容请关注我们团队的公众账号“全栈探索”。


扩展阅读

  • NutUI组件库 - https://nutui.jd.com

  • webpack output.libraryTarget - https://webpack.js.org/configuration/output/#output-librarytarget

  • umd - https://github.com/umdjs/umd


========前端直聘===========

京东Y事业部/大数据平台部/创新研究部 急需资深H5前端若干名,工作地点:北辰世纪中心,方向:照片美化工具产品,有兴趣的发送简历到[email protected],欢迎推荐。


========公众号推荐=========

漫谈Vue组件库开发