2024前端项目模板集合(一):Taro? Uniapp? 什么才是小程序的2024最佳工程实践呢?!(内有完整模板请收藏)

5,370 阅读22分钟

前言

在2023年末的这个节点,聊起小程序开发,离不开当前市场占有率最高的两个框架:Taro和Uniapp,那么到底哪个框架更符合当前主流开发,更好,更易用呢?具体的工程该怎么搭建呢?

在回答上诉问题之前,我们先来讲一个故事,并且回答几个问题。

如果太长不看可以直接拉到后面享用模板!

很久很久以前,一个公司要面向用户提供某项业务,得做一个app。开发维护的成本都极其高昂。 后来微信公众号出现,以公众号为流量入口,h5作为业务载体也风靡一时。但是h5毕竟有它的局限性:性能和体验问题使得它始终不能承载太复杂的业务,中小公司还是需要一个app。

后来,由于微信公众号已经承接不了蓬勃发展的需求了,小程序在历史滚滚长河中登场。 它的出现解决了很多掣肘公众号+h5的问题,例如:

  • 性能好,提供了相当原生的用户体验,加载快。
  • 提供wx-sdk,也就具备获取微信提供的泛能力,例如转发、分享等、支付扫一扫等。
  • 依托微信的用户体系和生态。

简单来说就是,微信搞了一个基础,大家在上面做开发可以又吃又拿。在流量池子里玩,别人还给你铲子,那就没什么理由不搞一搞小程序了。

当时几乎每家公司都把自己原来由h5和app承接的业务转移到小程序上,大树底下好乘凉。 但是由于小程序的处于安全和性能考虑的特殊架构,使得一开始在没有小程序框架出现前,小程序的开发体验不是特别好,市面上出现了专门的小程序开发工程师,那这不就是又变相提高了开发成本?

于是Taro、Weapp、Uni-app、Mpvue、Remax等小程序框架出现,但是经过这几年的大浪淘沙,基本就剩下Taro和Uni-app两家了。

一、小程序基础架构

小程序到底是怎么架构的,为啥他的原生开发有如此难用的语法和开发体验?

下图是小程序的架构:

image.png image.png 摘一下官方文档:

​网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。网页开发者可以使用到各种浏览器暴露出来的 DOM API,进行 DOM 选中和操作。而如上文所述,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。这一区别导致了前端开发非常熟悉的一些库,例如 jQuery、 Zepto 等,在小程序中是无法运行的。同时 JSCore 的环境同 NodeJS 环境也是不尽相同,所以一些 NPM 的包在小程序中也是无法运行的。

小程序的架构和普通的浏览器渲染不同,传统的浏览器渲染是单线程的,也就是渲染和逻辑的执行是互斥的,这个大家都学过。但是小程序的架构设计是双线程的设计,页面的渲染和js的逻辑执行是分成两个线程来执行的。同时,为了达到原生app类似的良好体验,小程序的页面的渲染是多页面渲染的(多个webview),可以理解为开了多个浏览器来渲染多个页面,可以参考上图。

那么为什么要使用双线程的架构模式呢? 原因主要有以下几点:

  • 为了很好的注入WXSDK
  • 为了确保安全

WXSDK就是上图和前文所阐述的微信的一些jsApi的集合,提供了微信的丰富原生能力和一些内部的方法。 那这些sdk在传统浏览器的架构中,得通过网络请求的方式去加载,这非常影响用户体验,甚至会出现白屏的情况。而在微信公众号的解决方案是做了离线缓存,一个公众号有一定容量的缓存大小。这样做是改善了用户体验,但是会造成微信的缓存越来越多,想起微信动辄几十个g的空间占用,是不是更加蛋疼了。 所以为了解决这些问题,小程序的出现必将采取新的架构,也就是在native层中往页面动态注入sdk来实现,也就是one weixin one sdk,这样是不是很爽。 当前,不只是sdk,还有底层基础库Service等很多服务都是事先放在Native层中的。

解决SDK的问题,那怎么确保安全呢? 我们知道,js是非常开发和灵活的,与此同时带来的是相对的不安全性。而微信和浏览器端不一样,微信对于安全的要求是要更高一级的,例如操作dom(就存在可能变相获取到用户的敏感数据),网页的跳转等,执行动态脚本等。这些都是不受控的,而微信又希望控制这些过程,以达到安全的目的。于是,就产生了封装一个沙箱来运行js,而不是在webview中运行js的理念。于是在ios采用专门jscore,在安卓上采用x5内核来执行js,也就是逻辑层,就能对js的灵活进行一定的管控。

