业务开发

业务逻辑这块儿并就没有什么特别重要的东西要说,无非就是样式、兼容,再加上vue的语法。这些都是基本功,移动端没有IE,故而不会有太多让人窒息的问题。如果说有什么难点,那就是vuex,主要是它的几个核心比较难理解,理解了也不难。

目前只是一些基础的业务逻辑,故只是提一下我在这个项目中遇到的一些问题。

## Tabbar多米诺骨牌 ##

使用Vant的Tabbar完成底部菜单,五个菜单项,中间项是一个创建消息的功能。类似于手机QQ空间底部菜单栏,这个功能需要功能需要登录且是一个弹窗。那么问题来了:第一,不能使用<router-link>,须根据是否已登录执行相应的逻辑;第二,页面跳转到相应的链接,这个地址再回来对应的索引必须正确;第三,弹窗关闭,对应tabbar的索引也应该进行正确的指示;第四,这个创建图标不能有指示效果,也就是说它只能有一种状态;第五,不是所有页面都有Tabbar,它相对于底部的间距需要处理。

先解决第五个问题,提取组件,所有样式、逻辑集中处理,在需要的地方引用即可。这很容易想到,那么底部菜单的间距怎么做?这也容易想得到,组件渲染完成,给body注入一个菜单高度的底部间距即可。别忘了,在组件结束要移除这个样式。那么,什么时候注入,在哪个生命周期函数内注入?我在百度这个问题的时候,发现有人说beforeMount,这样真的可以吗?看图说话:

vue项目实践@树洞(三)-LMLPHP

红线之上是第一次渲染,红线之下是切换Tabbar之后的渲染,beforeMount在beforeDestroy之前先执行了,所以逻辑应该写在mounted里面。这就OK了吗?vue这类框架设计出来的终极目的是不去操作DOM,让虚拟DOM来做这个工作,所以这样做是不科学的。这里可以绑定class属性或者绑定style属性来做这件事,body操作不了,只好对App.vue下手。绑定写好了,怎样触发它呢?

这就需要用到vuex,mounted触发一次状态值改变,beforeDestroy再触发一次状态值改变。这样,只要使用这个组件都会触发样式更替,第五个问题完美解决。同样利用vuex来解决第二和第三个问题,这两个问题可以并案处理。最初我想用参数来处理,后来觉得不科学,假如底部菜单架构改变会有很大改动。于是我改为映射,将路由地址存在数组里面,解析地址得出索引。

// 匹配路由是否存在于地址
function match(path) {
  return path ? (new RegExp(`${path}$`, 'ig')).test(document.location) : false
}
// 根据匹配得出路径索引
function getPathIndex() {
  return ['/', '/chat', '', '/mail', '/centre'].findIndex(v => match(v)) || 0
}

假如一个页面有多个路由的情况,现在的方法是无法完成的,比如首页,可以是/,也可以是/home,也可以是/index。还有一种路由地址冲突的情况,也会导致这个方法失效。这个根据具体情况做好映射就可以了,我的逻辑比较简单。得到索引之后,触发store.js里面的值改变,得到正确的显示。Vant的Tabbar索引必须正确,否则Tabbar切换路由上就会报错。因为创建消息是弹窗,弹窗不会触发地址改变,那么获取到的索引不会改变。

下面是地址携带索引参数的方式,则是通过路由参数来完成索引的识别,个人认为这会让地址变得很奇怪。

// 解析地址参数
function parse(url) {
  return (url || document.location.search || document.location.hash).match(/\?(.*?)$/ig)
}
// 将参数转为对象
function getParamsByReg(url) {
  let params = parse(url)
  if(params) {
    params = params[0].replace(/^\?/g, '').replace(/(^|&)(.*?)=(.*?)/g, ',"$2":$3')
    params = JSON.parse(`${params.replace(/^,/, '{')}}`)
  }
  return params || {}
}

第一个问题和第四个问题很简单,Vant的Tabbar默认不是路由模式,这个item不配置to属性。在触发切换的事件上根据需要进行逻辑判断,第一个问题解决。第四个问题,Vant同样给了开发者自由嵌入内容的选择。这两个问题看似不是什么问题,其实不然,这儿用的是别人封装好的库,很简单。提出这两个问题的目的在于,这是思考问题和解决问题的过程,也是写出优秀组件的必然过程。

## 下拉加载 ##

