如何使用vue3+vite+typescript+pinia+uni-ui+pnpm从0到1创建uni-app多端小程序APP工程化项目
🙂前言
一方面是为了把 vue3+typescript+pinia+pnpm+vite+uni-app+uni-ui 玩一玩,也为和前面用 taro3+vue3+tsx+pnpm+pinia做的小程序做个思想碰撞,因为 uni-app+vue3+typescript 用的 template 的写法思路来开发,而 taro3+vue3+tsx 用的 react 的 hooks 思想,看看两个思路的开发实践差异点和效率等方面具体的情况能碰撞出怎样的火花。
😍uni-app(vue3+vite+typescript+pinia+uni-ui)微信小程序APP工程模版实体截图
工程项目暂时定义为测试阶段的初始模版,所以里面有部分小波自测冗余代码,拿去用的话需要自己看看手动删除,后续小波有时间会更新版本清理。
😦目标功能
done:
- 集成 vue3、typescript、pinia、vite、uni-ui、nodejs-koa、pnpm,使用 vue3 模版渲染思路开发
- 多人协作 eslint、prettier 代码格式校验规范, vsocde 配置
- jest 单元测试
- pinia 全局状态管理
- 小程序分包配置
- 小程序自定义顶部导航
- nodejs-koa 接口服务
- 多环境 api 接口判断
- 页面:首页、点餐
todo:
- git提交 husky 校验
- git提交 commit 校验
- 改成 tsx 完善整个系统流程
- 生产环境去除 console
- nodejs-koa 提取到 vercel 做成在线 api 服务
- 打包 app 流程
- vite 深入学习实践
🧐主要技术栈
- vue3
- typescript
- uni-ui
- pinia
- vite
- pnpm
- koa
- nodejs
- sass
- jest
- eslint+prettier
- husky
- commit
🙂总纲
- 安装 vscode 插件 vue volar 全家桶、eslint、prettier、sass,禁用或者直接删除 vetur (这个插件是 for vue2),安装微信开发工具,HBuildX
- 安装 pnpm 和 uni-app 脚手架并初始项目
- 卸载不需要多余依赖库
- 安装 eslint + preitter 相关依赖库,创建相关配置文件 .eslintrc.js、.prettierrc.js, vscode 配置: .vscode/settings.json ,配置 package.json 包检测命令,配置别名
- 安装 pinia 并配置
- 安装 sass
- 封装 uni.request 请求
- 利用 nodejs - koa 配置 mock 数据服务器
- 配置 jest 单元测试
- 安装 uni-ui 依赖库
- 配置小程序渲染
- 配置多环境 api 接口
- 从0到1架构uni-app多端工程化项目遇见的问题
🤔搭建uni-app微信小程序APP工程化项目步骤
第一步:安装 vscode 插件、微信开发工具和 HBuildX
vscode 插件
eslint
prettier
volar (ts版本也一并安装)
sass
以上的插件可以在 vue volar extension pack 中直接全装
提示
不需要开发 vue2 则删除 vetur 插件,需要就先禁用,vue3 中会导致代码报错
微信开发工具
小程序帐号注册,获取的AppID并导入本地的项目
HBuildX
直接官网下载,主要是配置打包小程序后在微信开发工具中开发和测试
第二步:安装 pnpm 和 uni-app 脚手架
以下操作基于已经安装 nodejs 环境操作,例如小波的环境版本
1 |
|
安装 pnpm
1
2
3
4
5
6// 全局安装
npm install pnpm -g
// 切换淘宝源
pnpm config set registry https://registry.npmmirror.com/
// 查看源
pnpm config get registry提示
pnpm 跟 npm 和 yarn 的差距就是把原来每个项目安装 modules 放到统一的文件夹中,通过符号链接(软连接)和硬链接,注意项目要和 pnpm 统一存依赖的 modules 同盘,不然就等于丢失了 pnpm 的优势。
安装 uni-app 脚手架(参考官网文档[2])
1
2# 创建以 typescript 开发的工程
npx degit dcloudio/uni-preset-vue#vite-ts vue3-vite-uniapp注意
一定要选择和pnpm存依赖包相同的盘符安装创建项目哦
第三步:初始项目
切到自己创建的文件夹执行初始命令
1
2
3// cd到自己建立的文件夹
cd x/vue3-vite-uniapp
pnpm install初始的
package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70{
"name": "uni-preset-vue",
"version": "0.0.0",
"scripts": {
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-app-plus": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-components": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-h5": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-alipay": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-baidu": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-jd": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-lark": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-qq": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-toutiao": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-quickapp-webview": "3.0.0-alpha-3070720230316001",
"vue": "^3.2.45",
"vue-i18n": "^9.1.9"
},
"devDependencies": {
"@dcloudio/types": "^3.3.2",
"@dcloudio/uni-automator": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-cli-shared": "3.0.0-alpha-3070720230316001",
"@dcloudio/uni-stacktracey": "3.0.0-alpha-3070720230316001",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-3070720230316001",
"@vue/tsconfig": "^0.1.3",
"typescript": "^4.9.4",
"vite": "4.0.4",
// tsc默认已装
"vue-tsc": "^1.0.24"
}
}和小波已经搭建好的项目
package.json
差异的引入包1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64"scripts": {
// lint 检测
"lint": "eslint --ext .ts,tsx,vue src/** --no-error-on-unmatched-pattern --quiet",
// lint 修复
"lint:fix": "eslint --ext .ts,tsx,vue src/** --no-error-on-unmatched-pattern --fix",
// koa-mock数据服务
"mock": "cd mock && ts-node-dev mock.ts",
// jest 自测
"test": "jest",
// jest 测试覆盖率
"test:unit": "jest --coverage"
},
"dependencies": {
// uni-ui
"@dcloudio/uni-ui": "^1.4.26",
// pinia
"pinia": "^2.0.33",
},
"devDependencies": {
"sass": "^1.60.0",
// type申明
"@types/faker": "5.1.5",
"@types/jest": "^29.5.0",
"@types/koa": "^2.13.5",
"@types/koa-logger": "^3.1.2",
"@types/koa-router": "^7.4.4",
"@types/koa2-cors": "^2.0.2",
"@types/node": "^18.15.9",
// eslint && prettier
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vuedx/typescript-plugin-vue": "^0.7.6",
"eslint": "^8.36.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.10.0",
"prettier": "^2.8.7",
// jest
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"@vue/test-utils": "2.0.0-rc.18",
"babel-jest": "26.6.3",
"jest": "26.6.3",
"jest-environment-node": "27.5.1",
"@testing-library/jest-dom": "^5.16.5",
"ts-jest": "26.5.6",
"vue-jest": "5.0.0-alpha.10",
// koa 数据接口服务
"chalk": "4.1.2",
"faker": "5.1.0",
"koa": "^2.14.1",
"koa-body": "^6.0.1",
"koa-logger": "^3.2.1",
"koa-router": "^12.0.0",
"koa2": "2.0.0-alpha.7",
"koa2-cors": "^2.0.6",
"lodash": "^4.17.21",
"log4js": "^6.9.1",
"postcss": ">=8.1.0 <9.0.0",
"reflect-metadata": "^0.1.13",
"ts-node-dev": "^2.0.0",
"tslib": "^2.5.0",
}初始的文件树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23vue3-vite-uniapp
├─ index.html // 入口
├─ package.json // 安装依赖配置
├─ pnpm-lock.yaml // pnpm锁定配置
├─ README.md // md说明
├─ src
│ ├─ App.vue // 小程序页面状态
│ ├─ env.d.ts // .vue组件componet时的ts申明
│ ├─ main.ts // 入口js,导入了app.vue
│ ├─ manifest.json // uniapp相关配置应用名称、appid、logo、版本等打包信息
│ ├─ components
│ │ └─ counter
│ │ │ └─ counter.vue // 测试pinia状态组件
│ ├─ pages
│ │ └─ index
│ │ │ └─ index.vue // 首页
│ ├─ static // 静态资源包
│ │ └─ logo.png
│ ├─ pages.json // 小程序路由,分包
│ ├─ shime-uni.d.ts // vue hooks的ts申明(暂时理解)
│ └─ uni.scss // uni-app内置的常用样式变量
├─ tsconfig.json // ts的配置
└─ vite.config.ts // vite配置和小波已经搭建好的项目文件树的差异
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55uniapp_vue3_vite_pinia
├─ .gitignore // git忽略文件
├─ .eslintrc.js // eslint的配置
├─ .prettierrc.js // prettier的配置
├─ jest.config.js // jest测试配置
├─ vue.config.js // uni-ui依赖使用
├─ .hbuilderx // HBuildX配置
├─ .vscode
│ └─ settings.json // vscod配置
├─ coverage // jest单元测试覆盖查看UI界面
├─ src
│ ├─ utils
│ │ ├─ request.d.ts // 封装请求ts申明
│ │ └─ request.ts // uni的请求方法封装
│ ├─ config
│ │ └─ app.ts // 封装接口使用的常量
│ ├─ api
│ │ ├─ user.d.ts // 用户页面接口ts申明
│ │ └─ user.ts // 用户页面使用接口
│ ├─ components
│ │ └─ uni-nav-bar // 自定义头部导航栏
│ │ │ ├─ style.scss // 样式
│ │ │ ├─ types.d.ts // 申明
│ │ │ ├─ uni-nav-bar.vue // 组件入口
│ │ │ └─ uni-status-bar.vue // 组件依赖的小组件
│ ├─ pages
│ │ └─ menu // 自定义头部导航栏
│ │ │ ├─ style.scss // 样式
│ │ │ ├─ types.d.ts // 申明
│ │ │ ├─ index.vue // 页面入口
│ │ │ └─ menuHooks
│ │ │ │ └─ index.vue // hooks必须要用Index来命名,不然报错
│ ├─ stores
│ │ ├─ golbalSysInfo.ts // 系统信息
│ │ └─ index.ts // 导出createPinia(主要是jest测试使用,不然会报错)
│ ├─ subPages // 分包文件夹
├─ tests // jest 测试用例
├─ types
│ └─ global.d.ts // ts全局申明
├─ mock
│ ├─ controller
│ │ ├─ user.ts // 用户相关接口控制器
│ │ └─ banner.ts // banner图接口控制器
│ ├─ middleware // 前后端交互最重要的就是两个参数 request 和 respond ,每一个中间件执行完毕应该进入下一个中间件,因此还需要一个 next 参数,用来启动下一个中间件。
│ │ └─ resultHandler.ts // 用来给每个响应对象包装响应码等,输出ctx.body
│ ├─ mockdb // 各种假数据
│ ├─ utils
│ │ └─ logger.ts // 输出错误日志函数
│ ├─ constant.ts // 常量
│ ├─ mock.ts // 主入口
│ ├─ requestDecorator.ts // 生成 http method 装饰器,创建类路径装饰器
│ ├─ router.ts // 路由(拿到controller中定义的接口,结合meta数据添加路由 和 验证)
│ ├─ tsconfig.json // ts配置
│ └─ type.d.ts // ts申明启动命令 h5模式
1
pnpm dev:h5
确认初始么有问题,则开始后续操作
第四步:卸载不需要默认安装的依赖库
uni-app 脚手架默认安装了 vue-i18n 多语言依赖库,可根据需求自行选择是否卸载
1 |
|
第五步:安装 eslint + preitter 相关依赖库,配置 vscode
标准三件套
- 代码规范 ESlint
- 代码格式美化 Prettier
- 多人协作保持代码风格一致配置 vscode
安装相关依赖
1
2
3
4
5
6
7
8
9pnpm add @typescript-eslint/eslint-plugin -D
pnpm add @typescript-eslint/parser -D
pnpm add @vue/eslint-config-prettier -D
pnpm add @vue/eslint-config-typescript -D
pnpm add @vuedx/typescript-plugin-vue -D
pnpm add eslint -D
pnpm add eslint-plugin-prettier -D
pnpm add eslint-plugin-vue -D
pnpm add prettier -D提示
@vuedx/typecheck
和@vuedx/typescript-plugin-vue
不安装似乎也没啥影响,npm 上的描述是一个命令行检查 vue 项目的工具。在我的理解中一般使用于 githooks。安装也没关系,反正开发环境使用
设置代码规范和格式化规则
项目根目录创建
.eslintrc.js
并贴入以下代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51module.exports = {
// true: 它就会停止在父级目录中寻找
root: true,
// 预定义的全局变量,这里是浏览器环境
env: {
browser: true,
es2021: true,
node: true, // 如果defineProps报错
"vue/setup-compiler-macros": true,
},
// ESTree 只是一个 AST 的某一种规范,ESTree 本质上还是 AST
// ESLint 默认的 parser ,只转换 js,默认支持 ES5 的语法: 默认采用vue-eslint
parser: "vue-eslint-parser",
// 子配置:优先使用typescript-eslint,支持es2021
parserOptions: { parser: "@typescript-eslint/parser", ecmaVersion: 2021 },
// 扩展校验风格 合并 eslint 中的 plugins,rules 的
extends: [
"plugin:vue/base",
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/prettier",
"@vue/typescript",
],
// 它的默认 parser 参数会将代码转换为 AST,AST 被 plugin&rules 用来校验和生成错误信息
plugins: [],
rules: {
// 检测未使用的变量,函数和函数的参数
"no-unused-vars": "off",
// 检测未使用的变量,函数和函数的参数 for typescript
"@typescript-eslint/no-unused-vars": "off",
// 语句强制分号结尾
semi: 0,
// 如果报错回车结尾错误 window开发环境,但是上传git又是linux
endOfLine: "off",
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// 在rules中添加自定义规则 关闭组件命名规则
"vue/multi-word-component-names": "off",
},
// 忽略文件
ignorePatterns: [
"*.css",
"*.less",
"*.scss",
"*.jpg",
"*.png",
"*.gif",
"*.svg",
"*vue.d.ts",
],
};提示
extends:[]
配置语法说明:
plugin:vue/base
只是使解析工作的基本规则。还没有 lint 任何东西。plugin:vue/essential
以上,加上仅用于防止 Vue 中的错误或意外行为的规则。plugin:vue/strongly-recommended
以上,加上通常被认为是最佳实践的规则。plugin:vue/recommended
以上,加上一些经常被建议的样式规则。extends: [...]
大概语义:使用vue最基本规则,vue3错误或意外行为的规则,eslint最佳实践的规则
简写说明:
引入
@vue/eslint-config-prettier
可以简写为@vue/prettier
vue/base
全称应该是eslint-plugin-vue/base
简单理解就是 eslint 默认把和自己相关命名进行了转行
eslint-config-
,eslint-plugin-
ignorePatterns: []
忽略文件配置是为了 eslint 命令执行检测造成非必要的混淆报错
项目根目录创建
.prettierrc.js
并贴入以下代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22module.exports = {
// 超过最大值换行
printWidth: 120,
// 缩进2
tabWidth: 2,
// ??? == useTabs
tabs: false,
// 末尾添加分号
semi: false,
// 使用单引号
singleQuote: true,
// 给对象里的属性名是否要加上引号,默认为as-needed,即根据需要决定,如果不加引号会报错则加,否则不加
quoteProps: "as-needed",
// 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数参数只有一个时是否要有小括号 avoid: 省略括号
arrowParens: "always",
// 结尾是 \n \r \n\r auto
endOfLine: "auto",
};在
package.json
中 script 添加 Ts 检查命令和 Eslint 检查命令1
2
3
4"scripts":{
"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"
}调用
1
2pnpm lint
pnpm lint:fix配置 vscode
项目根目录创建
.vscode/settings.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
// 保存时格式化
"editor.formatOnSave": true,
// 保存时修复来自所有插件的所有可自动修复的ESlint错误
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
// 应通过ESLint验证的语言数组
"eslint.validate": ["typescript", "vue", "html", "json"],
// 默认格式化插件选择
"editor.defaultFormatter": "esbenp.prettier-vscode",
// js格式化选择
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
// vue格式化选择
"[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
// html格式化选择
"[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
// json格式化选择
"[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }
// "json.format.enable": false
}代码书写规范
请参考 vue 官方文档规范保持一致性,小波页面参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<script setup lang="ts">
/**
* 导入 start
* 顺序:1.依赖库 2.请求 3.常量 4.组件 5. ts申明
*/
import vue
import api/data/piniaStore
import hooks.vue/components.vue
import x.d
/**
* 变量 start
*/
const store: any = ...
const state: stateType = reactive({})
/**
* 函数 start
* @init 初始 - 判断骨架屏处理
*/
const handleInit = async (): Promise<void> => {
await api()
loading.value = false
}
/**
* 调用 start
*/
handleInit()
</script>
<template>
<view>...</view>
</template>
<style lang="scss">
@import './style.scss';
</style>配置别名
vite.config.ts
1
2
3
4
5
6
7
8
9
10import { resolve } from "path";
...
resolve: {
alias: [
{
find: "@",
replacement: resolve(__dirname, "src"),
},
],
},完整
vite.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import { defineConfig } from 'vite'
// 需要安装 @types/node
import { resolve } from 'path'
import uni from '@dcloudio/vite-plugin-uni'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [uni()],
resolve: {
alias: [
{
find: '@',
replacement: resolve(__dirname, 'src'),
},
],
},
})tsconfig.json
1
2
3
4
5
6"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [ "src/*" ],
},
}}完整
tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"lib": ["esnext", "dom"],
"types": ["@dcloudio/types", "jest"],
// 导入 x.d.ts 报错 x.d.ts”不是模块。
"typeRoots": ["node_modules/@types"],
// 此导入从不用作值,必须使用 "import type" ,因为 "importsNotUsedAsValues" 设置为 "error"
"importsNotUsedAsValues": "remove",
// 是一种类型,在同时启用了 "preserveValueImports" 和 "isolatedModules" 时,必须使用仅类型导入进行导入。
"preserveValueImports": false
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "./types"]
}注意
因为用了 typescript,
resolve
,_dirname
会报 ts 申明错误提示,需要在头部导入 ‘path’ 并安装 @types/nodes1
pnpm add @types/node -D
第六步:安装 pinia
安装 pinia
1
pnpm add pinia
导入 pinia
正常情况直接在
main.ts
导入 pinia1
2
3
4
5
6
7// pinia状态管理
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
app.use(createPinia())
return { app, }
}但是为了做 jest 单元测试,所以需要做点调整,创建
store/index.ts
1
2
3import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia代码使用
src/pages/menu/index.vue
1
2
3
4
5import pinia from '@/stores/index' // == createPinia()
import { useGolbalSysInfoStore } from '@/stores/golbalSysInfo'
const golbalSysInfo = useGolbalSysInfoStore(pinia)
console.log('customGlobalData------', golbalSysInfo.sysInfo)需要响应式则如下操作,同 vue3 组件父子传值绑定双向原理相同
1
2
3
4
5
6
7import pinia from '@/stores/index' // createPinia()
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counter = useCounterStore(pinia)
// 用 storeToRefs 双向后再解构出 count
let { count } = storeToRefs(counter) // 此时count为响应式的
第七步:安装 sass
1 |
|
因为我们是使用 Vite 进行开发,所以只需要安装一下 sass 就可以了,不需要额外配置,不像 webpack 那样安装loader
第八步:封装 uni.request
实现网络请求
src 文件夹中创建
config/app.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// app名字
export const APP_NAME = '廿壴博客'
// app的图片 // 静态资源的cos地址
export const IMAGE_URL = 'https://blog.ganxb2.com/img/about/blog_log.png'
// mock请求地址
export const HTTP_REQUEST_URL = 'http://localhost:3300'
// 请求头 - json
export const HEADER = {
'content-type': 'application/json',
}
// 请求头 - 表单
export const HEADERPARAMS = {
'content-type': 'application/x-www-form-urlencoded',
}
// tokenName
export const TOKENNAME = 'Authorization'src 文件夹中创建
utils/request.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117/* eslint-disable @typescript-eslint/ban-types */
// 获取常量
import { HEADER, HEADERPARAMS, TOKENNAME, HTTP_REQUEST_URL } from '@/config/app'
// import { useCounterStore } from '@/stores/counter'
// import { useStore } from 'vuex'
// 导入申明
import { RequestOptionsMethod, RequestOptionsMethodAll } from './request.d'
/**
* 发送请求
*/
// type handleBaseRequestType()
function handleBaseRequest(
url: string,
method: RequestOptionsMethod,
data: any,
{ noAuth = false, noVerify = false }: any,
params: unknown
) {
// const store = useStore()
// const token = store.state.app.token
// 从pinia中获取
const token = 'islogin'
const Url = HTTP_REQUEST_URL
let header = JSON.parse(JSON.stringify(HEADER))
// 如果传了参数 => 表单
if (params != undefined) {
header = HEADERPARAMS
}
// 如果未授权
if (!noAuth) {
// 并且token也是false
if (!token) {
// 提示未登录
return Promise.reject({
msg: '未登录',
})
}
// 有token并且不是Null 则拼token给请求头
if (token && token !== null) header[TOKENNAME] = 'Bearer ' + token
}
// 返回封装请求
// resolve: (value: unknown) => void, reject: (reason?: any) => void) => void
return new Promise((reslove, reject): any => {
// 加载提示
uni.showLoading({
title: '加载中',
mask: true,
})
// uni请求
uni.request({
// 常量Url拼上调用时传入的不同接口
url: Url + url,
method: method || 'GET',
header: header,
data: data || {},
success: (res: any) => {
console.log('uni请求封装 -------', res)
// 成功关闭loading
uni.hideLoading()
// 如果有token并且token不为null则修改状态管理的token
res.data.token && res.data.token !== 'null' && console.log('修改状态管理的token')
// store.commit('LOGIN', {
// token: res.data.token,
// })
// console.log('noVerify ------', noVerify, res)
// 如果未验证抛出返回对象
if (noVerify) {
reslove(res)
} else if (res.statusCode === 200) {
// console.log('statusCode ------', res.statusCode)
// 如果验证了并且 code = 200 则抛出返回的data
reslove(res.data)
} else {
// code 不是200也没有验证标识抛出错误
reject(res.data.message || '系统错误')
}
},
// 请求失败
fail: msg => {
uni.hideLoading()
reject('请求失败')
},
})
})
}
// const request: Request = {}
// 请求类型数组
const requestOptions: RequestOptionsMethodAll[] = [
'options',
'get',
'post',
'put',
'head',
'delete',
'trace',
'connect',
]
// 自定义ts的校验 methods
type Methods = (typeof requestOptions)[number]
// 定义request对象去接封装的请求(TS申明:如果在Methods中的一项)
const request: { [key in Methods]?: Function } = {}
// 循环请求类型数组
requestOptions.forEach(method => {
// item
const m = method.toUpperCase() as unknown as RequestOptionsMethod
// ge: { get(){} }
request[method] = (api: string, data: any, opt: RequestOptionsMethod, params: unknown) =>
handleBaseRequest(api, m, data, opt || {}, params)
})
export default requestsrc 文件夹中创建
api/user.ts
1
2
3
4
5
6
7
8
9
10import request from '@/utils/request'
import { userInfoType } from './user.d'
/**
* 获取用户信息
* 考虑错误返回 还要联合申明,暂时只写正确返回
*/
export function fetchUserInfo(): Promise<userInfoType> {
return request?.get?.('/user/userInfo', {}, { noAuth: true })
}调用
1
2
3
4
5
6
7import { fetchUserInfo } from '@/api/user'
...
fetchUserInfo()
.then((r: userInfoType) => {
console.log('用户信息---', r)
})
.catch((err: any) => console.log(err))
第九步:利用 nodejs - koa 配置 mock 数据服务器
因为这个服务是集成到当前 uni-app 中,所以不用把依赖装到 dependencies
1 |
|
安装依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23pnpm add ts-node-dev -D
pnpm add koa -D
pnpm add koa-body -D
pnpm add koa-logger -D
pnpm add koa-router -D
pnpm add koa2-cors -D
// 一个一致性、模块化、高性能的 JavaScript 实用工具库
pnpm add lodash -D
// 日志
pnpm add log4js -D
// ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。
pnpm add reflect-metadata -D
// 终端打印信息增加个性化颜色 特别注意 chalk 一定要指定版本
pnpm add chalk@4.1.2 -D
// 生成伪造数据 特别注意 faker 一定要指定版本
pnpm add faker@5.1.0 -D
pnpm add @types/faker@5.1.5 -D
pnpm add @types/koa -D
pnpm add @types/koa-logger -D
pnpm add @types/koa-router -D
pnpm add @types/koa2-cors -D
pnpm add @types/koa2-cors -D
pnpm add tslib -D贴入小波仓库中的 mock 文件夹
package.json
里配置 mock 服务器启动命令1
2
3"scripts": {
"mock": "cd mock && ts-node-dev mock.ts"
}开启 mock 服务
新建个终端窗口,然后切到 uni-app 工程项目文件夹执行以下代码
1
2cd your uni-app
pnpm mock
你可能遇到错误请参考如下解决方案
报错
1
[INFO] 17:13:02 ts-node-dev ver. 2.0.0 (using ts-node ver. 10.9.1, typescript ver. 4.9.5)Error [ERR_REQUIRE_ESM]: require() of ES Module C:\myWeb\vue3-vite-uniapp\node_modules\.pnpm\chalk@5.2.0\node_modules\chalk\source\index.js from C:\myWeb\vue3-vite-uniapp\mock\mock.ts not supported.Instead change the require of index.js in C:\myWeb\vue3-vite-uniapp\mock\mock.ts to a dynamic import() which is available in all CommonJS modules. at require.extensions..jsx.require.extensions..js (C:\Users\ganxb\AppData\Local\Temp\ts-node-dev-hook-33103583567897243.js:114:20) at Object.nodeDevHook [as .js] (C:\myWeb\vue3-vite-uniapp\node_modules\.pnpm\ts-node-dev@2.0.0_67kvwwhfsxe4y463wcu4dtvggu\node_modules\ts-node-dev\lib\hook.js:63:13) at Object.<anonymous> (C:\myWeb\vue3-vite-uniapp\mock\mock.ts:12:41) at Module._compile (C:\myWeb\vue3-vite-uniapp\node_modules\.pnpm\source-map-support@0.5.21\node_modules\source-map-support\source-map-support.js:568:25) at Module.m._compile (C:\Users\ganxb\AppData\Local\Temp\ts-node-dev-hook-33103583567897243.js:69:33) at require.extensions..jsx.require.extensions..js (C:\Users\ganxb\AppData\Local\Temp\ts-node-dev-hook-33103583567897243.js:114:20) at require.extensions.<computed> (C:\Users\ganxb\AppData\Local\Temp\ts-node-dev-hook-33103583567897243.js:71:20) at Object.nodeDevHook [as .ts] (C:\myWeb\vue3-vite-uniapp\node_modules\.pnpm\ts-node-dev@2.0.0_67kvwwhfsxe4y463wcu4dtvggu\node_modules\ts-node-dev\lib\hook.js:63:13) at Object.<anonymous> (C:\myWeb\vue3-vite-uniapp\node_modules\.pnpm\ts-node-dev@2.0.0_67kvwwhfsxe4y463wcu4dtvggu\node_modules\ts-node-dev\lib\wrap.js:104:1) at Module._compile (C:\myWeb\vue3-vite-uniapp\node_modules\.pnpm\source-map-support@0.5.21\node_modules\source-map-support\source-map-support.js:568:25) at Object.require.extensions..jsx.require.extensions..js (C:\Users\ganxb\AppData\Local\Temp\ts-node-dev-hook-33103583567897243.js:95:24)[ERROR] 17:13:08 Error: require() of ES Module C:\myWeb\vue3-vite-uniapp\node_modules\.pnpm\chalk@5.2.0\node_modules\chalk\source\index.js from C:\myWeb\vue3-vite-uniapp\mock\mock.ts not supported.Instead change the require of index.js in C:\myWeb\vue3-vite-uniapp\mock\mock.ts to a dynamic import() which is available in all CommonJS modules.
原因:因为chalk版本问题
解决方案:降到4.1.2
报错
1
@prefix('/banner')⨯ Unable to compile TypeScript:controller/banner.ts(3,1): error TS2354: This syntax requires an imported helper but module 'tslib' cannot be found.
解决方案:
安装
1
pnpm add tslib -D
mock 后端服务文件夹中
tsconfig.json
增加
1
2
3
4
5
6
7
8
{
"compilerOptions": {
...
"paths": {
"tslib": ["node_modules/tslib/tslib.d.ts"]
},
}
}报错
1
Cannot find module 'C:\myWeb\vue3-vite-uniapp\node_modules\faker\index.js'. Please verify that the package.json has a valid "main" entry
解决方案:卸载最新版本重新安装指定版本
1
2
3
4
5
pnpm remove faker
pnpm remove @types/faker
pnpm add faker@5.1.0
pnpm add @types/faker@5.1.5
第十步:配置 jest 单元测试
安装
1
pnpm add -D @babel/core @babel/preset-env @testing-library/jest-dom @types/jest @vue/test-utils@next babel-jest@26.6.3 ts-jest@26.5.6 vue-jest@next jest@26.6.3
项目根目录创建
jest.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const path = require('path')
module.exports = {
rootDir: path.resolve(__dirname),
clearMocks: true,
coverageDirectory: 'coverage',
coverageProvider: 'v8',
moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'],
// 别名设置 src下components里组件 <rootDir>/src/components/$1
moduleNameMapper: {
'@/(.*)$': '<rootDir>/src/$1',
},
preset: 'ts-jest',
testEnvironment: 'jsdom',
// 测试文件 自己写的
testMatch: ['<rootDir>/tests/unit/*.spec.ts?(x)'],
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\js$': 'babel-jest',
'^.+\\.(t|j)sx?$': 'ts-jest',
},
}配置
tsconfig.json
1
2
3"compilerOptions": {
"types": ["@dcloudio/types", "jest"],
}项目根目录中创建实例
tests/unit/HelloWorld.spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import { mount } from '@vue/test-utils'
// 导入需要测试页面
import HelloWorld from '@/components/counter/counter.vue'
import { useCounterStore } from '../request'
test('add stores count', async () => {
// 获取整个wrapper
const wrapper = mount(HelloWorld)
// 获取dom: title的text判断是不是'0'
expect(wrapper.find('.title').text()).toBe('0')
// 触发按钮点击
await wrapper.find('.button').trigger('tap')
const acount = useCounterStore()
expect(acount.count).toBe(1)
// 再获取dom: title的text判断是不是'1'
expect(wrapper.find('.title').text()).toBe('1')
})package.json
添加命令1
2
3
4
5
6
7{
"scripts": {
...
"test": "jest",
"test:unit": "jest --coverage"
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21pnpm test
// 输出结果
> uni-preset-vue@0.0.0 test C:\myWeb\uniapp_vue3_vite_pinia
> jest
PASS tests/unit/HelloWorld.spec.ts
PASS tests/unit/testIndex.spec.ts (9.762 s)
● Console
console.log
111
at Object.<anonymous> (tests/unit/testIndex.spec.ts:34:11)
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 13.767 s, estimated 19 s
Ran all test suites1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33pnpm test:unit
// 输出结果
> uni-preset-vue@0.0.0 test:unit C:\myWeb\uniapp_vue3_vite_pinia
> jest --coverage
PASS tests/unit/testIndex.spec.ts
● Console
console.log
111
at Object.<anonymous> (tests/unit/testIndex.spec.ts:34:11)
PASS tests/unit/HelloWorld.spec.ts
------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files | 97.11 | 100 | 40 | 97.11 |
src/components/counter | 100 | 100 | 100 | 100 |
counter.vue | 100 | 100 | 100 | 100 |
src/stores | 96.55 | 100 | 50 | 96.55 |
counter.ts | 96.15 | 100 | 50 | 96.15 | 12
index.ts | 100 | 100 | 100 | 100 |
tests | 95 | 100 | 0 | 95 |
request.ts | 95 | 100 | 0 | 95 | 20,26
------------------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 9.287 s, estimated 10 s
Ran all test suites.提示
执行查询覆盖命令后同时会在项目根目录创建coverage 文件夹,
coverage/lcov-report/index.html
可以网页UI模式查看。
你可能遇到错误请参考如下解决方案
报错
1
Test environment jest-environment-jsdom cannot be found.
原因:由于 jest 版本不对
解决方案:安装
1
pnpm add jest-environment-jsdom -D
报错
1
TypeError: Cannot destructure property 'config' of 'undefined' as it is undefined.
原因:同上,由于 jest 版本不对
解决方案:
1
2
3
换 "jest": "^26.6.3"
换 "ts-jest": "^26.5.6"
换 babel-jest@26.3.0
第十一步:挂载 uni-ui
安装
1
pnpm add @dcloudio/uni-ui
项目根目录创建并配置
vue.config.js
1
2
3
4// vue.config.js
module.exports = {
transpileDependencies: ['@dcloudio/uni-ui'],
}easycom 配置
src/pages.json
1
2
3
4
5
6
7
8
9
10
11{
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"pages": []
...
}提示
easycom规则例子:
组件文件夹参考此路径创建组件
src/components/uni-nav-bar/uni-nav-bar.vue
页面则不需要
component
中导入直接
<template>
书写即可:<uni-nav-bar></uni-nav-bar>
第十二步:HBuildX 配置小程序渲染
manifest.json
点击后基础配置–> uni-app 应用标识(AppID):注册dcloud自动生成基础配置–>微信小程序配置–>微信小程序AppID
工具–>设置–>运行配置–>微信开发者工具路径,填入自己安装的路径
微信开发工具—设置–通用设置–安全–服务端口(最好去安装目录里启动后操作)
提示
- 生成后项目根目录会创建
.hbuilderx
文件夹- 如果要用手机直接预览,需要去把
src/manifest.json
中"scope.userLocation": { "desc": ""}
注释哈,uni-app默认把地域调用打开了。但是初始项目未调用微信地域 api- 如果是用 cli 命令启动项目,微信开发工具中需要导入最后打包成功的文件夹而不是整个源码文件,不然会报错 无法找到
app.json
, 例如dist/dev/mp-weixin
第十三步:配置多环境 api 接口
src/config
文件夹分别创建.env.dev.ts
、.env.pre.ts
、.env.pro.ts
、.env.test.ts
、env.ts
.env.x.ts
内容就是不同环境将要访问的 api 地址1
2
3
4
5export default {
// 也就是你不同的环境将要访问的不同的服务器
BASE_API: 'http://localhost:3300',
ENV: 'dev',
}env.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import dev from './.env.dev'
import test from './.env.test'
import pre from './.env.pre'
import prod from './.env.pro'
// import.meta.env 这是vite环境变量
const NODE_ENV = import.meta.env.MODE
console.log('NODE_ENV----', NODE_ENV)
let ENV_VAR: { BASE_API: string } = { BASE_API: 'dev' }
if (NODE_ENV === 'dev' || NODE_ENV === 'development') {
console.log('开发---')
ENV_VAR = dev
} else if (NODE_ENV === 'test') {
console.log('测试---')
ENV_VAR = test
} else if (NODE_ENV === 'pre') {
console.log('预发布---')
ENV_VAR = pre
} else if (NODE_ENV === 'pro' || NODE_ENV === 'production') {
console.log('生产---')
ENV_VAR = prod
}
// else if (NODE_ENV === 'demo') {
// ENV_VAR = demo
// }
export default ENV_VARpackage.json
中增加命令这里增加即是为了让
import.meta.env.MODE
拿到对应的值1
2
3
4
5
6"scripts": {
...
"dev:weixin": "uni -p mp-weixin --mode dev",
"test:weixin": "uni -p mp-weixin --mode test",
"build:weixin": "uni build -p mp-weixin --mode pro"
}api 接口常量文件
src/config/app.ts
获取env.ts
返回的不同环境 api 地址1
2
3
4import ENV_CONFIG from '@/config/env'
console.log('不同环境不同地址---', ENV_CONFIG)
// mock请求地址 'http://localhost:3300'
export const HTTP_REQUEST_URL = ENV_CONFIG.BASE_API启动项目
1
pnpm dev:weixin
参考文章[8]
😫小波搭建uni-app微信小程序APP工程化项目遇见的一些问题
报错
vue cli3中eslint报错“no-undef“和eslint规则配置
解决方案:
.eslintrc.js
中rules
增加如下代码
1
2
3
rules: {
'no-undef': 0,
}
报错
[@vue/compiler-sfc] type argument passed to defineProps() must be a literal type, or a reference to an interface or literal type.
原因及解决方案:因为组件中
defineProps
不支持外部导入的ts申明,只能写在组件页面中
报错
Type '{}' is not assignable to type '(props: Readonly) => object'. Type '{}' provides no match for the signature '(props: Readonly): object'.
当使用基于类型的声明时,我们失去了为
props
声明默认值的能力。这可以通过withDefaults
编译器宏解决:解决方案:
数组,对象需要用函数返回
1
2
3
4
5
6
7
8
9
10
11
12
13
goods: () => [
{
id: 6905,
name: '',
icon: '',
sort: 1,
is_show_backstage: 0,
goods_list: [],
},
],
computedMenuCartNum: () => {
return 0
},
报错
Set "volar.inlayHints.eventArgumentInInlineHandlers": false to hide Event Argument in Inline Handlers.
上面翻译过来就是:设置
“volar.inlayHints。eventArgumentInInlineHandlers": false
在内联处理程序中隐藏事件参数。解决方案:
vscode 的
setting.json
中添加
1
"editor.inlayHints.enabled": "off"
报错
Component is not found in path "pages/menu/hooks/useNav" (using by "pages/menu/index")
原因及解决方案:uniapp 如果在页面文件夹引入子 hooks 组件,则只能用
menuHook/index.vue
模式, 不然报错
报错
JSON 中不允许有注释。
解决方案:右下角的
json
点一下,然后输入comment
,下拉选择json with comments
就可以了
报错
微信开发工具启用手机预览编译后报错小程序按需注入
解决方案:
manifest.json
中增加以下代码
1
2
3
4
"mp-weixin": {
// 小程序按需注入
"lazyCodeLoading": "requiredComponents"
}
😚直接克隆项目后安装步骤
pnpm install
后, 用 HBuildX 启动(选择项目–>运行–>运行到小程序模拟器) 或者pnpm dev:mp-weixin
- 微信开发工具勾选 不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书
😊来自小波的bilibili视频教程
- 安装搭建项目如果遇到什么问题,请留言,小波会尽快回复。
- 小波主参考 Cobyte 大大写的文章[3]。万分感谢 !
🙂小波用到的相关参考资料链接
『旅行者』,帮小波关注一波公众号吧。
小波需要100位关注者才能申请红包封面设计资格,万分感谢!
关注后可微信小波,前66的童鞋可以申请专属红包封面设计。
微信
支付宝