以上,就是双线程架构的由来,而双线程架构能够使得逻辑的归逻辑,渲染的归渲染,各司其职,安全且原生,看起来是不是十分完美。

但是!

什么都有个但是!

就苦了我们这帮苦哈哈的前端-马上被大厂裁员—或者被ai淘汰—程序员。

小程序的原生开发因此就有了和开发传统webapp不一样的地方,不管是语法还是生态,写起来,真是不太得劲。

直到了英特奈熊-taro 和uni-救世主-app的出现,使得大家能够采用习惯的vue和react语法以及相关的生态来进行小程序的开发。下面我们就就正式展开,如何使用Taro和Uniapp搭建一个规范的前端项目工程。

二、Uniapp篇

我们先来快速搭建一个uniapp的前端工程,该工程采用vue3+vite技术栈,使用uview组件库,使用vscode开发,tyepscript 封装并且使用如下项目规范的工具:

  • eslint: 负责校验代码
  • prettile: 格式化代码
  • stylelint: 校验和格式化样式代码
  • commitlint:校验git提交
  • lint-staged: 只对当前更改的进行校验
  • husky:在提交前做所有的校验

1.初始化项目

//根据uniapp官方文档,快速创建一个vue3+vite项目,并使用vscode开发
npm install -g @vue/cli
npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project

2.使用lint工具链来保证代码风格和质量

为什么要确保代码风格的统一?

在多人的项目开发过程中,代码风格的统一非常重要,因为代码不仅是给机器阅读的,同时也是给人阅读的。为了避免不同风格的代码导致的满屏红和难以阅读维护,同时确保代码的质量,需要一定的规范去约束。

如何统一代码风格和质量?

通过如:eslint prettier stylelint commitlint tsc等主流lint的工具的使用。配合husky lintstaged vscode插件来实现自动化的代码规范的检查和格式化,把语法错误,和低质量代码在开发阶段规避掉。人只需要关注开发逻辑本身,这就是lint的工具链的意义。

2.1 Eslint的使用

简介

ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误。

说人话就是,搞一个配置文件,里面有不同的规则(rules),eslint通过这个配置文件来来检验代码的规范是否符合要求。现在常见的规则如:Airbnb JavaScript 代码规范Standard JavaScript 规范Google JavaScript 规范等等,大部分情况下,直接采用以上开发规范就好了。

开始干!先安装eslitnt!

注意:全文的安装包都带有版本号,如想用最新版本会存在版本不兼容导致校验失败的问题。

yarn add eslint@8.55.0 -D

以及安装如下eslint衍生包

  • @typescript-eslint/eslint-plugin@latest(为了支持TypeScript)
  • @typescript-eslint/parser@lates(为了支持TypeScript)
  • eslint-plugin-vue@latest(为了支持Vue语法的解析)
  • @vue/eslint-config-typescript(Vue官方提供的在Vue项目中使用 TypeScript 时的代码规范检查)
yarn add eslint-plugin-vue@9.19.2 @typescript-eslint/eslint-plugin@6.14.0 @typescript-eslint/parser@6.14.0 @vue/eslint-config-typescript@12.0.0 -D

在vscode中安装插件prettier和eslint

image.png

image.png

然后在项目的根目录新建一个.eslintrc.js,然后把以下热乎乎的eslint配置粘贴进去。

注意:下面的配置和上面安装的衍生包是对应的,因为extends中用的别人的配置,所以就要有相应的包来支持,不然就会出现配置不生效的问题。如果要自定义配置,请注意这一点。

