Web项目实践@树洞(前端篇vue3)

原创
2021/07/14 18:21
阅读数 1.2K

前一篇对后端接口开发做了一个简简单单介绍,不够专业,这一篇主要对《树洞》前端做一个介绍。对于里面某些细节的部分不再做说明,比如移动端适配,可以参考我之前《树洞》系列文章。

《树洞》这次的需求设计并不像最初那么复杂,主要在于技术实践。在开始做这件事的时候,需求设计就像电影《解忧杂货铺》那样,分为两个部分:一个部分是公开信笺,另一个部分是私有信笺。从功能上,它们只是可见性的区别而已,所以最后只做了公开信笺。

功能需求

和电影不同的是公开信笺并不只是由后台管理员回复,而是随机从用户中抽取回复人,且只有该用户拥有回复权限。所有人都可以看到信笺内容和回复内容,在设计上是允许对信笺和回复发表自己的态度、分享,在做的过程中只做了对原信笺的赞同,分享并未进行功能实现。官网的功能还包括用户的登录、注册、修改密码、修改个人资料、查看创建内容和查看回复内容。

后台管理系统功能设计的功能包括对后台权限的增改查、后台用户的增改查、官网用户的增改查、信笺及用户对信笺操作数据的查看。起初的设计中这些功能还包括删除操作,对后台用户、官网用户的启用、禁用操作、信笺的审核等等。不过,一个人的精力实在有限,而关键的一点是它不能成为一个商业化的东西。于是不断的简化简化,尽可能去做一些技术上的练习。毕竟平时上班也在不停地重复一些界面功能,没什么悬念可言,最终只留下了一些简单的功能。

技术选型

最开始标题准备写《Web项目实践@树洞(前端篇)》,转念一想,搞得好像到终点站一样。然而并不是这样,《树洞》项目在早使用的vue2.x,现在使用的vue3.x,之后可能会有react、angular、小程序、手机App等等版本,于是加上一个vue3宣示主权。在开始写这个项目的时候觉得vue3.0还是少了点挑战,加了个TypeScript,还是感觉少点什么于是又决定用vite。现在项目写完了,先做个总结。

首先,虽然标榜使用TypeScript,但是最终逻辑并没有完全执行TypeScript写法。简单地尝试了一下,写着写着有些鸡肋的感觉。比如有一个表单,我要先去声明一个表单里面的数据类型。再比如传递给接口的参数为number和string的联合类型,但是在传参的时候它变成值就是其中一种类型,然而被认为是null而报错。最后实在受不了,我全换成了any类型。对于TypeScript的写法,我再适应一段时间,可能就习惯了。

然后是vite,对于这个工具其实也没有太多想说的,它的编译效率不可否认要快不少,只是我总感觉它的模板并不如vue-cli成熟。比如别名,说实话我没太明白它的配置规则,这个在后面的vite配置再做讨论(问题已解决)。有一点特别爽的是:在对包进行升级的时候不会像vue-cli受到webpack的约束。

最后,没有最后。vue整体上是比较简单的,哪怕是到了vue3,只要vue2比较熟悉,看看vue3迁移指南基本上没啥大问题。只是vue3对应的生态圈,诸如组件、插件之类的不如vue2丰富。就拿UI库来说,我最常用的是element和ant-design。ant-design-vue2.x看似已经转入正式版了,其实它还在不停的变动,而且还有比较大的变动。比如useForm在v2.2之前需要单独引入,在这之后已经集成到了Form。而element-plus截止到我写这篇文章,还处于beta版或者alpha版(一直在摇摆),这完全不敢用啊。

element和ant-design这两个UI库说不上谁好谁不好。ant-design在功能上稍微要更丰富一些,比如Form组件的useForm、Table组件的columns属性、Input的pressEnter事件等等。而element在细节上要稍微更注重一些,比如InputNumber的加减按钮、Select的异步搜索、Image的加载状态等等。因此,这两个库谈不上谁更优秀。element还处于不稳定状态,于是管理后台使用ant-design。原本打算element和ant-design在管理后台和官网上各使用一个,后舍弃了做PC,也就舍弃了用element-plus。

