cube-ui 是滴滴去年底开源的一款基于 vue.js 2.0 的移动端组件库, 主要核心目标是做到体验极致, 灵活性强, 易扩展以及提供良好的周边生态 - 后编译.
自 17 年 11 月开源至今已有 5 个月, 在这个过程中 cube-ui 受到了不少的关注, 同时从社区中也收到了很多很好的反馈和建议. 我们也一直在迭代更新, 从最初的 1.0 版本到最近发布的 1.7 的版本, 除了对原有组件做一些增强优化, 我们也提供了很多新的组件. 此外, 周边后编译技术生态也做了很多优化, 满足于更多场景需求, 官网也做了一次升级. 接下来就重点介绍下 cube-ui 在这个过程中的有哪些成果以及一些设计细节.
关键成果
cube-ui 的组件数已经从最初的 14 个增长了 28 个, 足足翻了一倍, 已有的组件生态:
除了上述的组件外, cube-ui 还对外暴露了三个模块: ● style ● create-API ● better-scroll
而且 cube-ui 也已经支持了如下特性: ● 自定义主题 ● rem 布局 ● SSR 支持
此外, cube-ui 的周边生态也有了进一步丰富: ● vue-cli 脚手架模板 cube-template ● 快速上手教程 cube-application-guide ● 后编译 webpack 插件 webpack-post-compile-plugin ● 按需引用 webpack 插件 webpack-transform-modules-plugin, 依赖 babel 插件 babel-plugin-transform-modules
设计细节
针对于上边所介绍的关键成果, 我们来聊聊具体设计上的细节.
组件模块
● 滚动 & Picker 类组件 在移动端, 由于手机尺寸以及交互特性, 我们需要处理很多滚动类需求: 下拉刷新, 上拉更多, 轮播等以及 Picker 选择等场景. cube-ui 底层滚动类组件以及 Picker 类依赖于我们团队的移动端利器 better-scroll 实现, 基于其出色的体验进而保证了我们上层封装的滚动类, Picker 类组件的出色的交互体验. ● 弹出层类组件 在实际开发中我们会遇到很多弹出层类组件, 因为我们设计了一个基础弹出层组件 Popup, 它主要解决移动端最为常见的居中(Tip: 文本换行位置也很重要哦), 置底以及是否有蒙层效果, 借助于它来实现绝大多数的弹出层组件. 另一个常见的痛点就是由于弹出类组件往往是全屏的状态, 如果我们按照 Vue 推荐的声明式的语法在子组件里使用弹出层组件, 由于嵌套层级问题, 很容易受到父级元素的样式影响. 为此我们单独开发了 create-API 模块, 通过 API 的形式将实例化的弹出层组件动态挂载到 Body 元素下, 因此摆脱了父级元素样式的影响, 同时会随着使用它的组件的销毁而自动销毁, 且为了降低开销成本, 根据需要有些弹出层类组件都被设计成了单例模式. 它是一个很通用的能力, 我们把这样的一个便捷的 API 对外暴露出去, 开发者也可以根据实际场景将自己开发的组件通过 createAPI 进行注册, 进而也可解决上述痛点. ● 表单类组件 表单类需求往往特应性比较大, 交互也很难做到统一, 但是仍然可以有主流的表单设计交互, 在 cube-ui 中表单可以设置 layout 来决定样式甚至是交互, 满足日常场景需求. 在表单设计中有两个很重要的组件: Validator 和 Form.Validator 成为独立的组件主要基于校验场景不确定性, 同时还需要满足各种形式的校验, 所以 Validator 就只做了两件核心的事情, 对数据源进行校验以及对应的错误信息的展示. 考虑到开发者开发表单的便利性, 我们参考 vue-form-generator 的设计, 把表单设计成了根据 Schema 配置自动生成表单, 这样开发表单的成本就降低了很多; 同时为了兼顾灵活性, 也支持通过插槽来自定义开发者需要的结构交互. 后编译
后编译是 cube-ui 的一个重要的生态, 借助于后编译, 整个的 Web 应用的开发都可以直接基于 ES2015+ 进行开发, 而项目依赖的一些 NPM 包也是可以直接使用 ES2015+ 进行开发, 并且无需编译可直接发布到 NPM 平台上(也可以是自己 NPM 私服). 这样, 这些组件库或者工具就可以有更多的想象空间, 可以做更多有意思的事情. cube-ui 支持的两个特性自定义主题以及 rem 布局都是基于我们主推的后编译技术实现. 接下来一起来看下这两个特性实现的细节. 自定义主题 一般而言, 组件库都是有默认主题的, 而往往还会搭配有多套主题(PC 类组件库比较常见). 现在借助于 CSS 预处理器, 我们可以给组件定义一些变量(一般都是颜色值), 然后在组件对应的样式中使用. 对于自定义主题这种需求, 主流的做法有: 样式覆盖和修改变量.
样式覆盖 样式覆盖是最古老的做法, 但是缺点也很明显, 第一就是样式冗余问题, 默认主题样式是一直存在的; 第二就是开发者需要确切的知道样式对应的优先级去覆盖, 要么是同级的优先级样式后置, 要么就是提升自身覆盖的样式优先级. 当然, 样式覆盖的做法也是有优点的, 那就是对于多主题同时存在, 自由切换场景会比较合适.
修改变量 现在有很多的 CSS 预处理器可以选择, 每一种 CSS 预处理器都提供了变量功能, 借助于变量, 我们可以很容易创建一个主题文件, 里边包含组件依赖的变量定义. 要实现自定义主题, 开发者需要在自己项目下创建一个单独的样式文件, 定义赋值变量, 同时引入组件库自身源码下的主题文件. 本质上也是一种后编译做法, 这个编译是利用 CSS 预处理器自身的变量能力达到目的. 对于 Vue 组件库而言, 主流的也是推荐的做法是把样式写在 .vue 文件中, 这样便于维护, 比较符合组件化开发思维; 但是为了方便的使用预处理器实现自定义主题, 通常都会把样式单独拿出来, 一般的做法是创建一个样式文件夹, 里边包含所有组件样式, 而在 .vue 文件中则是没有样式的.
cube-ui 做法 核心点就是借助于后编译, 我们可以按照原有我们习惯的方式去书写组件, 即在 .vue 文件中包含模板, 脚本和样式. 如果需要自定义主题, 就在自己项目下创建一个主题文件, 里边定义变量, 这个做法和一般的修改变量做法一样, 但是不需要引入所有样式入口文件, 因为也不存在这样的一个文件; 同时借助于 webpack, 我们完全可以做到在不侵入源码的情况下, 做到主题定制. 接下来就看下具体做法, 如果是新创建的项目, 那么推荐使用 Vue-cli + cube-template 模板生成; 而如果是现成的项目, 则具体参考官方文档 - 主题 中配置. 主要有两个核心点: ● 创建主题文件 theme.styl, 一般放在 src/ 目录下 ● 修改 webpack 中关于 stylus-loader 的配置项: 添加 import 字段用于依赖自定义主题文件 接下来就看一个简单项目演示, 假设创建了一个 demo 的项目, 这个项目默认跑起来是这样的:
如果我们想要把项目中使用的按钮的背景色该换掉, 那么可以修改 theme.styl 的文件内容: // 如果你需要使用 cube-ui 自带的颜色值 需要 require 进来
- @require "~cube-ui/src/common/stylus/var/color.styl"
- // button
- $btn-bgc := #409eff
- $btn-bdc := #409eff
- $btn-active-bgc := #66b1ff
- $btn-active-bdc := #66b1ff
配合我们的 webpack 配置, 刷新后的样子为:
这样我们就可以轻松做自己想要的主题定制, 所要做的就是修改 cube-ui 已经定义好的变量值即可. 对于 cube-ui 组件库自身, 则不会有任何修改, 且对于应用开发者而言, 用不用自定义主题, 本身的源代码不用修改, 只需要创建一个主题文件 (无需手工引入) 配合 webpack 插件配置即可.
其实对于主题定制, 还可以更进一步, 未来 cube-ui 会考虑借助于 CSS 自身支持的变量 (自定义属性) 达到主题定制的目的, 例如可以把处理器变量改为原生的变量, 编译的话可以通过 post-CSS-variables 插件把默认变量值做替换, 可以实现和现有编译后功能相同的效果, 同时在后编译的情况下不失原生 CSS 变量的动态优势. 这样, 不仅可以做到主题定制, 也可以做到多主题的自由切换, 因为 CSS 原生变量可以直接修改变量值而不需要通过事先写死然后切换 class 覆盖的方式做多主题切换.
rem 布局
在移动端还是有很多设计师, 产品或者开发者偏爱用缩放来达到不同尺寸屏幕适配目的, 而缩放的实现一般都是采用 rem 进行布局, 业内比较出名的方案就是手机淘宝前端团队开源的 lib-flexible. 现在其实是不推荐使用 rem 进行布局的, 如果真的要缩放的效果, 可以考虑 vw vh 等 CSS 单位来实现. rem 布局有两个核心的点:
在运行时动态根据视口宽度更新 rem 的值, 即修改根元素 html 的 font-size 的值
在编译时 (或开发时) 需将设计稿的 px 单位转换为 rem 单位 对于组件库而言, 如果想要同时做到即支持普适的 px 又支持 rem 这种方式的话, 社区貌似还没见到. 和后编译搭配, 则比较容易实现, 在 cube-ui 中, 已经提供了 rem 支持, 主要采取的方案:
可选的 amfe-flexible, 也就是 lib-flexible 动态计算更新 rem 的值(注 2.x 版本)
选择了 postcss 的插件 postcss-px2rem 作为将 px 转换为 rem 的库 这其实是对组件库本身有了一定要求, 和尺寸相关的尽量要用样式控制, 这样才能通过处理工具 postcss-px2rem 将 px 单位处理成 rem 单位, 进而实现动态缩放需求. 来看下 cube-ui 使用 rem 的效果, 默认 iPhone 5 尺寸效果:
当尺寸变大, 例如为 iPhone 6 Plus 尺寸时效果:
可以看出整体的效果, 当尺寸较小时, Button 和 Toast 都是比较小的, 而当尺寸比较大时, 相对应的都会更大, 达到了缩放的目的.
上层扩展
这里上层扩展主要是指基于组件库进行二次封装, 例如在滴滴内部, 我们的很多业务组件库就是在开源的 cube-ui 组件库之上做增强而来的. 这个能力是非常重要的, 因为移动端组件库和 PC 组件库最大的区别是移动端多是 to C 的业务场景, 不同的业务场景下的设计是不一样的, 所以 cube-ui 专注于通用组件和基础能力的建设, 并不会在布局和业务组件方面大做文章; 而 PC 组件库一般都是用于 to B 的场景, 如内部 MIS 类的系统, 对于设计的要求并没有特别苛刻, 所以基础的样式, 组件都是可以统一的. 因此 cube-ui 的定位并不是要提供一个 "大而全" 的组件库, 而是提供了二次扩展的能力, 目标是任何移动端的业务场景都可以基于 cube-ui 提供的能力做二次扩展. 以我们的快速上手教程为例, 我们要开发如下图的弹窗组件.
我们基于 cube-ui 提供的能力开发它就非常方便了. 首先可以基于 Popup 组件开发一个 subscribe-dialog.vue 组件:
- <template>
- <div class="subscribe-dialog-view">
- <cube-popup ref="popup" @mask-click="hide">
- <div class="subscribe-dialog-wrapper">
- <span class="close" @click="hide"><i class="cubeic-close"></i></span>
- <div class="title">开启推送通知</div>
- <img src="./subscribe.png">
- <p class="desc">第一时间获取最新鲜出炉的新闻攻略, 赛事咨询, 数据专题, 精彩视频</p>
- <cube-button class="button" @click="start">现在开启</cube-button>
- </div>
- </cube-popup>
- </div>
- </template>
- <script>
- export default {
- name: 'subscribe-dialog',
- methods: {
- show () {
- this.$refs.popup.show()
- this.$emit('show')
- },
- hide () {
- this.$refs.popup.hide()
- this.$emit('hide')
- },
- // ...
- }
- }
- </script>
接着使用 createAPI 模块把它变成一个 API 式的组件:
- import SubscribeDialog from './components/subscribe-dialog/subscribe-dialog'
- createAPI(Vue, SubscribeDialog, [], true)
然后调用它就非常方便了:
- this.subscribeDialog = this.$createSubscribeDialog()
- this.subscribeDialog.show()
周边生态 周边生态有两个核心: 后编译 + 按需引入. 为此, 我们开发了两个 webpack 的插件来帮助我们更好的去使用, 开发. ● 后编译 webpack 插件 webpack-post-compile-plugin ● 按需引用 webpack 插件 webpack-transform-modules-plugin webpack-post-compile-plugin 这个插件主要是读取应用 package.JSON 中的 compileDependencies 字段的值(用于指定应用需要后编译哪些依赖包), 而且还能解决嵌套后编译包的问题, 因为开发者只需要关注自己依赖需要后编译的包, 而不需要关注依赖的依赖包, 这样就能构成一条生态链.
为什么不是一个 NPM 包自己声明需不需要后编译, 而是由使用者去声明? 主要考虑整个 NPM 生态, 例如 lodash-es 并不在我们控制范围之内, 为了更好的使用整个 NPM 生态圈的包, 我们决定由使用者去声明需要后编译的 NPM 包. webpack-transform-modules-plugin 这个插件主要解决更方便, 友好地使用按需引入的问题, 为了更好的统一应用使用后编译和不使用的情况, 我们在原本 babel-transform-imports 的基础上做了升级优化产出了 babel-plugin-transform-modules 插件, 但是和后编译的场景类似, 这个是不能解决后编译场景下 NPM 包嵌套按需引入的问题的, 为此才开发了 webpack-transform-modules-plugin 这个插件, 和 compileDependencies 字段类似, 我们新增了 transformModules 字段来声明按需引入的 NPM 包的的转换规则, 例如:
- "transformModules": {
- "cube-ui": {
- "transform": "cube-ui/src/modules/${member}",
- "kebabCase": true
- }
- }
当然在后编译的场景下, 我们借助于 webpack 4 Tree shaking 中新增的 side-effects 也可以达到目的, 这个是未来我们去优化的方向.
脚手架 & 教程 任何的技术都是有成本的, 我们新增了 webpack 插件, 也有一些需要配合的改动, 所以为了降低开发者成本, 我们提供了适用于 vue-cli 脚手架的模板 cube-template, 当然对应的也会新增一些配置项, 感兴趣的可以了解下 cube-template wiki. 同时为了初次使用 cube-ui 的开发者快速上手, 我们还有一个简单的上手教程 cube-application-guide.
展望 cube-ui 目前还处于初步的阶段, 还缺少很多组件, 但是我们一直在努力, 希望在很快的未来可以提供更多更好用的组件. 不仅如此, 我们希望的是除去组件库本身, 额外还会丰富周边的整个生态建设, 给开发者一个良好的生态环境, 进一步提升开发体验, 提升应用性能等. 当然, 我们也希望社区的小伙伴也能参与进来, 一块共同建设, 共同进步. 未来 cube-ui 会朝着如下方面继续前行: ● 丰富组件 ● 组件优化 ● 文档优化 ● 示例优化 ● 周边建设 希望感兴趣的同学可以一起共建或者加入我们团队, 一起玩技术!
来源: https://juejin.im/post/5c0fdb14f265da613a53df1d