//.eslintrc.js
module.exports = {
  root: true, // 表示这是项目的根 ESLint 配置。
  env: {
    node: true, // 指定环境为 Node.js。
  },
  //extends 用于继承其他配置的属性。你可以基于已有的配置来构建自己的配置,避免从头开始定义所有的规则。也就是抄别人的成熟模板~
  extends: [
    "plugin:vue/vue3-essential", // Vue3 的基本配置规则。
    "eslint:recommended", // 推荐的 ESLint 配置规则。
    "@vue/typescript/recommended", // Vue中 TypeScript 的推荐配置规则。
    "@vue/prettier", // 扩展 Prettier 的 ESLint 配置规则。
    "@vue/prettier/@typescript-eslint", // 扩展 TypeScript 特定规则的 Prettier ESLint 配置。
  ],
  //parserOptions是解析器选项,对eslint语法解析器的能力进行定制
  parserOptions: {
    ecmaVersion: 2020, // 指定要使用的 ECMAScript 版本(在这种情况下为 ES2020)。
    ecmaFeatures: {
      jsx: true, // 启用 JSX 解析。
    },
  },
  //rules-这里就是具体的规则配置拉
  rules: {
    "prettier/prettier": "error", // 强制执行 Prettier 规则,如果不遵循则生成错误。
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", // 在开发中允许 console.log,在生产中警告。
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", // 在开发中允许 debugger,在生产中警告。
    "@typescript-eslint/no-unused-vars": [
      "error",
      {
        vars: "local", // 强制局部变量使用。
        args: "none", // 不允许未使用的函数参数。
        varsIgnorePattern: "usePresenter|model|reactive", // 在未使用变量检查中忽略特定变量名。
        caughtErrors: "none", // 不允许未使用的捕获的错误。
      },
    ],
    "@typescript-eslint/no-explicit-any": 2, // 不允许使用 any 作为类型声明。
    eqeqeq: 2, // 强制使用严格相等(=== 和 !==)。
    "max-lines": ["error", 800], // 强制文件的最大行数。
    complexity: ["error", 20], // 强制函数的最大圈复杂度。
    "require-await": "error", // 强制异步函数有 await 表达式。
    "vue/multi-word-component-names": "off", // 禁用 Vue 文件中多词组件名称的规则。
    "@typescript-eslint/no-empty-function": 1, // 警告空函数。
    "no-shadow": "error", // 强制变量名不与其作用域链中的变量名重复。
    "@typescript-eslint/ban-types": "off", // 禁用对特定类型的禁令。
    "@typescript-eslint/no-non-null-assertion": "off", // 禁用对非空断言的禁令。
    "vue/valid-v-model": "off", // 禁用 Vue 文件中 v-model 使用的规则。
  },

  overrides: [
    {
      files: [
        "**/__tests__/*.{j,t}s?(x)", // 对 __tests__ 目录下的文件进行覆盖。
        "**/tests/unit/**/*.spec.{j,t}s?(x)", // 对单元测试文件进行覆盖。
      ],
      env: {
        mocha: true, // 指定 Mocha 测试环境。
      },
    },
  ],
};

同样在根目录配置一个.eslintignore来忽略检测的文件

.eslintignore
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
Dockerfile
/types

配置完.eslintrc.js之后,就完事了吗? 搜德麻跌! 这个配置文件中这个prettier是什么意思?

ok,术业有专攻,eslint可以做两件事:

  • 代码的规范和质量的检查和提示,例如使用未声明的变量、修改const变量、字符串是否是双引号等
  • 按照规范格式化这些不符合要求的代码

prettier在第二件事中,做的比eslint好。因为除了js/ts外,prttier还支持对多种语言进行格式化,如vue、html、css、less、scss、json、jsx等等,是一个比较综合的代码格式化工具,且lint的速度更快。于是,我们一般把格式化这件事情,交给prettier去做。

2.2 prettier和eslintの故事

首先我们安装一下prettier

yarn add prettier@2.7.1 -D

在项目根目录下新建.prettierrc.js配置文件,然后再把下面第二份热乎乎的配置粘贴进去,并按照你们的团队规范自定义修改

// .prettierrc.js
module.exports = {
  printWidth: 80, //一行的字符数,如果超过会进行换行,默认为80
  tabWidth: 2,  // 一个 tab 代表几个空格数,默认为 2 个
  useTabs: false, //是否使用 tab 进行缩进,默认为false,表示用空格进行缩减
  singleQuote: false, // 字符串是否使用单引号,默认为 false,使用双引号
  semi: true, // 行尾是否使用分号,默认为true
  trailingComma: "all", // 是否使用尾逗号
  bracketSpacing: true, // 对象大括号直接是否有空格,默认为 true,效果:{ a: 1 }
  htmlWhitespaceSensitivity: "ignore",// 在 HTML 中空格的敏感性,"ignore" 表示忽略空格敏感性
  endOfLine: "auto" // 换行符的风格,"auto" 表示自动识别当前操作系统的换行符风格
};

