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(默认值)输出为一个变量
var MyLibrary = _entry_return_ ;
this 输出为 this 的一个属性
this["MyLibrary"] = _entry_return_ ;
window 输出为 window 对象的一个属性
window["MyLibrary"] = _entry_return_ ;
global 输出为 global 对象的一个属性
global["MyLibrary"] = _entry_return_ ;
commonjs 输出为 exports 的一个属性
exports["MyLibrary"] = _entry_return_ ;
commonjs2 以 module.exports 形式输出
module.exports = _entry_return_ ;
amd 输出为 AMD 模块
umd 暴露给所有模块定义, 允许它和 CommonJS/AMD / 全局变量一起工作
很显然, 我们需要把 output.libraryTarget 的值设为 umd, 以使我们的库可以工作在各种场景下
另一个与库打包有关的设置项是 output.umdNamedDefine, 在 output.libraryTarget 设为 umd, 且 output.library 也设置了的情况下, 把此选项值设为 true, 将会为 AMD 模块命名
- webpackConfig.output = {
- path: path.resolve(__dirname, 'dist'),
- publicPath:"/",
- filename: '[name].js',
- library: 'xxx', // 模块名称
- libraryTarget: 'umd', // 输出格式
- umdNamedDefine: true // 是否把模块名作为 AMD 输出的命名空间
- };
Vue 组件库只提供组件, Vue 文件自身需要组件库使用者在项目中自行引入, 库中无需打包所以我们可以把 Vue 加到 externals 中
- externals: {
- vue: 'vue'
- }
这样 Vue 就不会被打包不过, 有个问题, 就是用 script 标签的形式引用 Vue 的时候, 挂在 window 上的变量名是 Vue, 而不是我们需要的 vue, 因此使用时会报 vue 未定义的错误
还好, webpack 的 externals 配置项支持传入一个对象, 可以为不同导出形式指定不同名称所以下面这种写法可以解决这个问题
组件类型
规划中的 Vue 组件库包含组件 (Component) 指令 (Directive) 和过滤器 (Filter) 三种类型的存在
比较特殊的是模态弹窗类 (Modal) 组件, 如 DialogToast 等等页面中可能存在很多个 Modal, 而很多场景下用户的行为只会触发其中一部分, 如果把所有可能弹出的 Modal(特别是异步的结构内容复杂的 Modal)全部写在页面上, 是否妥当? 对于多页面应用, 每个页面都写一遍或者再封装一层组件是否繁琐而冗余? 这个问题在知乎上引发过讨论, 尤大 (Vue.js 作者尤雨溪) 本人在参与讨论时给出建议, 组件多层嵌套时, 应该把 Modal 放在根组件里, 然后在子组件里通过事件触发在具体应用里, 应该这么用, 这符合 Vue 提倡的状态驱动不过在组件库里, 我们还是希望提供一种更便捷更通用的方式来使用 Modal 类型的组件
参考了 Element UI 等优秀组件库的做法, 我们把 Modal 类型的组件挂到了 Vue.prototype 上, 使之成为 Vue 的实例方法, 一次安装全局调用
this.$dialog(options);
因此, 我们的组件库组件类型还包括实例方法
组件 CSS 作用域
对于一个组件, 我们希望它的 CSS 只作用于当前组件内的元素, 所以我们给每个组件的 Vue 单页面文件的 style 标签加上了 scoped 属性编译后, html 标签会被自动添加一个随机生成的唯一属性 (比如 data-v-f3f3eg9) , 同时对应的 CSS 选择器也会增加同名的属性选择器(如. example[data-v-f3f3eg9]), 这样组件内的 CSS 便指定了作用域
编译后:
通过 scoped 属性的确能达到给组件样式设置作用域的目的, 基本能避免组件内的样式影响外部, 但是它也带来了另外一个问题, 就是给外部覆盖内部样式带来了不便无论组件功能多么通用, 接口多么灵活, 只要涉及到 UI, 就难免无法满足所有项目样式需求, 所以应该允许在具体的项目中根据需要覆盖组件部分甚至全部样式而 scoped 随机生成属性名提高了覆盖样式的难度
经过权衡, 我们在组件里移除了 scoped 属性, 改用 class 策略来避免组件内样式影响外部当然, scoped 属性也不是没有存在的意义, 它更适合在具体应用中使用, 对于复用性高的组件来说, 不是最佳选择
按需使用与自定义构建
随着项目推进, 组件库里的组件越来越多, 目前已超过 40 个, 构建之后的文件也越来越大如果某个应用只用到了库里的少数几个组件, 完全没有必要使用完整的构建包, 所以我们需要提供一种按需使用的方式早期, 我们是让用户通过私有 npm 安装组件库之后, 根据应用自身需要直接引用 src 目录下组件源码的方式来实现按需加载这种方式有较大局限性, 因为引用的源码没有经过编译, 需要用户自己去处理组件的依赖关系, ES6/SCSS/Vue 模板等编译工作也需要用户在自己的项目里完成, 繁琐易出错, 也难以支持 webpack 外的其他场景 我们设想提供一种自定义构建的方式, 来实现按需打包首先让用户选择需要哪些组件, 然后基于这些信息生成一个个性化的配置文件, 再基于这个文件进行构建, 最终只打包编译用户指定的这些组件
那么, 通过哪种方式与用户交互, 收集用户指令呢? 比较友好的方式是通过 web, 比如在项目主页中提供一个页面, 让用户在线选择组件, 然后下载构建之后的文件而根据我们组件库目前的定位, 推荐的使用方式是通过私有 npm 安装, 所以我们首先推出的是通过命令行界面 (CLI) 方式来完成自定义构建
用户只需要在终端执行命令 npm run custom, 即可得到全部组件的列表, 通过键盘选择需要的组件, 然后按下回车, 脚手架便开始自动完成剩余的个性化构建工作
片刻之后, 只包含用户所选组件的构建包会出现在 dist 目录下, 文件体积比完整版本小很多
这种方式下, 所选组件会经历组件库脚手架完整的构建流程, 自动处理组件依赖关系, 对 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 颜色和透明度等样式, 甚至可以实现颜色渐变效果
我们并没有选择 ICON FONT 方案, 我们认为 SVG 方案更适合移动端组件库:
SVG 虽在 PC 端个别古董浏览器中兼容较差, 但在移动端兼容良好
ICON FONT 被认为是文本, 所以一些浏览器会对其进行抗锯齿处理, 这可能导致图标不那么锐利, 清晰度打折扣
SVG 样式控制比 ICON FONT 更灵活, 甚至可以控制图标各个部分的颜色, 实现彩色图标而这对 ICON FONT 来说是不可能实现的
ICON FONT 通常是用伪对象或伪类插入页面, 其展示受到 line-heightvertical-alignletter-spacingword-spacing 及字体相关 CSS 属性影响, 也受到字体字符设计本身影响而 SVG 在页面中就是一个标签, 更方便控制, 语义化也更好
结合 symbol 元素可以实现所谓 SVG Sprite, 也就是把很多 SVG 图标整合在一起, 通过 ID 引用指定图标, 可以复用这种方式比 CSS Sprite 还要方便, 因为不需要关心图标具体位置信息
SVG Sprite 也不是必须手动去组合, 借助 webpack 的 svg-sprite-loader 可以轻松实现 SVG Sprite 的动态生成, 图标的按需加载不是梦
- webpack output.libraryTarget webpack.js.org/configurati
- umd github.com/umdjs/umd
来源: https://juejin.im/entry/5abe0621518825555e5df56d