开始折腾

npm init vite@latest

首先创建项目,截图是我最开始搭建项目时所留下,关于vite的相关信息请参考Vite官方文档

切换目录到项目下,开始安装包,然后让项目跑起来。哎,怎么跑不起来,是因为没腿吗?

可能是因为我的node环境,我用的是node v14,既然它没腿就让它长上腿吧。

node node_modules/esbuild/install.js

项目可以跑起来了,下面我们开始引入UI库,做一些配置。先说一下配置,vue-cli的配置是在vue.config.js里面配置,而vite是在vite.config.ts(因为我是选的是ts模板)里面。配置项大都与vue-cli差不多,当然也有不同的地方,毕竟是不同的工具。比如插件配置,再比如我们接着要说的UI库ant-design-vue的主题定制,我们首先需要引入的不是css而是less文件,然后要开启javascript,否则会报错。

按照官网的配置方法配置,结果不生效。需要特别说明一下的是:我源码中使用的less-loader是10.0,这也会有影响,不同版本的参数可能存在差异。不可否认,我最开始是直接安装的less和less-loader,它们都是用的最新版,然后我去查官网资料,结果查到:

它告诉我这个配置项已经被废弃了,是因为这个原因吗?当然,并不是,我立即按照官网的版本重新下载包,结果还是有问题。同样的版本同样的配方,还是不生效,为什么。再去查看vite、ant-design-vue、less-loader文档,最后发现它一直在玩lessOptions这个配置项。去掉它,搞定,自定义终于生效了。在使用vue-cli的时候,我对sass-loader升级,结果一下子就报错了,我就发现它和webpack有极强的关联。

不仅如此,less也一样,loader、nodejs、UI库都有可能影响到它们,有时稍不注意一个升级就报错了。如果我在vite升级,会出现什么情况?带着这个疑问,我用npm-check-updates对项目进行升级,结果发现没什么问题。于是我用同样的配方对vant进行配置,等我配置完使用才发现,vant3.x根本不需要,因为它使用的css4语法中的var进行变量的定义。

css: {
  preprocessorOptions: {
    less: {
      javascriptEnabled: true,
      modifyVars: {
        hack: `
          true;
          @import "${resolve(__dirname, 'src/styles/variables.less')}";
          @import "${resolve(__dirname, 'src/styles/mixins.less')}";
        `
      }
    }
  }
}

到此为止,我们的项目总该又能跑起来了吧?不,不行,因为我们在配置less的javascriptEnabled的之前,有这么一段代码:

@import '~ant-design-vue/dist/antd.less';

这段代码会导致程序报错:

在vite里面它不认识“~”了,不认识我们就需要去配置这个别名。既然它不认识“~”,同样也不认识“@”。

然而,这两个别名的配置让我有些想不明白,先上代码:

import path from 'path'