接下来我们把prettier和eslint一起打配合,各司其职。

细心的朋友发现了,为啥eslint一套规范,prettier一套规范,到底听谁的?不冲突吗?

所以我们得安装如下两个包,来确保校验的归eslint,格式化的归prettier,不能打架!

  • @vue/eslint-config-prettier(用来覆盖eslint本身的规则配置)
  • eslint-plugin-prettier(让prettier来接管eslint --fix也就是代码修复格式化的能力)
yarn add @vue/eslint-config-prettier@6.0.0 eslint-plugin-prettier@4.2.1 -D

写到这,再回顾之前写的.eslintrc.js的配置中,涉及prettier的部分代表什么意思了。

//.eslintrc.js
module.exports = {
  ...
  extends: [
    ...
    "@vue/prettier", // 扩展 Prettier 的 ESLint 配置规则。
    "@vue/prettier/@typescript-eslint", // 扩展 TypeScript 特定规则的 Prettier ESLint 配置。
  ],
  //rules-这里就是具体的规则配置拉
  rules: {
    "prettier/prettier": "error", // 强制执行 Prettier 规则,如果不遵循则生成错误。
    ...
  }
};

我们在package.json中定义一个脚本,然后执行 yarn run lint 就可以通过命令行的方式格式化代码了。

