使用Vue3 + Vite + TS 创建uni-app项目工程化实践

31,566 阅读5分钟

根据uni-app官方的文档指示构建工程

根据uni-app官方的文档指示,我们目前可以使用以下方式创建vue3 + vite + ts的uni-app工程项目

使用Vue3/Vite版

# 创建以 typescript 开发的工程  
npx degit dcloudio/uni-preset-vue#vite-ts vue3-vite-uniapp

下载好后,进行以下操作

cd ./vue3-vite-uniapp
yarn install

然后运行h5模块

yarn dev:h5

解决报错

发现报以下错误

01.png

这个错误是由于Node版本导致的,然后使用nvm管理Node包工具切换高级版本的Node,我是切换到15.6.0,然后再运行就没报错了。

成功运行

02.png

解决vscode语法报错

然后打开项目代码,瞄一眼,哎呀发现报语法错误

03.png

这是因为vscode的语法检查工具导致的,使用Vue3需要安装Volar,安装了Volar发现还是报这个错,然后查看一眼Volar文档,其中有这么一句话:

You need to disable Vetur to avoid conflicts.

所以需要把Vue2的Vetur语法提示检查工具禁止了,因为我还安装了Vetur-wepy所以也需要把这个也禁止了。

04.png

卸载不需要的包

因为默认的项目中带了vue-i18n的包,而我们小程序和微信H5项目一般都不需要多语言所以把vue-i18n删除了

npm uninstall vue-i18n

安装sass或less样式处理器

npm i -D sass

因为我们是使用Vite进行开发,所以只需要安装一下sass就可以了,不需要额外配置,比如像webpack那样安装loader

配置 eslint + prettier 自动格式化代码

安装相关依赖包

yarn add @typescript-eslint/eslint-plugin --dev
yarn add @typescript-eslint/parser --dev
yarn add @vue/eslint-config-prettier --dev
yarn add @vue/eslint-config-typescript --dev
yarn add @vuedx/typescript-plugin-vue --dev
yarn add eslint --dev
yarn add eslint-plugin-prettier --dev
yarn add eslint-plugin-vue --dev
yarn add prettier --dev

这个时候发现报错:

An unexpected error occurred: "EPERM: operation not permitted, unlink 'E:\test\vue3-vite-uniapp-test\node_modules\esbuild-windows-64\esbuild.exe'".

这个问题产生的原因就是在装包的时候,会删除之前的.bin文件再重新生成,由于文件被占用导致无法删除文件,因此就会报错,只需要关闭相应的占用程序即可。

配置.eslintrc.js文件

安装完成之后配置.eslintrc.js文件

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/typescript/recommended',
    '@vue/prettier',
    '@vue/prettier/@typescript-eslint',
     // eslint-config-prettier 的缩写
    'prettier',
  ],
  parserOptions: {
    ecmaVersion: 2021
  },
  plugins: [],
  rules: {
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': 'off',
    semi: 0,
  }
}

prettier配置 .prettierrc.js

module.exports = {
  printWidth: 120,
  tabWidth: 2,
  tabs: false,
  semi: false,
  singleQuote: true,
  quoteProps: 'as-needed',
  bracketSpacing: true,
  jsxBracketSameLine: false,
  arrowParens: 'always',
  endOfLine: 'auto',
}

vscode 配置 .vscode/settings.json