resolve: {
  alias: [
    // { find: '@', replacement: path.resolve(__dirname, 'src') },
    // 使用正则的时候只能使用path.join,否则路径不对
    { find: /^@\//, replacement: path.join(__dirname, './src/') },
    { find: /^~/, replacement: '' }
  ]
}

一个使用字符串,一个使用正则,必须这样,否则不生效。“~”使用正则,我能想明白,它是在替换以“~”开头的路径。于是我就想用正则来匹配以“@/”开头的路径,结果不是我想要的结果。然后我去查看vue-cli的配置,没发现什么特别的地方,这让我很纳闷。这只能留到后面再细细研究了,先把坑留在这儿。

(问题已解决:网上很多教程都使用的是字符串和path.resolve,path.resolve的路径后面少一个“/”从而导致路径错误,只能用path.join,它们的区别请自行查阅相关资料。另外,使用path须要重启服务,注意避坑。)

现在UI库引进来了,但是任务还没有结束,vue-routervuex还没有,得自己引入。这样的做法无疑在告诉开发者,你可以用你自己喜欢的解决方案。当然vue3几乎可以用Provide / Inject取代vuex,不管怎么样,要用vuex就得自己引入。配置vue-router,我从vue-cli的配置中拷贝了一个过来,结果发现它报错了:

process不存在?在官方文档中createWebHashHistory并没有传这个参数。本着研究的原则,假如我需要这么一个环境变量,怎么办?vite给出了自己的变量import.meta.env.BASE_URL。

OK路由已经加上,该让代码策马崩腾了!

最后

还没完呢,界面逻辑是没问题了,我们还需要请求数据。我使用axios:一般情况下我们只会对正常的数据进行处理,其余不管是请求和响应时的错误只需要做一个统一提示即可。除了响应拦截,我们还要对请求进行拦截,放入token之类的头部参数,所以需要对axios进行二次封装。另外,我们还需要对重复请求、路由跳转的时候未完成的请求进行处理。

首先初始化创建一个axios:

const $axios = axios.create({
  baseURL: '/api',
  timeout: 10000
})

为了处理重复请求、路由跳转的时候未完成的请求进行处理,axios提供了cancel方法,我们需要将这些cancel存起来方便处理:

// cancel token存储
const cancelToken = new Map()

/**
 * 存储cancel token
 * @param config axios请求配置
 */
function setCancelToken(config: AxiosRequestConfig) {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
    if (!cancelToken.has(url)) {
      cancelToken.set(url, cancel)
    }
  })
}

/**
 * 移除cancel token
 * @param config axios请求配置
 */
function deleteCancelToken(config: AxiosRequestConfig) {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  if (cancelToken.has(url)) {
    cancelToken.get(url)()
    cancelToken.delete(url)
  }
}

/**
 * 清除所有cancel token
 */
export function clearCancelToken() {
  for (const cancel of cancelToken.values()) {
    cancel()
  }
  cancelToken.clear()
}

路由跳转时需要清除所有未完成的请求,所以要将其暴露出去。最后配置路由拦截,因为我要统一处理错误,所以我做了以下封装:

/**
 * 错误处理,401提示登录,其余提示错误信息
 * @param error 错误数据
 */
function errorHandle(error: any) {
  // 提示处理
}

/**
 * 处理正常响应数据处理错误
 * @param response axios响应数据
 * @returns 错误信息
 */
function misdataHandle(response: AxiosResponse) {
  const err = {
    status: response.status,
    statusText: response.statusText,
    code: response.data && response.data.code,
    message: response.data ? response.data.message : createError(response.status),
    data: response.data && response.data.data
  }

  // 错误处理
  errorHandle(err)

  return err
}

/**
 * 响应异常错误处理
 * @param error axios异常错误数据
 * @returns 错误信息
 */
function exceptionHandle(error: AxiosError) {
  let err: any = {}

  if (error.response) {
    err = misdataHandle(error.response)
  } else if (error.request) {
    err = misdataHandle(error.request)
  } else {
    err.message = error.message || createError()
  }

  // 非cancel token产生的错误处理
  if (!axios.isCancel(error)) {
    deleteCancelToken(error.config)
    errorHandle(err)
  }

  return Promise.reject(err)
}

其中createError()是当没有拿到错误信息时,根据状态码返回固定的错误信息,完整的封装请参考源码。然后,在路由中引入clearCancelToken通过路由守卫调用清除未完成的请求,在接口文件引入暴露的axios实例统一管理接口。

具体的业务逻辑就不说了,没什么新意,打完收工!

## 代码仓库 ##

前后端所有代码都在一个仓库不同的分支,代码拉下来切换分支即可。

仓库地址:GiteeGithub

##@树洞系列文章##

Web项目实践@树洞(接口篇)

Web项目实践@树洞(前端篇vue3)

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
1 收藏
0
分享
返回顶部
顶部