Framework7 Vue 踩坑记录

@lmk123 2019-02-11 12:19:01发表于 lmk123/blog Framework7Vue.js

三年前我刚入职的时候接手了一个移动端的项目,当时代码已经很难维护了,构建工具用的是 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 个工作日,升级的步骤是:

  1. 用 Vue CLI 替换 webpack-boilerplate
  2. 升级 Framework7 到 v3,并用 Framework7 Vue 替换 vue-framework7
  3. 升级项目的依赖到最新版本

升级过程中踩到了不少坑,于是我决定写篇文章记录一下,接下来我会按照顺序依次写下踩到的坑和解决办法。

Babel 的 loose 模式导致的错误

项目在替换成 Vue CLI 之后,运行的时候控制台报了个错,最后发现跟 Babel 的 loose 模式有关。

举个例子,代码 a.push(...b) 中,当 bundefined 的时候,按照 ES6 的规范,这里是应该报错的。默认情况下,Babel 会把这段代码转换成:

// 默认情况下
a.push.apply(a, babelRuntimeHelpers.toConsumableArray(b))

toConsumableArray() 方法会确保当 bundefined 的时候抛个错出来,但是如果开启了 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。除此之外,大部分组件都提供了 texttitle 这种可以控制组件文本内容的属性,我的建议是为了保险起见,优先使用属性。

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 中,有两种生命周期周期:

  • 组件的生命周期,例如 createdmounted
  • router 的生命周期,例如 beforeRouteEnterbeforeRouteLeave

但 F7 中的「页面」是特殊的组件,它的顶级根元素只能是 f7-page,且生命周期有三种:

  • 类似于 Vue 组件的 createdmounted
  • Router 生命周期,有点不同于 vue-router 的是,vue-router 的 beforeRouteEnterbeforeRouteLeave 既可以直接写在组件上,也可以写在路由配置里,但 F7 的 beforeRouteafterRoute 只能写在 Router 配置里
  • f7-page 独家提供了 page:initpage:beforein 等事件

要理清这么多事件不简单,但它设计的这么复杂也是有原因的,因为默认情况下,页面之间的切换是有滑动效果的,所以它分别设计了三套事件,全面满足用户的各种需求。

文档上分别介绍了这三套事件,但它们组合起来就是另一回事了。我通过观察,大致了解了它们的触发顺序与时机,这里我简单介绍一下。

  1. 假设我们有三个页面 A、B、C,首页是 A,第一次进入时它会依次触发一系列创建事件:beforeCreatecreatedbeforeMountmountedpage:initpage:beforeinpage:afterin
  2. 然后我们从 A 跳转到 B,因为 B 被初始化了,它会依次触发第一步中的一系列创建事件,但是此时 A 并没有被销毁,它只触发了两个事件:page:beforeoutpage:afterout,且还留在 DOM 中,以便从 B 返回时能有滑动效果
  3. 然后我们从 B 跳转到 C,这时 A 才会被销毁,依次触发一系列销毁事件:page:beforeremovebeforeDestroydestroyed;而 B 仅仅触发了 page:beforeoutpage:afterout;C 会触发第一步中一系列的创建事件
  4. 现在我们从 C 返回到 B,此时 C 会触发一系列销毁事件,B 仅仅只会触发 page:reinitpage:before-inpage:afterin接下来注意,虽然还没有返回到 A,但 A 在此时被提前创建了,触发了第一步中的一系列创建事件
  5. 最后我们从 B 返回到 A,这时 B 才会触发一系列销毁事件,A 仅仅只触发了 page:reinitpage:beforeinpage:afterin

从上面这个步骤可以大致摸清 F7 Router 的特点:

  • 页面组件默认会保持两个,所以当路由深入时,上一个页面组件不会销毁,除非进入到了第三个页面
  • 当前页面组件在返回上一页时会立刻被销毁,并提前创建前面一个页面

这样会导致一些问题,例如我一般会在 A 的 created 钩子里请求数据,并显示一个 loading 层,现在由于从 C 返回 B 时会提前创建 A,导致本应该在 A 显示的 loading 层出现在了 B 页面,所以我还得区分这次请求数据时,A 是第一次进入还是由子页面提前创建的,避免显示不必要的 loading 层……

page:beforeout 事件可能不会触发

上面提到过,F7 Router 的 beforeEnterafterEnter 不能直接写在组件里,很不方便,所以我一般用 page:beforeinpage:beforeout 代替这两个事件,但后来我发现在用router.navigate(url, options) 方法或者用 f7-link 组件时,如果 options 里设置了 reloadCurrentreloadAllreloadPrevious 的其中一个为 true,会导致 page:beforeout 不被触发,page:beforein 倒是不受影响。

其它不同之处

除了生命周期要比 vue-router 复杂的多之外,F7 Router 还有这些坑要注意一下:

  1. 文档上专门提到过$f7router 只能在页面组件上获取到,如果页面组件的子组件里需要用到 $f7router 上的属性,需要用 $f7.views.main.router 访问。而我在开发的过程中发现,子组件的 this 是能直接读取到 $f7router 的,可用了之后才发现读到的不是实时的路由状态,最后老老实实从页面组件往下传了。
  2. f7-link 组件生成的 href 是不带 #! 的,所以如果你的项目用的是前端 hash 路由,没有配置 History 模式,那么用户选择「在新标签页中打开链接」时会得到一个 404 页面。移动端的项目一般很少有用户会这么做,这算是我吹毛求疵了,但我觉得还是值得提一下。
  3. F7 Router 路径里的中文 params 会被自动 encode 成乱码,所以如果你在路径里用到了中文 params,还需要 decode 一下;querystring 里的中文不会被 encode。
  4. F7 默认情况下会且仅会在移动端下,给被点击的 a, button, label, span, .actions-button 元素临时加上 .active-state 样式,所以 click 事件里对 className 的判断(例如 $event.target.className === 'my-class-name')会失效,本地开发过程中根本不会发现这个问题,所以要确保用 classList 之类的 API 来判断类名。
  5. vue-router 中的 meta 写法在 F7 router 中照旧,但读取 meta 的 $router.meta.xxx 要改成 $f7route.route.meta.xxx
  6. 在页面的滑动效果进行中的时候不要改变 DOM,这会导致滑动效果卡顿,可以在 page:afterin 事件触发之后再改变 DOM

全文完。