Vant封装了List组件,上滑加载更多,默认要预先加载一屏的内容,而且似乎只对异步有效。它的原理大概是监听滚动的距离,再根据视窗的高度和内容的高度计算距离底部的位置进行加载。这些逻辑不算复杂,需要注意的是加完事件监听,在组件销毁的时候应该对对应的引用进行销毁,否则很容易造成内存泄漏。

除了使用系统的滚动监听外,还可以自己编写滚动效果,这样计算会更加方便,这方面的逻辑可以参考iScroll。如果自己懒得动手,可以借助别人写好的库,目前我还没在vue中使用过,暂无推荐。any-touch支持vue,better-scroll是改写的iScroll,具体效果怎样,只有使用之后才知道。

## 修改<title> ##

很多人立马想到JS动态修改,先在路由里定义好title,然后在路由跳转的时候进行修改。

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      meta: {
        title: '森林'
      },
      component: () => import('./views/Home.vue')
    }
  ]
})
router.beforeEach((to, from, next) => {
  if (to.meta.title) {
    document.title = to.meta.title
  }
  next()
})

假如title需要读取文章的标题,每次都不一样,这种方法就有问题了。自己撸代码就不必了(想锻炼一下自己也未尝不可),有现成的车轱辘vue-wechat-title。

# 安装
npm i -D vue-wechat-title
// 全局引入
import Vue from 'vue'
import Title from 'vue-wechat-title'

Vue.use(Title)
<template>
  <!-- 使用 -->
  <div id="app" v-wechat-title="$route.meta.title" img-set=" ">
    <router-view/>
  </div>
</template>

v-wechat-title属性换成变量,就能实现title动态改变了。img-set属性用来修改图标,默认值是一个base64图标会有警告,因为还没有图标,所以我用了一个空格占位。

## vue-lazyload ##

vant是集成了vue-lazyload,我的引入方式是全部导入,在使用图片懒加载的时候却告诉我没有指令。因此,我只好另外安装vue-lazyload,然后全局引入,也就是现在代码库的样子。后来仔细看了官方文档,默认的是vant输出,集成的组件不在vant中,改成这样就可以了。

import Vant, { Lazyload } from 'vant'
import 'vant/lib/index.css'

Vue.use(Lazyload)
Vue.use(Vant)

## 不存在的路由 ##

当用户请求了一个不存在的地址,因为匹配不到对应的路由,这时就是一片空白,对这样的路由要进行拦截。

router.beforeEach((to, from, next) => {
  if (to.matched.length === 0) {
    // 跳转到404页面
    next({ path: '/404', replace: true })
  } else {
    next()
  }
})

打包测试

当引入vant之后,再一次跑项目其实很快就会发现,编译速度特别慢。事实上vue-cli 3在编译上做了优化,速度应该比vue-cli 2快才对,怎么会感觉慢呢?!先打包一下再看看什么效果!

vue项目实践@树洞(三)-LMLPHP

两个警告,说的都是同一个问题——资源过大。别急,复制第一个警告去问下度娘怎么处理。度娘给了我们很多答案,答案都差不多,归结起来就两点:第一,关闭警告,我不听你的警告;第二,调节阈值,将警戒线调高。然后呢,然后可爱的小伙伴们就按着他的意思去做了。如果是一两篇文章这么说情有可原,一连串的都这么说,误人子弟啊。我前面的文章就提到过,很多人自己压根儿就没搞清楚就去复制别人的文章,搞得满世界都是差不多文章。他还不服,转载标识都舍不得给原创。

为什么说误人子弟?脚疼就把脚给据了,手疼把手砍了,头疼头部以下截肢:这不是瞎胡闹吗?闭上眼,看不见,问题就解决了吗?为什么webpack给的上限建议是244KiB?这个值其实已经很大了,这么大的文件会影响到资源的加载!我们不能捂着眼睛就当做看不见了,而应该去优化资源。

VUE CLI自身给了图片压缩,这个且不管了,进入vue.config.js做一些调整。生产环境不需要排错,先关闭map。

productionSourceMap: false

代码的压缩工具换成uglifyjs-webpack-plugin,可以利用环境变量来判断是否去除打印。将它按照到devDependencies里面,在.env.debug文件增加一个值NPM_CONFIG_REPORT,利用这个值判断是测试服务器还是正式服务器。

NODE_ENV=production
VUE_APP_HOST=https://www.debug.com
NPM_CONFIG_REPORT=true