{
    "editor.formatOnSave": false,
    "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
    },
    "eslint.validate": ["typescript", "vue", "html", "json"],
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "[javascript]": {
      "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[vue]": {
      "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[html]": {
      "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[json]": {
      "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "json.format.enable": false
  }

配置package包检测命令

"lint": "eslint --ext .ts,tsx,vue src/** --no-error-on-unmatched-pattern --quiet",
"lint:fix": "eslint --ext .ts,tsx,vue src/** --no-error-on-unmatched-pattern --fix"

配置忽略检查的文件.eslintignore

*.css
*.less
*.scss
*.jpg
*.png
*.gif
*.svg
*vue.d.ts

设置别名

vite.config.ts

resolve: {
    alias: [
        {
            find: '@',
            replacement: resolve(__dirname, 'src'),
        },
    ],
},

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": [ "src/*" ],
    },
  }
}

配置vuex

yarn add vuex@next

目录结构

05.png

src\store\modules\app\action-types.ts

export enum AppActionTypes {
  ACTION_LOGIN = 'ACTION_LOGIN',
  ACTION_RESET_TOKEN = 'ACTION_RESET_TOKEN',
}

src\store\modules\app\ations.ts

import { ActionTree, ActionContext } from 'vuex'
import { RootState } from '@/store'
import { AppState } from './state'
import { Mutations } from './mutations'
import { AppActionTypes } from './action-types'
import { AppMutationTypes } from './mutation-types'

type AugmentedActionContext = {
  commit<K extends keyof Mutations>(key: K, payload: Parameters<Mutations[K]>[1]): ReturnType<Mutations[K]>
} & Omit<ActionContext<AppState, RootState>, 'commit'>

export interface Actions {
  [AppActionTypes.ACTION_RESET_TOKEN]({ commit }: AugmentedActionContext): void
}

export const actions: ActionTree<AppState, RootState> & Actions = {
  [AppActionTypes.ACTION_LOGIN]({ commit }: AugmentedActionContext, token: string) {
    commit(AppMutationTypes.SET_TOKEN, token)
  },
  [AppActionTypes.ACTION_RESET_TOKEN]({ commit }: AugmentedActionContext) {
    commit(AppMutationTypes.SET_TOKEN, '')
  },
}

src\store\modules\app\index.ts

import { Store as VuexStore, CommitOptions, DispatchOptions, Module } from 'vuex'
import { RootState } from '@/store'
import { state } from './state'
import { actions, Actions } from './ations'
import { mutations, Mutations } from './mutations'
import type { AppState } from './state'

export { AppState }

export type AppStore<S = AppState> = Omit<VuexStore<S>, 'getters' | 'commit' | 'dispatch'> & {
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
    key: K,
    payload: P,
    options?: CommitOptions
  ): ReturnType<Mutations[K]>
} & {
  dispatch<K extends keyof Actions>(
    key: K,
    payload: Parameters<Actions[K]>[1],
    options?: DispatchOptions
  ): ReturnType<Actions[K]>
}

export const store: Module<AppState, RootState> = {
  state,
  actions,
  mutations,
}

src\store\modules\app\mutation-types.ts

export enum AppMutationTypes {
  SET_TOKEN = 'SET_TOKEN',
}

src\store\modules\app\mutations.ts

import { MutationTree } from 'vuex'
import { AppState } from './state'
import { AppMutationTypes } from './mutation-types'

export type Mutations<S = AppState> = {
  [AppMutationTypes.SET_TOKEN](state: S, token: string): void
}
export const mutations: MutationTree<AppState> & Mutations = {
  [AppMutationTypes.SET_TOKEN](state: AppState, token: string) {
    state.token = token
  },
}

src\store\modules\app\state.ts

export interface AppState {
  token: string
}

export const state: AppState = {
  token: '',
}

src\store\getters.ts

import { RootState } from '@/store'
export default {
  token: (state: RootState) => state.app.token,
}

src\store\index.ts

import { createStore } from 'vuex'
import { store as app, AppState, AppStore } from '@/store/modules/app'
import getters from './getters'
export interface RootState {
  app: AppState
}
export type Store = AppStore<Pick<RootState, 'app'>>

export const store = createStore<RootState>({
  modules: {
    app,
  },
  getters,
})

export function useStore(): Store {
  return store as Store
}

vuex使用示例

<template>
  <view class="content">
    <image class="logo" src="/static/logo.png" />
    <view class="text-area">
      <text class="title">{{ title }}</text>
      <view @click="setToken">login</view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { AppActionTypes } from '@/store/modules/app/action-types'
import { useStore } from 'vuex'
const title = ref('Hello')
const store = useStore()
const setToken = () => {
  store.dispatch(AppActionTypes.ACTION_LOGIN, 'token')
  title.value = store.state.app.token
}
</script>

配置uni.request实现网络请求

/* eslint-disable @typescript-eslint/ban-types */
import appConfig from '@/config/app'
import { useStore } from 'vuex'
const { HEADER, HEADERPARAMS, TOKENNAME, HTTP_REQUEST_URL } = appConfig
type RequestOptionsMethod = 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT'
type RequestOptionsMethodAll = RequestOptionsMethod | Lowercase<RequestOptionsMethod>

/**
 * 发送请求
 */
function baseRequest(
  url: string,
  method: RequestOptionsMethod,
  data: any,
  { noAuth = false, noVerify = false }: any,
  params: unknown
) {
  const store = useStore()
  const token = store.state.app.token
  const Url = HTTP_REQUEST_URL
  let header = JSON.parse(JSON.stringify(HEADER))
  if (params != undefined) {
    header = HEADERPARAMS
  }
  if (!noAuth) {
    if (!token) {
      return Promise.reject({
        msg: '未登录',
      })
    }
    if (token && token !== 'null') header[TOKENNAME] = 'Bearer ' + token
  }

  return new Promise((reslove, reject) => {
    uni.showLoading({
      title: '加载中',
      mask: true,
    })
    uni.request({
      url: Url + url,
      method: method || 'GET',
      header: header,
      data: data || {},
      success: (res: any) => {
        console.log('res', res)
        uni.hideLoading()
        res.data.token &&
          res.data.token !== 'null' &&
          store.commit('LOGIN', {
            token: res.data.token,
          })
        if (noVerify) {
          reslove(res)
        } else if (res.statusCode === 200) {
          reslove(res)
        } else {
          reject(res.data.message || '系统错误')
        }
      },
      fail: (msg) => {
        uni.hideLoading()
        reject('请求失败')
      },
    })
  })
}

// const request: Request = {}
const requestOptions: RequestOptionsMethodAll[] = [
  'options',
  'get',
  'post',
  'put',
  'head',
  'delete',
  'trace',
  'connect',
]
type Methods = typeof requestOptions[number]
const request: { [key in Methods]?: Function } = {}

requestOptions.forEach((method) => {
  const m = method.toUpperCase as unknown as RequestOptionsMethod
  request[method] = (api, data, opt, params) => baseRequest(api, m, data, opt || {}, params)
})

export default request

利用Koa配置Mock数据服务器

dependencies

"dependencies": {
    "koa": "^2.13.0",
    "koa-body": "^4.2.0",
    "koa-logger": "^3.2.1",
    "koa-router": "^10.0.0",
    "koa2-cors": "^2.0.6",
    "lodash": "^4.17.20",
    "log4js": "^6.3.0",
    "faker": "^5.1.0",
    "reflect-metadata": "^0.1.13"
}

devDependencies

"devDependencies": {
    "@types/koa": "^2.11.6",
    "@types/koa-logger": "^3.1.1",
    "@types/koa-router": "^7.4.1",
    "@types/koa2-cors": "^2.0.1",
    "@types/faker": "^5.1.5",
}

在根目录下建一个mock目录

具体目录结构如下

06.png

package.json里配置mock服务器启动命令

"scripts": {
	"mock": "cd mock && ts-node-dev mock.ts"
}

api/user.ts

import request from '@/utils/request.js'

/**
 * 获取用户信息
 *
 */
export function fetchUserInfo() {
  return request?.get?.('/user/userInfo', {}, { noAuth: true })
}

测试访问使用

fetchUserInfo()
  .then((r) => {
    console.log('r', r)
  })
  .catch((err) => console.log(err))

测试运行

运行小程序测试环境

npm run dev:mp-weixin

启动正常并打印出了mock的数据

07.png

打印出mock数据

08.png

同理测试H5端运行情况

npm run dev:h5

运行正常并打印出了mock数据

09.png

实践总结

目前成功实践配置了使用Vue3 + Vite + TS 来搭建uniapp项目,并成功运行了H5端和小程序端,目前这两个端应该是需求最多的,至于其他端,本人并不擅长,所以就留给大神们去探索研究吧。

后续将会继续探索实现具体的项目。

项目源码地址

github.com/amebyte/vue…