blog
blog copied to clipboard
Framework7 Vue 踩坑记录
三年前我刚入职的时候接手了一个移动端的项目,当时代码已经很难维护了,构建工具用的是 Browserify,不是当时正火热的 Webpack,而且大部分依赖的版本也很老旧了,所以接手这个项目之后,我做的第一件事就是重写了这个项目。
那时,Framework7 还是 v1 版本,还没有对 Vue 做支持,所以我写了一个 Vue 组件版本的 Framework7(见 lmk123/vue-framework7);另外,当时在使用 Vuejs 官方的 Webpack 模版时遇到不少问题,提了 issue 给官方但迟迟没有修复,所以我又自己整理了一套 Webpack + Vue 的项目模版(见 lmk123/webpack-boilerplate)。
这两个项目一直用到了现在,但在这三年的时间里,Webpack 已经更新到 v4 了,Vue CLI v3 提供了能通过 npm 更新的项目模版,Framework7 也从 v1 更新到了 v3,并官方支持了 Vue(见 Framework7 Vue),与此同时,维护 webpack-boilerplate 与 vu-framework7 的成本越来越高,所以趁着临近春节比较得空,我决定给这个项目做一次升级。
这次升级大概用了 7 个工作日,升级的步骤是:
- 用 Vue CLI 替换 webpack-boilerplate
- 升级 Framework7 到 v3,并用 Framework7 Vue 替换 vue-framework7
- 升级项目的依赖到最新版本
升级过程中踩到了不少坑,于是我决定写篇文章记录一下,接下来我会按照顺序依次写下踩到的坑和解决办法。
Babel 的 loose 模式导致的错误
项目在替换成 Vue CLI 之后,运行的时候控制台报了个错,最后发现跟 Babel 的 loose 模式有关。
举个例子,代码 a.push(...b) 中,当 b 是 undefined 的时候,按照 ES6 的规范,这里是应该报错的。默认情况下,Babel 会把这段代码转换成:
// 默认情况下
a.push.apply(a, babelRuntimeHelpers.toConsumableArray(b))
toConsumableArray() 方法会确保当 b 是 undefined 的时候抛个错出来,但是如果开启了 loose 模式,代码会转换成:
// loose 模式下
a.push.apply(a, b)
这导致 undefined 可能会被 push 到数组中,产生不可预测的 bug。
升级之前,为了减小代码体积,我给 Babel 开启了 loose 模式,升级之后,Vue CLI 默认没有开启,所以这个问题暴露出来了。现在看来,开启 loose 模式是有问题的,所以我建议慎重启用。
Framework7 的源码里用到了 ES6 的幂运算符(**)导致不兼容低版本的设备
项目上线之后,立刻就有一个同事反馈打开项目白屏,且这个同事的 iOS 版本很低,查了下线上的代码,发现代码里出现了幂运算符,但我完全不记得自己在项目里用过,看了下上下文,才发现是 Framework7 里用到了,而 node_modules 目录下的代码默认是不经过 Babel 的。
这个问题也很好解决,让 Framework7 经过 Babel 处理就可以了,在 Vue CLI 3 中,需要在 vue.config.js 添加下面的设置:
module.exports = {
transpileDependencies: ['framework7']
}
Framework7 Vue 不支持 vue-loader 的 Hot Reload
这大概是目前为止最棘手且没有解决办法的问题了。每次更改代码之后,hot reload 都会失败,并且会在浏览器的控制台抛一个错误,而且 Vue CLI v3 还没有提供配置项关闭 hot reload 改为自动刷新,我也尝试过直接改 Webpack 的配置,但是 Vue CLI 对 devServer 配置做了特别处理,改了不生效,最后只能作罢。
所以,目前我只能手动刷新浏览器,期待有人能提供更好的办法。
应该优先使用 Framework7 Vue 组件的 text 属性
在使用组件时,我习惯把内容放在标签内,例如:
<f7-link>文字内容</f7-link>
一开始我很好奇,明明可以直接把文字写在标签内,为什么 f7-link 还要提供 text 属性,后来我发现,如果我们用了 icon,这个组件会根据 text 属性来判断这个链接是否有文字,以此来决定要不要给最终生成的 <a> 标签加上 .icon-only 的 CSS 类。
举例来说,下面的代码:
<f7-link icon="my-icon">文字内容</f7-link>
<f7-link icon="my-icon" text="文字内容"></f7-link>
会被渲染成:
<a class="icon-only my-icon">文字内容</a>
<a class="my-icon">文字内容</a>
如果有文字内容的链接加上了 .icon-only 这个类,样式上就会有问题——文本和图标会重叠。
有同样情况的还有 f7-button。除此之外,大部分组件都提供了 text 或 title 这种可以控制组件文本内容的属性,我的建议是为了保险起见,优先使用属性。
Framework7 Vue 的表单组件不提供 v-model
举个例子,f7-input 的 checked 属性只是定义了 input 元素初始的勾选状态,如果用户点击了 input,这个 checked 属性完全不会变化,而这个组件又不提供 v-model,作者对此的回复是需要我们自行实现 v-model 机制,所以升级之后项目里有很多这样的代码:
<f7-searchbar :value="search" @input="search = $event.target.value"></f7-searchbar>
<f7-list-item radio :checked="checked" @change="checked = $event.target.checked"></f7-list-item>
不够优雅,但是也没有办法。
f7-searchbar 的位置
当 f7-searchbar 是 f7-page 的直接子节点时,它的 DOM 会自动跑到 .page-content 下面去:
<f7-page>
<f7-searchbar></f7-searchbar>
这行文本会出现在 .page-content 下
</f7-page>
会渲染为
<div class="page">
<div class="page-content">
<div class="searchbar">
...
</div>
这行文本会出现在 .page-content 下
</div>
</div>
为了让它保持在 .page 下,需要用一个 div 包裹它:
<f7-page>
<div>
<f7-searchbar></f7-searchbar>
</div>
这行文本会出现在 .page-content 下
</f7-page>
Framework7 的 Router 与 vue-router
剩下的问题全都是跟 Router 相关的,我先吐槽一句:Framework7 的 Router 很强大,但这也导致它很难用。
用习惯了 vue-router 之后,在 Framework7 的 router 里肯定会撞好几次墙。vue-router 的页面生命周期简单明了,keep alive 用起来也很方便,刚开始用 Framework7 的 Router 的时候,你会发现大部分 API 都是一样的,等碰了几次壁之后,就会发现它们之间有很多差别。vue-router 相信大家都挺熟了,接下来我简单介绍一下 F7 的生命周期。
F7 的生命周期
在 Vue 中,有两种生命周期周期:
- 组件的生命周期,例如
created、mounted等 - router 的生命周期,例如
beforeRouteEnter、beforeRouteLeave
但 F7 中的「页面」是特殊的组件,它的顶级根元素只能是 f7-page,且生命周期有三种:
- 类似于 Vue 组件的
created、mounted等 - Router 生命周期,有点不同于 vue-router 的是,vue-router 的
beforeRouteEnter、beforeRouteLeave既可以直接写在组件上,也可以写在路由配置里,但 F7 的beforeRoute、afterRoute只能写在 Router 配置里 - f7-page 独家提供了
page:init、page:beforein等事件
要理清这么多事件不简单,但它设计的这么复杂也是有原因的,因为默认情况下,页面之间的切换是有滑动效果的,所以它分别设计了三套事件,全面满足用户的各种需求。
文档上分别介绍了这三套事件,但它们组合起来就是另一回事了。我通过观察,大致了解了它们的触发顺序与时机,这里我简单介绍一下。
- 假设我们有三个页面 A、B、C,首页是 A,第一次进入时它会依次触发一系列创建事件:
beforeCreate、created、beforeMount、mounted、page:init、page:beforein、page:afterin - 然后我们从 A 跳转到 B,因为 B 被初始化了,它会依次触发第一步中的一系列创建事件,但是此时 A 并没有被销毁,它只触发了两个事件:
page:beforeout、page:afterout,且还留在 DOM 中,以便从 B 返回时能有滑动效果 - 然后我们从 B 跳转到 C,这时 A 才会被销毁,依次触发一系列销毁事件:
page:beforeremove、beforeDestroy、destroyed;而 B 仅仅触发了page:beforeout、page:afterout;C 会触发第一步中一系列的创建事件 - 现在我们从 C 返回到 B,此时 C 会触发一系列销毁事件,B 仅仅只会触发
page:reinit、page:before-in、page:afterin,接下来注意,虽然还没有返回到 A,但 A 在此时被提前创建了,触发了第一步中的一系列创建事件 - 最后我们从 B 返回到 A,这时 B 才会触发一系列销毁事件,A 仅仅只触发了
page:reinit、page:beforein、page:afterin
从上面这个步骤可以大致摸清 F7 Router 的特点:
- 页面组件默认会保持两个,所以当路由深入时,上一个页面组件不会销毁,除非进入到了第三个页面
- 当前页面组件在返回上一页时会立刻被销毁,并提前创建前面一个页面
这样会导致一些问题,例如我一般会在 A 的 created 钩子里请求数据,并显示一个 loading 层,现在由于从 C 返回 B 时会提前创建 A,导致本应该在 A 显示的 loading 层出现在了 B 页面,所以我还得区分这次请求数据时,A 是第一次进入还是由子页面提前创建的,避免显示不必要的 loading 层……
page:beforeout 事件可能不会触发
上面提到过,F7 Router 的 beforeEnter、afterEnter 不能直接写在组件里,很不方便,所以我一般用 page:beforein、page:beforeout 代替这两个事件,但后来我发现在用router.navigate(url, options) 方法或者用 f7-link 组件时,如果 options 里设置了 reloadCurrent、reloadAll 和 reloadPrevious 的其中一个为 true,会导致 page:beforeout 不被触发,page:beforein 倒是不受影响。
其它不同之处
除了生命周期要比 vue-router 复杂的多之外,F7 Router 还有这些坑要注意一下:
- 文档上专门提到过,
$f7router只能在页面组件上获取到,如果页面组件的子组件里需要用到$f7router上的属性,需要用$f7.views.main.router访问。而我在开发的过程中发现,子组件的this是能直接读取到$f7router的,可用了之后才发现读到的不是实时的路由状态,最后老老实实从页面组件往下传了。 - f7-link 组件生成的
href是不带#!的,所以如果你的项目用的是前端 hash 路由,没有配置 History 模式,那么用户选择「在新标签页中打开链接」时会得到一个 404 页面。移动端的项目一般很少有用户会这么做,这算是我吹毛求疵了,但我觉得还是值得提一下。 - F7 Router 路径里的中文 params 会被自动 encode 成乱码,所以如果你在路径里用到了中文 params,还需要 decode 一下;querystring 里的中文不会被 encode。
- F7 默认情况下会且仅会在移动端下,给被点击的
a, button, label, span, .actions-button元素临时加上.active-state样式,所以 click 事件里对className的判断(例如$event.target.className === 'my-class-name')会失效,本地开发过程中根本不会发现这个问题,所以要确保用classList之类的 API 来判断类名。 - vue-router 中的 meta 写法在 F7 router 中照旧,但读取 meta 的
$router.meta.xxx要改成$f7route.route.meta.xxx - 在页面的滑动效果进行中的时候不要改变 DOM,这会导致滑动效果卡顿,可以在
page:afterin事件触发之后再改变 DOM
全文完。