{
  "scripts": { 
    ...
    "lint": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./src", }
}

在开发阶段,我们还可以通过vite插件在开发阶段进行扫描,以命令行的方式展示出代码中的规范问题,并直接定位到源文件。如下图:

image.png

先安装插件

 yarn add  vite-plugin-eslint -D

然后再vite.config.js中进行配置


import viteEslint from "vite-plugin-eslint"; //1.引入

export default defineConfig({
  plugins: [...省略其他插件, viteEslint()], //2.使用
});

这样重新运行项目,开发时就会有上图的提示了。

2.3 样式的规范-stylelint

先来一个官网的说明

Stylelint 是一个强大、先进的 CSS 代码检查器(linter),可以帮助你规避 CSS 代码中的错误并保持一致的编码风格。 Stylelint 的强大源于:

  • 拥有超过 100 条内置规则 来检查最新的 CSS 语法和功能
  • 支持 插件 以创建你自己的规则
  • 自动 修复 大多数代码格式上的问题
  • 支持创建或扩展 可共享的配置
  • 可定制,让其而符合你的需求
  • 经过 15000 多次的单元测试,保证其健壮性
  • 被大型公司所采用,例如 Google 和 GitHub

我们建议在使用 Stylelint 的同时使用格式化工具 Prettier。代码检查工具和格式化工具是互相补充的,能够辅助你编写一致且正确的代码。

同样,根据官网所说,stylelint也是一个代码检查工具,专注于样式的检查,格式化的部分还是交给prettier来完成。

先安装依赖

  • stylelint
  • stylelint-prettier(stylelint插件,用于集成Prettier格式化工具
  • stylelint-config-prettier(禁用了stylelint中与Prettier冲突的规则
  • stylelint-config-standard(基于stylelint的预定义配置,包含了一系列的标准化的规则和推荐配置
  • stylelint-config-clean-order(它提供了一套规则,用于指定CSS属性排序的顺序
yarn add stylelint@13.2.1 stylelint-prettier@1.1.2 stylelint-config-prettier@8.0.1 stylelint-config-standard@20.0.0 stylelint-config-clean-order@2.0.0 -D

然后再根目录下新建文件.stylelintrc.js,把以下配置粘贴进去。

这个配置文件指定了 stylelint 的配置选项,具体解释见注释。通过这些配置,stylelint 将遵循 Standard 规范的代码规则,并与 Prettier 格式化工具兼容,同时禁用了一些特定的警告和错误。

module.exports = {
  processors: [], // 指定要使用的处理器,这里为空数组,表示不使用任何处理器
  extends: [
    "stylelint-config-standard", // 继承了 stylelint-config-standard 配置,即使用了 Standard 规范的代码规则
    "stylelint-prettier/recommended", // 继承了 stylelint-prettier/recommended 配置,即使用了与 Prettier 推荐配置兼容的 stylelint 规则
    "stylelint-config-prettier", // 继承了 stylelint-config-prettier 配置,用于确保 stylelint 和 Prettier 规则的一致性
    "stylelint-config-clean-order", // 继承了 stylelint-config-clean-order 配置,用于定义 CSS 属性的排序规则
  ],
  rules: {
    "prettier/prettier": true, // 启用 prettier/prettier 规则,确保代码与 Prettier 的格式化规则一致
    "at-rule-no-unknown": null, // 禁用对未知@规则的警告或错误
    "no-empty-source": null, // 禁用对空样式表的警告或错误
    "unit-no-unknown": null, // 禁用对未知单位的警告或错误
    "no-descending-specificity": null, // 禁用对特异性下降的警告或错误
    "selector-pseudo-class-no-unknown": null, // 禁用对未知伪类的警告或错误
    "declaration-block-no-duplicate-properties": null, // 禁用对重复属性的警告或错误
    "selector-type-no-unknown": null, // 禁用对未知选择器类型的警告或错误
    "block-no-empty": null, // 禁用对空块的警告或错误
    "font-family-no-missing-generic-family-keyword": null, // 禁用对缺少通用字体系列关键字的警告或错误
    "declaration-block-no-shorthand-property-overrides": null, // 禁用对简写属性覆盖的警告或错误
  },
};

我们在 package.json 中,增加如下的 scripts 配置:

{
  "scripts": {
    // ...省略其他配置
    // stylelint 命令
    "lint:style": "stylelint --fix \"src/**/*.{css,scss}\""
  }
}

执行yarn run lint:style即可完成样式代码的规范检查和自动格式化。

在 VSCode 中安装Stylelint插件,这样能够在开发阶段样式代码问题,见下图。

image.png

image.png

同样,vite也有stylelint插件支持,可以实现在终端提示样式问题,如下图

image.png 安装vite-plugin-stylelint

yarn add vite-plugin-stylelint -D //支持vite>=3.0

然后再vite配置文件中配置

import viteStylelint from 'vite-plugin-stylelint';

{
  plugins: [
    // ...其他插件
    viteStylelint({
      //排除node_modules的文件
      exclude: /node_modules/
    }),
  ]
}

到此就可以在开发阶段实现stylint的格式检查和格式化了。

2.4 在vscode保存时自动格式化

在前面的配置已经实现了对规范的检查和格式化,但是格式化的操作只能通过执行命令行来进行,例如:yarn lint。

这样操作十分麻烦,有没有办法让我们每一次保存文件的时候,都自动格式化代码呢?答案是肯定的。 我们做如下的配置。

在根目录条件.vscode文件夹,并新建settings.json文件。

把如下配置粘贴进去,就可以实现保存自动格式化了。

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": "explicit"
  },
  "stylelint.validate": ["css", "less", "scss", "vue"],
  "[vue]": {
    "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  }
}

3.封装路由

1.1 独特的小程序路由

前文提到,小程序不是一个spa应用,每打开一个页面相当于打开一个新的webview,这样做的好处是可以给用户带来类似于App的体验,但由此给我们开发所带来的影响就是,小程序的路由写法和常规Vue项目有很大的不同,具体如下:

常规Vue项目路由写法:

1.定义: image.png 2.使用

<router-link :to="{ name: 'user', params: { username: 'erina' }}">User</router-link>

router.push({ name: 'user', params: { username: 'erina' } })

小程序路由的写法

1.在src目录的pages.json中去定义路径 image.png

2.使用如下api去做跳转和参数传递

  • switchTab:跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面

  • reLaunch: 关闭所有页面,打开到应用内的某个页面

  • redirectTo: 关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面

  • navigateTo: 保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。

  • navigateBack: 关闭当前页面,返回上一页面或多级页面。

uni.navigateTo({
 url: '/abc?customerId=1'
})

3.vue3的uniapp小程序的参数接收

//方式1
const props = defineProps({
  customerId: String,
});

//方式2
import { onLoad } from "@dcloudio/uni-app";
onLoad((options) => {
   console.log(options?.customerId);
});

两者之间存在的很多的不同,例如:

  • 路由的定义不同
  • 参数的传递不同
  • 路由的导航守卫
  • 路由的嵌套
  • 路由别名
  • ...

1.2 实践

那为了和之前的vue开发习惯保持一致,或者是以更轻松的方式去写路由:

有两种方案:

  • 简易封装:如果项目比较轻量,那么可以自己进行简易封装
  • 第三方插件:采用成熟的第三方插件,如vue-simple-router;
1.2.1简易封装

目标:

1.简化写法,有类型别名

由uni.navigateTo({ url: '/pages/index/index' })变成router.navigate('index')

2.有类型提示

如果别名错误有提示: image.png

如果传参错误有提示:

image.png

3.传参优化

//原生传参方式
 uni.navigateTo({
   url: "/pages/recordLedView/index?fromType=customer",
 });
 
 //封装后
 router.navigate("recordLedView", {
   fromType: "customer",
   customerId: model.customerDetail.value?.id,
 });

原来传参方式需要把参数拼接到地址后面,如果遇到复杂的数据,需要做特殊的处理;且取值需要在onLoad钩子里或者defineProps中取,比较麻烦。

4.参数的获取优化

优化点1:如果从A页面跳转到C页面,C页面需要A页面传递给B页面的参数projectId,那么C页面还要再传递一次。

sequenceDiagram
A页面->>B页面: navigateTo projectId=1
B页面-)C页面: navigateTo projectId=1

优化点2:在有一些场景中需要获取路由跳转后携带的数据,如某个页面去选择项目,选择完毕后返回页面把项目id带回。

sequenceDiagram
A页面->>B页面: 去选择项目
B页面-->>A页面: 返回携带项目Id projectId=1

实践:

在src目录下新建router文件夹,并新建三个文件,index.ts\type.ts\page.ts

image.png

在page.ts中编写路由,直接复制即可(当然,前提是你得在pages.json文件也写了路由)

// 主包
const mainPackage = {
  index: "/pages/index/index",
  mine: "/pages/mine/index",
};

// 分包
const subPackage = {};

const pages = {
  ...mainPackage,
  ...subPackage,
};
export default pages;

在type.ts中定义不同页面的参数

export interface pageA {
  fromType: string;
  houseId?: string;
  houseName?: string;
  customerId?: string;
  projectId?: string;
}

export type RoutePramasMap = {
  pageA: pageA;
};

在index.ts中引入路由和参数类型

import { RoutePramasMap } from "./types";
import pages from "./pages";
type PageNames = keyof typeof pages;//定义类型别名

然后再index.ts中重新封装 getRouteParams navigate redirect reLaunch switchTab back方法

image.png

要解决上述的痛点,其实本质是做了几件事情

  • 定义一个全局的对象,以路由名称做为键,在路由跳转方法navigate内存储,在跳转后的页面使用getRouteParams方法读取
  • 返回参数的获取是利用了uni的事件传递,在navigate方法中使用uni.$once注册事件,然后在返回方法back中触发事件并传递参数
  • 类型的提示的具体实现

index.js的详细实现如下:

import { RoutePramasMap } from "./types";
import pages from "./pages";
import { readonly } from "vue";
type PageNames = keyof typeof pages;

type ObjectType<T> = T extends keyof RoutePramasMap ? RoutePramasMap[T] : never;

const routeStore = {} as Record<PageNames, unknown>;

export function getRouteParams<T extends PageNames>(page: T) {
  console.log(routeStore, "routeStore");
  const p = routeStore[page] as ObjectType<T>;
  return readonly(p);
}

let navigateLock = false;
function navigate<T extends PageNames>(
  page: T,
  params?: ObjectType<T>,
  event?: string,
) {
  if (navigateLock) return Promise.reject();
  const routePath = pages[page];
  const eventName = event || routePath; // 生成唯一事件名,使用页面地址作为事件名
  navigateLock = true;
  routeStore[page] = params;
  console.log(page, "routePath", params, "params");
  uni.navigateTo({
    url: `${routePath}?eventName=${eventName}`,
    complete() {
      navigateLock = false;
    },
  });

  return new Promise((resolve, reject) => {
    //eslint-disable-next-line @typescript-eslint/no-explicit-any
    uni.$once(eventName, (data: any) => {
      resolve(data);
    });
  });
}

function redirect<T extends PageNames>(page: T, params?: ObjectType<T>) {
  routeStore[page] = params;
  uni.redirectTo({ url: pages[page] });
}

function reLaunch<T extends PageNames>(page: T, params?: ObjectType<T>) {
  routeStore[page] = params;
  uni.reLaunch({ url: pages[page] });
}

function switchTab<T extends PageNames>(page: T, params?: ObjectType<T>) {
  routeStore[page] = params;
  uni.switchTab({ url: pages[page] });
}

interface BackParams {
  /** 返回页面层级 */
  delta?: number;
  /** 返回携带的数据 */
  //eslint-disable-next-line @typescript-eslint/no-explicit-any
  data?: any;
  event?: string;
}

function back({ delta, data, event }: BackParams = { delta: 1, data: null }) {
  const currentRoute = getCurrentPages().pop();
  if (!currentRoute) return;
  const eventName = event || "/" + currentRoute.route;
  uni.$emit(eventName, data);
  uni.navigateBack({
    delta,
  });
}

const router = {
  navigate,
  redirect,
  reLaunch,
  switchTab,
  back,
};

export default router;

这样就完成了page.ts、type.ts和index.ts的编写,按如下方式使用即可

const routeBackData= await router.navigate("pageA", {
  fromType: "customer",
  customerId: 1,
 });
};

//routeBackData就是下方返回方法中返回的参数:{id:1,name:'abc'}

router.back({
   delta: 1,
   data: { id: 1, name: 'abc' },//返回携带的参数
});
1.2.2第三方插件

如果项目复杂度高,那你可能需要一个第三方的成熟路由插件。 本文对uniapp市场上的路由插件做了对比,有两款插件符合基本需要。可以满足路由拦截,路由重命名、路由嵌套等功能,且使用vue路由的写法。

  • uni-simple-router
  • uniapp-router-next

我们分别对比两款插件,添加到我们项目中

uni-simple-router

该插件的简介和使用见github文档,不再赘述。uni-simple-router在vue2版本被使用的比较多,在vue3版本开始收费.

Vue2版本github: github.com/SilurianYan…

Vue3版本官方文档:www.hhyang.cn/blog/functi…

优点: uni-simple-router在uni-app插件市场中的路由插件是点赞收藏数最多的,github上的点赞数也到达了7百多赞,代表该插件受到市场的认可。

缺点: 收费。uni-simple-router有两个版本,一个是v3版,一个是v2版,区别如下图。但是v3版本是收费的,价格还不菲,可根据具体情况酌情使用。

image.png

uniapp-router-next

gitee地址:www.npmjs.com/package/uni…

uniapp-router-next是一个类似于vue-router的路由器,用于uniapp(vue3),支持h5和微信小程序和app,其他小程序请自测

先安装

npm install uniapp-router-next
npm install unplugin-uni-router -D

在vite.config.ts中引入

//vite.config.ts
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import uniRouter from 'unplugin-uni-router/vite'

export default defineConfig({
  plugins: [uni(), uniRouter()]
})

在src目录下新建router.ts,并把如下代码粘贴进去

import routes from 'uni-router-routes' //由unplugin-uni-router/vite根据pages.json生成
import { createRouter } from 'uniapp-router-next'
const router = createRouter({
  routes: [
    ...routes,
    // 通配符,一般用于匹配不到路径跳转404页面
    {
      path: '*',
      redirect: () => {
        // 可返回{ name: '404' },{ path: '/pages/404/404' }, '/pages/404/404'
        return { name: '404' }
      }
    }
  ],
  //@ts-ignore
  platform: process.env.UNI_PLATFORM,
  h5: {}
})
export default router

在main.ts中导入

import { createSSRApp } from 'vue'
import router from './router'
import App from './App.vue'
export function createApp() {
  const app = createSSRApp(App)
  app.use(router)
  return {
    app
  }
}

全局注册

{
     "easycom": {
        "custom": {
            "router-navigate": "uniapp-router-next/components/router-navigate/router-navigate.vue"
        }
    }
  }

使用基本和vue就差不多了,跳转,导航守卫,路由命名等。

import { useRouter } from 'uniapp-router-next'

const router = useRouter()

//1.跳转
router.navigateTo({
  path: '/pages/index/index',
  //参数
  query: {
    name: 'name'
  }
})
router.navigateTo...
router.reLaunch...
router.redirectTo...
router.switchTab..
router.navigateBack...

//2.导航守卫
router.beforeEach(async (to, form, next) => {
  console.log(to, form, 'beforeEach')
})

router.afterEach((to, form) => {
  console.log(to, form, 'afterEach')
})

//3.路由信息
const route = useRoute()
console.log(route)
//   fullPath: '/pages/index/index'
//   meta: {}
//   query: {}  上一个页面的参数
//   path: '/pages/index/home'
//   name: ''

未完待续:pnpm、组件库、全局变量配置、pinia、CI/CD、性能优化、低代码方案...