如果是正式服务器就去除打印,如果是测试服务器就允许打印,这样就不必为了调试产生的代码烦恼。很多人认为console.log无所谓,毕竟在手机端是看不到的。这是错误的想法,console.log会影响性能。可以做个测试,用canvas绘制一个1920*1080的壁纸,然后console.log输出base64,感受效果。

// 配置工具
configureWebpack: config => {
    if(process.env.NODE_ENV === 'production') {
      if(!process.env.NPM_CONFIG_REPORT) {
        // 压缩代码
        config.plugins.push(new UglifyJsPlugin({
          uglifyOptions: {
            compress: {
              drop_debugger: true,
              drop_console: true,
              pure_funcs: ['console.log']
            }
          },
          sourceMap: false,
          parallel: true
        }))
      }
    }
  }

稍微做了下优化,再次打包,没什么作用嘛,依旧包很大。为什么呢?安装webpack-bundle-analyzer到devDependencies,修改vue.config.js。

// 引入工具
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

// 配置工具
chainWebpack: config => {
    // 其他代码
    // ...

    if(process.env.NODE_ENV === 'production') {
      // 打包分析
      if(process.env.NPM_CONFIG_REPORT) {
        config.plugin('webpack-bundle-analyzer').use(BundleAnalyzerPlugin).end()
        config.plugins.delete('prefetch')
      }
    }
  }

我将分析工具配置在debug环境下运行,用debug环境打包,看下究竟是什么占据这么大空间。

vue项目实践@树洞(三)-LMLPHP

vant最大,其次是vue。整个vant都在里面,必然会增加包的体积,所以在引入的时候我就表示这种引入方式不科学。前面我讲废话时也曾吐槽过将jQuery引入vue,就是这个原因。用document.getElementById解决的事,偏偏去引入一个jQuery,这是看不起这个时代呀?

废话少说,有没有办法压缩它呢?答案是肯定的。看下打包时控制台的打印,三个字段File、Size、Gzipped。第三个字段就是压缩包的大小,这需要服务端配合。而且解压需要一个过程,这肯定不是优先的解决办法。既然它们合并在一起会干坏事,可不可以将它们分离?首先想到的就是cdn,通过人工去解决,很常见的一个办法。可是,这个操作其实是机械化的事情,如果每个项目都这么做,不就不符合工程化思想了吗?

当然,cdn并不是不好的思路,为了分流,这还是必然的。我在想,有没有自动化的方式来分离代码,这些资源几乎是不变的,何不提取公共库呢?利用webpack-cli,将需要提取的库分离出来,然后将公共库插入到html中。具体操作参考《vue-cli3 DllPlugin 提取公用库》,我就不再多说。配置好了,先跑一下公共库,再次打包。

vue项目实践@树洞(三)-LMLPHP

什么?公共库的包又超标了,它让我去webpack官网看看代码分割!这个超标的库只有vant,还能继续分割吗?我也没有答案,于是我试着将vant的整体引入改为按需引入,很明显包的体积一下子就下来了。不过,在上传的代码中我并没有按需引入,主要是方便使用。在实际开发中,只会用到部分组件,按需引用是最佳解决方式。

刚刚webpack提示已经提示了分割代码,在开发中还可以利用这个办法来减轻包的体积。VUE CLI是用的webpack打包,因此,学习webpack也是一个必修的功课,更多优化可以参考《Vue Cli3 项目打包优化》

最后

因为这个项目缺少产品角色,一开始并没有架构好,想法不成熟。我一边搭建项目,一边思考业务的时候,发现这个项目有点大,牵扯的业务甚多。加上各种原因,最后并没有按着理想的进度完成,被迫中止。这个系列暂且会告一段落,计划中还有一个服务器渲染的章节。

服务器渲染是建立在服务器基础之上,如果只是简单的业务逻辑就没必要拿出来说,看官方文档就好了,这一定得和服务器一起才有意义。通常,在实际工作中接触不到服务器,绝大部分人就是码农而已。我认为知道服务器部署对服务器渲染会理解更多一点,这对于我来说也是一个全新的课题。

服务器渲染没有数据的话也是没有意义的,在中止的这段时间我会重新梳理@树洞的业务,从最核心的地方入手,精简项目,确保项目完整。也希望再更新的时候能给大家带来更多有用的东西。

## 代码仓库 ##

https://gitee.com/IanLew/tree-hole.git

## @树洞系列 ##

vue项目实践@树洞(一)

vue项目实践@树洞(二)

vue项目实践@树洞(三)

09-20 12:29