1万字!uni-app+vue3.2+ts+vite大杂烩实战

1,208 阅读16分钟

介绍

背景

因为经历过公司没有代码规范的野蛮生长时期,自己也是野蛮生长的一员,没有代码规范,代码复用率不高等等等问题,这些问题导致了如今的我们不愿去维护过去的代码,成本和代价太大,时不时还想给过去的自己一个大比兜。所以Typescript类型推断、eslint代码检查工具、prettier代码格式化工具、以及vue3的compositionApi让代码更好的去复用等,让我神往,所以就搭建了一个项目模板,并且用于生产。

文章内容介绍

因为想顺便锻炼自己的写作能力和整理知识能力,所以本文是从项目搭建到uni-app的云打包和本地打包都会介绍。相当于从0开始讲。文章在叙述的时候会反复提示按需引入,因为俺的也不一定是最好的,毕竟是小白,大家主要以灵活为主,在前端百花齐放的年代,大家不必一模一样的使用,能达到最终目的就行,讲究的还是灵活运用。

文章中搭建项目用的包管理工具时pnpm,条件不允许的也可以用yarn和npm。node版本最好大于等于14.18.1。因为只测试过node版本14.18.1到node版本16+

快速构建框架

全局安装vue-cli

如果电脑已安装vue-cli,检查是否是vue-cli 4.x,目前uni-app官方推荐使用vue-cli 4.x。

pnpm install -g @vue/cli@4

使用vue3/vite/ts

# 命令行安装
npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project

若命令行安装失败,可直接到gitee下载模板
运行完命令行后会生成my-vue3-project文件夹,文件夹目录我就不介绍了

tsconfig.json配置

根目录的tsconfig.json文件配置,有其他特殊需求,可按需自行配置

{
    "compilerOptions": {
        "target": "esnext",
        "useDefineForClassFields": true,
        "module": "esnext",
        "moduleResolution": "node",
        "strict": true,
        "jsx": "preserve",
        "sourceMap": true,
        "resolveJsonModule": true,
        "esModuleInterop": true,
        "lib": ["esnext", "dom"],
        "skipLibCheck": true,
        "types": ["@dcloudio/types", "node"],
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        }
    },
    "include": [
        "src/**/*.ts",
        "src/**/*.d.ts",
        "src/**/*.vue",
        "types/**/*.d.ts",
        "types/**/*.ts",
        "build/**/*.ts",
        "build/**/*.d.ts",
        "mock/**/*.ts",
        "vite.config.ts"
    ],
    "exclude": ["node_modules","docs"]
}

vite.config.ts配置

根目录的vite.config.ts文件配置,有其他特殊需求,可按需自行配置

import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
import path from 'path';

export default defineConfig({
    plugins: [uni()],
    resolve: {
        alias: {
            '@': path.resolve(__dirname, './src'),
            '*': path.resolve(''),
        },
    },
});

uni-app页面文件pages,json模块化

背景

我们在写uni-app项目的时候,我们每加一个页面,我们就得到src的pages.json中去注册一个页面,一旦页面过多时维护和复用还有灵活性是个头疼的问题。所以 我觉得将其拆分成模块或者是配置文件我认为是个很好的解决方案。

介绍钩子文件

uni-app自带一个webpack loader钩子文件pages.js,大家可以趁此机会去了解一下webpack的一个核心概念loader。pages.js必须和pages.json文件同级,最终pages.js导出的配置会被pages.json文件导入合并。 因为pages.js要求的是CommonJS规范,直接通过module.exports输出一个钩子函数。但是Common.js规范下的模块导入和导出输出的是值的拷贝,不是值的引用, 所以没法做到热加载 详情可参考官方源码
源码文件路径:uni-app/packages/uni-cli-shared/lib/pages.js

# 源码
const pagesJsonJsFileName = 'pages.js'

function processPagesJson (pagesJson, loader = {
  addDependency: function () {}
}) {
  // 这个就是用来解析pages.js文件路径
  const pagesJsonJsPath = path.resolve(process.env.UNI_INPUT_DIR, pagesJsonJsFileName)
  // fs.existsSync()方法用于同步检查给定路径中是否已存在文件。它返回一个布尔值,该值指示文件的存在。
  // 如果存在pages.jS文件就用引用pages.js导出的模块
  if (fs.existsSync(pagesJsonJsPath)) {
   
    delete require.cache[pagesJsonJsPath]
    const pagesJsonJsFn = require(pagesJsonJsPath)
    if (typeof pagesJsonJsFn === 'function') {
      pagesJson = pagesJsonJsFn(pagesJson, loader)
      if (!pagesJson) {
        console.error(`${pagesJsonJsFileName}  ${uniI18n.__('cliShared.requireReturnJsonObject')}`)
      }
    } else {
      console.error(`${pagesJsonJsFileName} ${uniI18n.__('cliShared.requireExportFunction')}`)
    }
  }
  // 将 subpackages 转换成 subPackages
  if (pagesJson.subpackages && !pagesJson.subPackages) {
    pagesJson.subPackages = pagesJson.subpackages
    delete pagesJson.subpackages
  }

  let uniNVueEntryPagePath
  if (pagesJson.pages && pagesJson.pages.length) { // 如果首页是 nvue
    if (isNVuePage(pagesJson.pages[0])) {
      uniNVueEntryPagePath = pagesJson.pages[0].path
    }
  }
  // pages
  filterPages(pagesJson.pages)
  // subPackages
  if (Array.isArray(pagesJson.subPackages) && pagesJson.subPackages.length) {
    pagesJson.subPackages.forEach(subPackage => {
      filterPages(subPackage.pages, subPackage.root)
    })
  }

  if (uniNVuePages.length) { // 直接挂在 pagesJson 上
    pagesJson.nvue = {
      pages: uniNVuePages.reverse()
    }
    if (uniNVueEntryPagePath) {
      pagesJson.nvue.entryPagePath = uniNVueEntryPagePath
    }
  }
  return pagesJson
}

热加载插件(uni-pages-hot-modules)

在网上找了个热加载的插件uni-pages-hot-modules,用来解决模块化热重载和缓存的问题.此次配置的是vue3版本的模块化,我这边还准备了的vue2项目的模块化,但是今天主讲vue3,vue2版本模块化解决方案可留言或者是私信。

uni-pages-hot-modules做了什么

// 为loader添加新依赖项
loader.addDependency(modulePath)
// 删除原来的模块缓存
delete require.cache[modulePath]
// 重新载入模块,并重新缓存
require(modulePath)

安装

pnpm install uni-pages-hot-modules -S

新建pages.js文件(src目录下和pages.json同级)

// pages.js文件代码
module.exports = {
  easycom: {
    autoscan: true,
    custom: {
      // 使用了uni-ui需要加此配置
      '^uni-(.*)': '@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue',
    },
  },
  pages: [
    // 这些页面文件汇总其实也可以写方法或者脚本检索指定目录下文件进行模块合并,这边为了直观,所有形式就整成了像vue路由,方便大家理解和快速上手
    // 登录
    ...require('./page_config/router/login.js'),
    // 首页 && 设置
    ...require('./page_config/index.js'),
    // 演示demo页面文件
    ...require('./page_config/router/demo.js'),
  ],
  // tabBar
  tabBar: require('./page_config/tabbar.js'),
  // 全局样式
  globalStyle: require('./page_config/global-style.js'),
}
// page_config/router文件夹下的login.js
const modules = [
    {
        path: 'pages/login/index',
        style: {
            navigationBarTitleText: '登录',
        },
    },
];
module.exports = modules;
// page_config文件夹下的global-style.js
const modules = {
  navigationBarTextStyle: 'black',
  navigationBarTitleText: 'uni-app',
  navigationBarBackgroundColor: '#F8F8F8',
  backgroundColor: '#F8F8F8',
}

module.exports = modules
// page_config文件夹下的tabbar-style.js
module.exports = {
  color: '#7A7E83',
  selectedColor: '#2196f3',
  backgroundColor: '#fff',
  height: '50px',
  list: [
    {
      pagePath: 'pages/index/index',
      iconPath: 'static/index.png',
      text: '首页',
      selectedIconPath: 'static/ihdex-active.png',
    },
    {
      pagePath: 'pages/setting/index',
      iconPath: 'static/setting.png',
      text: '设置',
      selectedIconPath: 'static/setting-active.png',
    },
  ],
}

vite.config.ts引入uni-pages-hot-modules

插件需要在vite.config.ts进行配置

import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import path from 'path'
import uniHot from 'uni-pages-hot-modules'

uniHot.setupHotJs()

export default defineConfig({
  plugins: [uni(), uniHot.createHotVitePlugin()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '*': path.resolve(''),
    },
  },
})

ESLint

ESLint简介

Eslint是一个代码检查工具,用来检查我们的代码是否符合指定规范并且在不符合我们指定规则的时候,Eslint还会做一些简单的代码格式化修正,使我们的代码风格统一,代码正确。

安装依赖

pnpm install eslint --save-dev

Eslint增强

  • 由于 ESLint 默认使用 Espree 进行语法解析,无法识别 TypeScript 的一些语法,故我们需要安装 typescript-eslint-parser,替代掉默认的解析器
  • 由于 typescript-eslint-parser 对一部分 ESLint 规则支持性不好,故我们需要安装 eslint-plugin-typescript,弥补一些支持性不好的规则。
# 安装依赖
pnpm install @typescript-eslint/eslint-plugin --save-dev
pnpm install @typescript-eslint/parser --save-dev

Eslintrc.js

用来配置eslintrc规则的文件,需要在项目根目录新增一个叫eslintrc的js文件,文件配置如下,仅供参考,大家可以自行修改里面的规则。

module.exports = {
  parser: 'vue-eslint-parser',
  extends: ['plugin:vue3/recommended', 'plugin:prettier/recommended'],
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 2020,
    sourceType: 'module',
  },
  plugins: ['vue', '@typescript-eslint'],
  rules: {
    'vue/multi-word-component-names': 'off',
  },
}

如果有需要忽略代码检查的文件可在同级目录中添加eslintignore文件,注意eslintignore不带文件后缀,不是JS文件,然后将需要忽略检查的文件写入即可。

vscode配置

插件安装

插件市场搜索ESLint,安装重启即可

settings.json

选择vscode左上角菜单文件=>首选项=>设置,找到setting.json文件,然后添加代码如下,规则除了基本的保存自动修复等规则,其他的可按需引入,不必一致。

{
    // 保存自动修复代码,保证代码风格统一,但推荐使用editor.codeActionsOnSave功能,因为它可配置性更好
    "editor.formatOnSave": true,
    // 在下方显示ESLint图标
    "eslint.alwaysShowStatus": true,// 在快速修复菜单中显示打开的 lint 规则文档网页
    "eslint.codeAction.showDocumentation": {
        "enable": true
    },
    // 启用ESLint作为文件的格式化工具
    "eslint.format.enable": true,
    // ESlint必须要校验的语言类型,只有加上对应的语言后 VSCode才会在编辑器中正确的高亮错误的语法,而且会在窗口中输出错误信息。
    "eslint.probe": [
        "javascript",
        "javascriptreact",
        "typescript",
        "typescriptreact",
        "html",
        "vue",
        "markdown",
        "json"
  ],
}

webstorm配置

webstorm有三种使用Eslint模式,禁用、自动引用当前项目node_modules文件中的规则,或者是eslintrc* 中的规则,还有就是选择指定修复规则。一般都是默认使用自动引用的方式,自动保存格式化代码需要打开file=>settings,然后搜索eslint,然后到页面勾选Run eslint --fix on save,如图所示 eslint.png

prettier

prettier简介

代码格式化工具,可以让代码结尾是分号还是不用符号,也可以让代码是否使用单引号,把错乱的代码格式化为符合指定标准的漂亮代码。

安装依赖

pnpm install prettier --save-dev

prettierrc.js

prettier代码格式化规则文件,项目根目录添加prettierrc.js文件,文件规则如下,规则我配的只是一些通用的,如果大家有其他要求,自行配置规则,不必全部一样,按需引入。

module.exports = {
    // 一行最多最多几个字符,此时设置为120 字符
    printWidth: 120,
    // 使用 4 个空格缩进
    tabWidth: 4,
    tabs: false,
    // 行尾需要有分号
    semi: true,
    // 使用单引号
    singleQuote: true,
    // 对象的 key 仅在必要时用引号
    quoteProps: 'as-needed',
    // 大括号内的首尾需要空格
    bracketSpacing: true,
    // jsx 标签的反尖括号需要换行
    jsxBracketSameLine: false,
    // 箭头函数,只有一个参数的时候,也需要括号
    arrowParens: 'always',
    // 行尾形式 lf|crlf|cr|auto 默认lf
    endOfLine: 'auto',
};

如果有需要忽略代码检查的文件可在同级目录中添加prettierignore文件,注意prettierignore不带文件后缀,不是JS文件,然后将需要忽略格式化的文件写入即可。

消除Eslint和Prettier的冲突

  • 用于当 ESLint 的规则和 Prettier 的规则相冲突时,就会发现一个尴尬的问题,用其中一种来格式化代码,另 一种就会报错。 prettier 官方提供了一款工具 eslint-config-prettier 来解决这个问题。 本质上这个工具其实就是禁用掉了一些不必要的以及和 Prettier 相冲突的 ESLint 规则。
  • 上面介绍的工具,仅仅只是将部分 ESLint 规则给禁用了,避免 Prettier 格式化之后的代码导致 ESLint 报错而已,如何将两者结合起来使用呢? prettier 官方提供了一个 ESLint 插件 eslint-plugin-prettier。 这个插件的主要作用就是将 prettier 作为 ESLint 的规则来使用,相当于代码不符合 Prettier 的标准时, 会报一个 ESLint 错误,同时也可以通过 eslint --fix 来进行格式化。
  • ESLint是检查JS代码的依赖,那么它怎么去检查Vue语法的文件呢?要完成这件事,我们就需要安装vue官方开发的ESLint插件eslint-plugin-vue。这样ESLint就知道该怎么检查vue的文件了。

安装依赖

# 安装依赖
pnpm install eslint-config-prettier
pnpm install eslint-plugin-prettier
pnpm install eslint-plugin-vue

stylelint

stylelint简介

一个强大的、现代化的 CSS 检测工具,与 ESLint 类似, 是通过定义一系列的编码风格规则帮助我们避免在样式表中出现错误。

安装依赖

# 安装依赖
pnpm install stylelint postcss postcss-less postcss-html stylelint-config-prettier stylelint-config-recommended-less stylelint-config-standard stylelint-config-standard-vue stylelint-less stylelint-order -D

stylelintrc.js

css检查规则文件,项目根目录添加stylelintrc.js文件,文件规则如下,规则我配的只是一些通用的,如果大家有其他要求,自行配置规则,不必全部一样,按需引入。

module.exports = {
    extends: [
        'stylelint-config-standard',
        'stylelint-config-prettier',
        'stylelint-config-recommended-less',
        'stylelint-config-standard-vue',
    ],
    plugins: ['stylelint-order'],
    // 不同格式的文件指定自定义语法
    overrides: [
        {
            files: ['**/*.(less|css|vue|html)'],
            customSyntax: 'postcss-less',
        },
        {
            files: ['**/*.(html|vue)'],
            customSyntax: 'postcss-html',
        },
    ],
    // 忽略校验的文件
    ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts', '**/*.json', '**/*.md', '**/*.yaml'],
    rules: {
         // 禁止未知单位
        'unit-no-unknown': null,
        // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
        'no-descending-specificity': null, 
        'selector-pseudo-element-no-unknown': [
            true,
            {
                ignorePseudoElements: ['v-deep'],
            },
        ],
        'selector-pseudo-class-no-unknown': [
            true,
            {
                ignorePseudoClasses: ['deep'],
            },
        ],
        // 指定样式的排序
        'order/properties-order': [
            'position',
            'top',
            'right',
            'bottom',
            'left',
            'z-index',
            'display',
            'justify-content',
            'align-items',
            'float',
            'clear',
            'overflow',
            'overflow-x',
            'overflow-y',
            'padding',
            'padding-top',
            'padding-right',
            'padding-bottom',
            'padding-left',
            'margin',
            'margin-top',
            'margin-right',
            'margin-bottom',
            'margin-left',
            'width',
            'min-width',
            'max-width',
            'height',
            'min-height',
            'max-height',
            'font-size',
            'font-family',
            'text-align',
            'text-justify',
            'text-indent',
            'text-overflow',
            'text-decoration',
            'white-space',
            'color',
            'background',
            'background-position',
            'background-repeat',
            'background-size',
            'background-color',
            'background-clip',
            'border',
            'border-style',
            'border-width',
            'border-color',
            'border-top-style',
            'border-top-width',
            'border-top-color',
            'border-right-style',
            'border-right-width',
            'border-right-color',
            'border-bottom-style',
            'border-bottom-width',
            'border-bottom-color',
            'border-left-style',
            'border-left-width',
            'border-left-color',
            'border-radius',
            'opacity',
            'filter',
            'list-style',
            'outline',
            'visibility',
            'box-shadow',
            'text-shadow',
            'resize',
            'transition',
        ],
    },
};

如果有需要忽略代码检查的文件可在同级目录中添加stylelintignore文件,注意stylelintignore不带文件后缀,不是JS文件,然后将需要忽略检查的文件写入即可。

vscode配置

插件安装

插件市场搜索Stylelint,安装重启即可

settings.json

选择vscode左上角菜单文件=>首选项=>设置,找到setting.json文件,然后添加代码如下,规则是一些通用的规则,有其他需要可按需引入,不必一致。

 // 语言标识符数组,指定要为哪些文件启用代码段
 "stylelint.snippet": ["css", "less", "postcss", "scss", "vue"],
 // 配置stylelint检查的文件类型范围
 "stylelint.validate": [
     "css",
     "less",
     "postcss",
     "scss",
     "vue"
],

webstorm配置

webstorm不需要设置啥,正常安装依赖就行

husky

husky简介

husky是一款git-hooks执行工具,帮助我们在提交代码时执行一些代码检查或其他的操作,一般用到pre-commit和commit-msg钩子。 安装过程请看官方文档的自动安装步骤手动安装步骤

依赖安装

# 自动安装
pnpm dlx husky-init
pnpm install

准备工作

安装依赖成功后会在根目录生成.husky文件夹,还需要做两步操作

  1. 在.husky文件夹下找到commit-msg文件,没有就新建一个,将代码替换成如下代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# git commit之前执行,用于判断commit提交信息格式是否正确,依赖于commitlint.config.js配置文件中配置的规则。
npx --no-install commitlint --edit $1
  1. 在.husky文件夹下找到pre-commit文件,没有就新建一个,将代码替换成如下代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 这样的话提交时只要在lint-staged.config.js文件配置了指定规则,提交时只校验指定规则的文件,不用校验所有文件
npx lint-staged 

commitlint

commitlint简介

用来检查提交的信息说明是否遵循一定的规范

依赖安装

# 安装依赖
pnpm install --save-dev @commitlint/config-conventional @commitlint/cli

commitlint.config.js

commitlint的配置文件,我这边只用了默认的规则,如果有特殊需求,可根据实际情况进行配置规则

module.exports = {
    // 继承的规则
    extends: ['@commitlint/config-conventional'],
};

lint-staged

lint-staged简介

一个仅仅过滤出 Git 代码暂存区文件(被 git add 的文件)的工具

安装依赖

# 安装依赖
pnpm install -D lint-staged

lint-staged.config.js

当前我们项目是只校验src文件夹下的文件,如有特殊需要,按需配置

module.exports = {
    'src/**/*.{js,vue,ts}': ['npm run lint', 'git add'],
    'src/**/*.{vue,scss,css}': ['npm run lint:css', 'git add'],
};

uni-ui

ui框架使用背景

由于之前使用过uview1.0和uview2.0,还用过一些需要付费的ui框架,但是最后发现最合适的还是uni-app自己写的内置组件和扩展组件uni-ui,这样我们只需关注uni-app对组件的更新迭代,并且uni-app对亲儿子的支持肯定也是最好的,所以我选择了使用uni-ui,ui选型大家可以根据自身需求和喜好去进行安装选用。

安装依赖

# 1.先安装sass
pnpm add sass -D
# 2. 然后安装
# sass-loader安装最新的node版本需要大于16,如果node版本低于16,请安装低于@11.0.0 的版本
pnpm add sass-loader
# 安装uni-ui
pnpm add @dcloudio/uni-ui

配置easycom

使用 pnpm 安装好 uni-ui 之后,需要配置 easycom 规则,让 pnpm 安装的组件支持 easycom打开项目根目录下的 pages.js 并添加 easycom 节点:

// pages.js
module.exports = {
  easycom: {
    autoscan: true,
    custom: {
      // 安装uni-ui后需要添加的节点
      '^uni-(.*)': '@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue',
    },
  },
  pages: [
    // 登录
    ...require('./page_config/router/login.js'),
    // 首页 && 设置
    ...require('./page_config/index.js'),
    // 钢瓶入厂
    ...require('./page_config/router/in-storage.js'),
    // 生产管理
    ...require('./page_config/router/product-manage.js'),
    // 钢瓶出厂
    ...require('./page_config/router/out-storage.js'),

    // 不合格区
    ...require('./page_config/router/unqualified-area.js'),
  ],
  // tabBar
  tabBar: require('./page_config/tabbar.js'),
  // 全局样式
  globalStyle: require('./page_config/global-style.js'),
}

开发uni-app时vscode插件推荐

  • uni-app-snippets(uni-app 基本能力代码片段)
  • uni-ui-snippets(uni-ui 组件代码片段)

uni.request封装

对于前后端联调,通过uni.request进行了网络请求封装。

import { httpType, httpData } from './types'
import { hideLoading, showLoading, showToast } from './messageTip'
import { clearToken } from './uni-storage'

const BASE_URL = import.meta.env.VITE_API_URL as string

/**
 * 未登录码
 */
const NOT_LOGIN_CODE = -997

/**
 * 异常提示码
 */
const EXCEPTION_CODE = 300
/**
 * 成功码
 */
const SUCCESS_CODE = 120


function http(data: httpData, method: httpType, url: string) {
  const token = uni.getStorageSync('token')

  showLoading('请求中')
  return new Promise((resolve) => {
    uni.request({
      url: BASE_URL + url,
      method,
      data,
      dataType: 'json',
      header: {
        'v-token': token,
      },
      success: (res) => {
        responHandler(res, resolve)
      },
      fail: () => {
        errorHander()
      },
      complete: () => {
        hideLoading()
      },
    })
  })
}

function responHandler(res: httpData, resolve: httpData) {
  const { code } = res.data
  if (code === NOT_LOGIN_CODE) {
    // 未登录处理
    clearToken()
    uni.showToast({
      title: '登录失效,请重新登录',
      icon: 'none',
      mask: true,
      complete: () => {
        uni.navigateTo({ url: '/pages/login/login' })
      },
    })
    console.log('我失效了')

    return false
  }

  if (code === EXCEPTION_CODE) {
    showToast(res.data.message)
    return resolve(false)
  }

  if (code === SUCCESS_CODE) {
    return resolve({ ...res.data })
  }
}

/**
 * 错误处理
 */
function errorHander() {
  showToast('网络错误或服务器错误')
}

/**
 * get请求
 */
export function get(url: string, data?: httpData) {
  return http(data, 'GET', url)
}

/**
 * post请求
 */
export function post(url: string, data: httpData) {
  return http(data, 'POST', url)
}

# 接口api设置
import { httpData } from '@/utils/types'
import { get, post } from '@/utils/request'
export default {
  /**
   * app-登录
   */
  login: (data: httpData) => post('/api/v1/user/manage/login', data),
  /**
   * 退出登录
   */
  logout: () => get('/api/v1/user/manage/logout'),
}

数据缓存

将数据存储在本地,类似于vuex,pinia,新建文件uni-storage.js文件,文件内容如下

/**
 * 将 data 存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个同步接口。
 * @param key 属性名称
 * @param data 要保存的数据
 */
export function saveStorage(key: string, data: unknown) {
  uni.setStorageSync(key, data)
}

/**
 * 从本地缓存中同步获取指定 key 对应的内容。
 * @param key 取数据的key
 * @returns
 */
export function getStorage(key: string) {
  return uni.getStorageSync(key)
}

/**
 * 清除TOKEN
 * @param {string} tokenKey token key值
 */
export function clearToken(tokenKey = 'token') {
  uni.setStorageSync(tokenKey, null)
}

uni-app打包

uni-app打包方式目前有两种,云打包和本地打包,主要介绍本地打包,云打包比较简单,只进行简单的介绍

云打包

  1. 用HBuilderX打开要打包的项目
  2. 选中项目列表中的项目右击选择发行=>原生App-云打包,弹出打包页面
  3. 开发者中心生成证书
  4. 证书生成后下载下来,存到指定目录,然后在开发者中心选择需要打包项目=>我的应用=>证书管理=>证书详情中可查看别名密码
  5. 打包页面输入别名密码,选择下载好的证书,可以选择不要开屏广告,然后选择打包
  6. 最后会生成apk包,下载安装即可使用

本地打包

本地打包其实不难,就是过程稍微有点繁琐,写文档还是不太好组织语言,我尽量嘿嘿。

下载HBuilderX

下载地址

下载AndroidStudio

-下载地址

SDK下载地址

App离线打包SDK 下载

android打开SDK打包配置文件

下载好SDK后,解压找到HBuilder-Integrate-AS文件夹,用androidStudio打开HBuilder-Integrate-AS文件夹。 index.png 如下图即为成功,文件内容build之类的是后期我这边打包,自动生成的,新打开的应该是没有,所以文件夹内容可能会有些许不一样,不用在意。 index2.png

生成keystore

生成keystore需要jre运行环境,据我所知,AndroidStudio安装成功后自带它的安装目录中自带jre运行环境,或者也可以下载JAVA的JDK,里面附带JRE运行环境。

# 生成keystore文件命令(当前目录下生成)
# mystore是证书别名,可修改为自己想设置的字符,建议使用英文字母和数字
# youstore.keystore是证书文件名称,可修改为自己想设置的文件名称,也可以指定完整文件路径
# 36500是证书的有效期,表示100年有效期,单位天,建议时间设置长一点,避免证书过期
keytool -genkey -alias mystore -keyalg RSA -keysize 2048 -validity 36500 -keystore youstore.keystore

# 查看keystore信息,要到youstore.keystore的路径下运行此命令,否则找不到
keytool -list -v -keystore  youstore.keystore

然后将生成的keystore文件复制或者剪切到simpleDemo目录下,与src同级

生成本地资源

  1. 选中项目列表中的项目右击选择发行=>原生App-本地打包(L)=>生成本地打包App资源(R),控制台会进行打包,然后生成资源文件,最后会出现一个路径,点击路径打开文件夹 。打开后如图所示 file.png
  2. 然后打开当前目录的上上级,复制目录下唯一的文件夹,如图所示 copy.png
  3. 将复制的文件夹粘贴到,之前androidStudio打开的HBuilder-Integrate-AS\simpleDemo\src\main\assets\apps文件夹下,删除apps文件夹下唯一的文件,只留下复制过来的文件夹

replace.png

生成Android

  1. 进入uni-app开发者中心
  2. 选择要打包的项目=>我的应用=>离线打包key管理
  3. 根据之前生成的keystore文件信息生成Android,如图所示

android.png 4. 然后就可以在androidStudio中进行配置打包信息了

打包信息配置

  1. appid更换,如下图所示 image.png
  2. app名称,如下图所示 image.png
  3. package修改,如下图所示 image.png
  4. Android更换,如下图所示 image.png
  5. 证书配置更换,如下图所示 image.png

打包之debug测试包

前面配置好后即可进行打包 image.png

打包之打包正式包

  1. 第一步 image.png
  2. 第二步 image.png
  3. 第三步 image.png

最终效果

image.png

打包总结

总得来说本地打包不算复杂,还有一些没有介绍的,比如打包后app启动页面、app图标、版本号之类的,这些细节我就不过多的去介绍,因为这些不影响项目的打包,大家花点心思应该就能自己掌握。

vue

项目主要用的版本是vue3.2,说到这,就想巩固一下过去学习的知识点,顺便简单的整理一番,所以简单的谈谈过去用vue2开发的体验,以及用了vue3后给我们带来了哪些提升和便利,然后介绍一下3.2版本的一些新特性,对比一下3.0,新特性给我们带来了哪些好的的开发体验。

vue2

先说说个人开发体验,最早接触vue2也是写的app,后来也vue2写了后台管理系统,总的来说,那时候因为还没接触vue3,因为没有对比和个人代码水平低、见识少,很多时候没有考虑过为什么要这么做,为什么不能简便些。现在回顾一下之前俺写的代码,首先代码规范上,缺少了Typescript,代码书写时,为了抽离各种业务,需要写很多的mixins,而且需要像引入组件一样,先import引入,然后到mixins注册,而且mixins一旦过多,及其容易造成上下文丢失、命名冲突等问题,甚至有些细微的差别,就需要重新写一个mixinxs。接下来介绍一下vue2的选项式API和选项式API存在的问题

选项式API(opetion API)

字面意思就是选择式(opetion)API,约定在什么位置做什么事,顺其自然地强制了代码分割

  • data里面定义状态

  • methods里面设置事件方法

  • props里面设置接收参数

  • computed里面设置计算属性

  • watch里面设置监听属性

option API存在的问题

  • 不利于复用
  • 潜在命名冲突
  • 上下文丢失
  • 有限的类型支持
  • 前期也做了一些关于文章的阅读,vue3之前对TS的支持并不是很好,必须使用第三方库来支持;而vue2说到底其实是配置形式,只能是局部支持。 vue-class-component 按API类型组织

代码

单纯的文字描述不一定直观,我写个例子让大家更为直观些,实际开发过程中,业务复杂并且我们不想把所有代码写到一个页面中,或者是方便在其他地方复用,我们可能需要引入更多甚至超过10个mixins,因为是演示我就只写两个mixins。

teacherMixins
# teacherMixins
export default {
    data() {
        return {
            // 教师数量
            teacherNum: 1
        };
    },
    methods: {
        addTeacher() {
            // 自增教师数量
            this.teacherNum++
        },
    },
};
studentMixins
# studentMixins
export default {
    data() {
        return {
            // 学生数量
            studentNum: 1
        };
    },
    methods: {
        addStudent() {
            // 自增学生数量
            this.studentNum++
        },
    },
};
index.vue

当前vue文件引入以上两个mixins,代码如下

<template>
    <div>
        <button type="primary" @click="addTeacher">
            新增教师数量
        </button>
        <button type="primary" @click="addStudent">
            新增学生数量
        </button>
        <button type="primary" @click="computedTotal">
            计算教师和学生的总数量
        </button>
        <text>{{ total }}</text>
    </div>
</template>
<script>
import teacherMixins from "./teacherMixins";
import studentMixins from "./studentMixins";

export default {
    mixins: [teacherMixins, studentMixins],
    data() {
        return {
            // 教师和学生的总数
            total: 0
        };
    },
    methods: {
        computedTotal() {
            // 计算教师和学生总数,当然也可以用watch,computed来计算
            this.total = this.studentNum + this.teacherNum;
        }
    }
};
</script>

所以从上面的代码看来,首先我们是没办法一眼就知道this.studentNum和this.teacherNum这两个字段从哪来,我们需要点到mixins中才能核实,当前字段由哪个mixins定义的。同理addTeacher和addStudent方法也没法一眼就知道来自哪,一旦业务开始复杂,就会造成上下文丢失,命令冲突等问题。

vue3.0

vue2.0到3.0变化还是很大的,简单的整理了一下以下几条

写法

首先写法上就有了较为大的变化,如下所示

# vue2
<template>
    <div>
        <button type="primary" @click="addCount">
            数量加1
        </button>
        <text>{{ count }}</text>
    </div>
</template>
<script>
import Index2 from "./index2.vue";
import myMixins from "./myMixins"

export default {
    components: { Index2 },
    mixins: [myMixins],
    data() {
        return {
            count: 0
        };
    },
    methods: {
        addCount() {
            this.countcount++;
        }
    }
};
</script>
# vue3.0
<script lang="ts">
import Index2 from "./index2.vue"
import useCompositionAPI from "./useFunction"

export default defineComponent({
  components: { Index2 },
  setup(props, context) {
    const count = ref<number>(1)
    
    const { reduceCount } = useCompositionAPI() 
    
    function addCount(){
        count++;
    };
    
    function submit(): void{
        console.log('提交成功')
    }
    
    function cancel(): void{
        console.log('取消')
    }
    
    return {
      addCount,
      submit,
      cancel
    };
  }
});
</script>

生命周期

生命周期不同

2.0生命周期3.0生命周期3.2生命周期描述
beforeCreate-setup数据还未初始化,组件实例创建之前
created-setup数据已经初始化好,组件实例已创建
beforeMountedonBeforeMountonBeforeMount组件还未挂载之前调用
mountedonMountedonMounted组件已挂载完成调用
beforeUpdateonBeforeUpdateonBeforeUpdate组件数据还未更新,data数据已经更新,数据还未同步到组件
updatedonUpdatedonUpdated组件数据和data数据已同步,组件已重新渲染完毕
beforeDestoryonBeforeUnmountonBeforeUnmount组件销毁之前调用,此时实例还是正常的,组件正常使用
destoryedonUnmountedonUnmounted组件已经被销毁,组件实例已不可用
activitedonActivatedonActivated当组件被keep-alive缓存,激活时调用
deactivatedonDeactivatedonDeactivated当组件被keep-alive缓存,销毁时调用

双向绑定

vue2用的是ES5的API Object.definepropert方法来对数据进行劫持,结合发布订阅模式来实现的,而vue3使用的是ES6的API proxy来进行对数据的拦截处理,单词本身翻译为代理,大家都叫proxy代理。

Object.defineProperty

vue2的双向绑定是基于Object.defineProperty进行底层封装的, Object.defineProperty是ES5的一个API,它只能劫持对象的属性,所以我们监听时需要遍历对象的每一个属性。老样子,下面写个例子操作一波。

<template>
    <div class="form">
        <input type="text" @input="changeValue" />
        <div id="text"></div>
    </div>
</template>
<script setup>
let obj = {
    name: '我自有一剑开天门',
};

Object.defineProperty(obj, 'name', {
    enumerable: true,
    get() {
        console.log('我被劫持了,救我');
        return '我被读取了';
    },
    set(val) {
        // 我在同步更新Dom
        console.log(`我是新的值${val}`);
        updateDom(val);
    },
});
// 更新Dom
function updateDom(objData: { name: string }) {
    document.getElementById('text').innerHTML = objData;
}

function changeValue(e) {
    obj.name = e.target.value;
    console.log(obj.name);
}

<script>

Object.defineProperty()方法接受三个参数,第一个参数是目标对象,第二个参数是对象的属性名称,第三个参数是描述对象某些操作的配置项,通常是一个对象。
小提示

  1. 第三个参数拥有6个api
api描述
enumerable是否可以出现在对象的枚举中,默认为false
configurable只有configurable为true时,对应对象属性的描述符才能够被改变并且也能删除对应的属性,默认为false
value对应属性的值,默认为undefined
writable是否可以改变对应属性的值,默认为false
get当函数被读取时调用,返回值是对象对应属性的值
set当劫持的对象对应的属性值发生变化时触发,可以用来做Dom的更新或者调用一些其他的方法
proxy

关于proxy早先我只知道一个拦截概念,现在我知道的也不多,嘿嘿。proxy是ES6的API,主要是用于操作对象的,在需要操作的对象外面包一层拦截,每次操作对象时,都需要经过这层拦截,因此我们可以在访问或者操作对象时做一些而外的操作。下面手写一个响应式的例子给大家有个直观的了解。

<template>
    <div class="form">
        <input type="text" @input="changeValue" />
        <div id="text"></div>
    </div>
</template>
<script setup>
let obj = {
    name: '我自有一剑开天门',
};

let proxy = new Proxy(obj, {
    get: function (target, p, receiver) {
        // console.log(target);
        // console.log(p);
        // console.log(receiver);
    },
    set: (target, p, value, receiver) => {
        console.log(target);
        console.log(p);
        console.log(value);
        console.log(receiver);
        updateDom(value);
        return true;
    },
});

// 更新Dom
function updateDom(objData: { name: string }) {
    document.getElementById('text').innerHTML = objData;
}

function changeValue(e) {
    proxy.name = e.target.value;
}

<script>

image.png

  1. proxy接收两个参数,第一个参数为目标对象,第二个参数为用来描述要对目标对象所做的一些操作,例如get、set。
  2. get主要用于读操作,第一个参数target是目标对象,第二个参数p是对象的属性,第三个参数通常是proxy本身。
  3. get主要用于写操作,第一个参数target是目标对象,第二个参数p是对象的属性,第三个参数是对象属性的值,第四个参数通常是proxy本身。

script setup

script setup是vue3.2后新加的语法糖,相比较于之前的3.0版本到3.2版本,我觉得3.2推出的语法糖在开发过程中极大的提升了我的开发体验,下面分别写个例子让大家更为直观.

3.0语法
<script lang="ts">
import draggable from 'vuedraggable';
import {defineComponent, Ref, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, FormSchema, useForm } from '/@/components/Form';
import { Transfer } from 'ant-design-vue';
import pageConfig from "/@/utils/pageConfig";

export default defineComponent({
  components: { BasicModal, BasicForm, Transfer, draggable },
  props: {
    routerName: {
      type: String,
      required: true,
    },
    currentMenu: {
      type: String,
      required: true,
    },
    businessFlag: {
      type: String,
      required: true,
    },
  },
  setup(props, context) {
    const test = ref<string>('');
    const count = ref<number>(1)
    
    function addCount(){
        count++;
    };
    
    function submit():void{
        console.log('提交成功')
    }
    
    function cancel():void{
        console.log('取消')
    }
    
    return {
      test,
      addCount,
      submit,
      cancel
    };
  }
});
</script>
3.2 script setup语法糖
<script lang="ts" setup>
import draggable from 'vuedraggable';
import {defineComponent, Ref, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, FormSchema, useForm } from '/@/components/Form';
import { Transfer } from 'ant-design-vue';
import pageConfig from "/@/utils/pageConfig";

const props = withDefaults(
    defineProps<{
        routerName: String;
        currentMenu: String;
        businessFlag: string;
    }>(),
    {
        routerName: '',
        currentMenu: '',
        businessFlag: '',
    }
);

const test = ref<string>('');

const count = ref<number>(1)

function addCount(){
    count++;
};

function submit():void{
    console.log('提交成功')
}

function cancel():void{
    console.log('取消')
}
</script>
对比3.0和3.2
  1. 在不用语法糖时,每引入一个组件,都需要去components中注册一次,而语法糖自动注册,较少了工作量,也减少了代码量。
  2. 在不用语法糖时,每定义一个变量或是方法,都需要在最后给return出去,一旦页面代码较多时,需要每次都要滚到最低下return返回,给开发增加了挑战,并且在代码维护上也是挑战。使用语法糖后,所有的顶级变量、函数,都会自动暴露给模板使用,不需要一个个的return了,大幅度的减少了代码量

组合式API

前面介绍到了optionAPI以及其存在的问题。到了vue3使用组合式API,它给我们带来了很多便利,个人强烈推荐使用组合式API来写项目,实战后发现代码复用和维护以及代码美观等方面都提升了很多。
组合式API带来的能力

  • 极易复用
  • 可灵活组合
  • 提供更好的上下文支持
  • 按功能/逻辑组织
  • 更好的TypeScript类型支持
组合式API的使用
# index.vue文件
<script setup lang="ts">
import { ref } from 'vue';
import Login from '@/api/login/index';
import useLogin from '@/pages/login/useLogin';
import { saveStorage } from '@/utils/uni-storage';
import { unknownType } from '@/utils/types';

const { formRef, form, rules, imageList } = useLogin();

// 登录
const login = async () => {
    const validForm = ref<boolean>(false);
    await formRef.value.validate((valid: boolean) => {
        if (!valid) {
            validForm.value = true;
        } else {
            validForm.value = false;
        }
    });

    if (!validForm.value) {
        return;
    }

    const res: unknownType = await Login.login(form.value);
    if (!res) {
        return;
    }
    // token
    saveStorage('token', res.data.token);
    // 登录用户的信息
    saveStorage('userInfo', res.data);
    uni.switchTab({ url: '/pages/index/index' });
};
</script>
# useLogin文件
import { ref } from 'vue';
import cloud from '@/assets/images/login_cloud.png';
import order from '@/assets/images/login_order.png';
import sale from '@/assets/images/login_sale.png';
import analysis from '@/assets/images/login_analysis.png';

interface loginForm {
    account: string;
    password: string;
}

export default function useLogin() {
    const formRef = ref();

    const form = ref<loginForm>({
        account: '',
        password: '',
    });

    const imageList = ref({
        cloud: cloud,
        order: order,
        sale: sale,
        analysis: analysis,
    });

    const rules = ref({
        account: {
            rules: [
                {
                    required: true,
                    errorMessage: '请输入用户名',
                },
            ],
        },
        password: {
            rules: [
                {
                    required: true,
                    errorMessage: '请输入密码',
                },
            ],
        },
    });
    return {
        form,
        formRef,
        rules,
        imageList,
    };
}

通过上述代码可看到我将表单对象、图片列表、表单rules、以及表单ref都放在了useLogin.ts文件导出的默认函数useLogin()中,最后将其return出来了。我们在index.vue文件中对其解构使用。非常的方便,将代码按照功能分离,这只是一个简单的例子,其实如果设计到复杂的业务场景且业务差不多,我们就不需要定义很多遍同样的变量和方法,我们可以按需的从useFunction中将我们需要的按需解构出来。

defineExpose

使用script setup语法糖时组件的实例是默认无法被其他组件通过ref直接拿到的,我们需要使用官方提供的defineExpose编译器宏来将需要暴露出去的属性暴露出去,使得其他组件能够使用ref来获取script setup组件中的实例。

推荐使用场景

弹框
对话框的打开和关闭,当我们正常使用对话框时需要设置v-model的值去控制对话框的打开和关闭,并且当对话框时组件时,我们需要父组件调用子组件,关闭时子组件还需要给父组件emit回调来告诉父组件它的状态变为关闭了。实际生产业务中操作起来还是比较繁琐的,代码量较大。下面分享实战例子
业务场景:当我在实际生活中超市对采购物品进行出库或者售出时,为了节省人力,我们需要通过扫码枪去扫描商品的条形码或者二维码,从而获取到物品的生产日期、保质期等信息,如果商品的保质期过了,我们需要弹出一个弹框提示,告诉操作人员,商品保质期异常了,然后操作人员将商品进行继续使用申请或者是待处理操作。

# 弹框组件代码
template>
    <view>
        <uni-popup ref="popupRef" type="dialog">
            <uni-popup-dialog
                mode="base"
                title="保质期异常"
                :before-close="true"
                type="error"
                confirm-text="继续使用申请"
                cancel-text="待处理"
                @close="cancel"
                @confirm="confirm"
            >
                <template #default>
                    <view class="dialog-content">
                        <view class="dialog-content-icon"
                            ><uni-icons type="info-filled" color="red" size="120"></uni-icons
                        ></view>
                        <view class="dialog-content-text">生产日期 : {{ date.manufactureDate }}</view>
                        <view class="dialog-content-text">保质期 : {{ date.nextInspectionDate }}</view>
                    </view>
                </template></uni-popup-dialog
            >
        </uni-popup>
    </view>
</template>
<script lang="ts" setup>
import useIndex from './useIndex';
import { yearlyDateType } from '@/utils/types';

withDefaults(
    defineProps<{
        date: yearlyDateType;
    }>(),
    {
        date: () => {
            return {
                manufactureDate: '',
                nextInspectionDate: '',
            };
        },
    }
);
const emit = defineEmits<{
    (e: 'yearlyCheckSubmit'): void;
    (e: 'continueUseSubmit'): void;
}>();

const { popupRef, openDialog, cancel, confirm, close } = useIndex(emit);

/**
 * 暴露属性/方法
 */
defineExpose({
    open() {
        openDialog();
    },
    close() {
        close();
    },
});
</script>
<style lang="scss" scoped>
.dialog-content {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.dialog-content-icon {
    margin: 0 10rpx;
}

.dialog-content-text {
    margin: 10rpx 0;
}
</style>
# 使用弹框页面代码
# XForm组件是封装好的表单组件,相当于表单配置化,里面有扫码扫码输入框,可操作扫码,扫码成功后会使用emit回调sacnValueHandle()方法。
<template>
    <view>
        <XForm ref="xFormRef" :form-list="formList" @scan="sacnValueHandle"></XForm>
        <XYearlyCheckError
            ref="xYearlyCheckErrorRef"
            :date="date"
            @yearly-check-submit="yearlyCheckSubmit"
            @continue-use-submit="continueUseSubmit"
        ></XYearlyCheckError>
    </view>
</template>
<script lang="ts" setup>
import useIndex from './useIndex';
const { formList, sacnValueHandle, xFormRef, xYearlyCheckErrorRef, yearlyCheckSubmit, continueUseSubmit, date } =
    useIndex();
</script>


# 上面的useIndex代码
import { unknownType, yearlyDateType } from '@/utils/types';
import { ref } from 'vue';
import { XFormItem } from '@/components/x-form/form';
import { schemaList } from './conf';
import { showToast } from '@/utils/messageTip';
import { YEAR_INSPECT_STATUS } from '@/constant';
export default function useIndex() {
    // 表单实例
    const xFormRef = ref();

    // 保质期异常弹框
    const xYearlyCheckErrorRef = ref();

    // 表单配置列表
    const formList = ref<XFormItem[]>(schemaList);

    // 保质期
    const date = ref<yearlyDateType>({
        manufactureDate: '',
        nextInspectionDate: '',
    });

    // 表单值
    const formValue = ref<unknownType>({});

    /**
     * 扫码成功回调
     */
    async function sacnValueHandle() {
        // 通过表单的ref获取表单组暴露出来的获取值的方法
        const form = await xFormRef.value.getForm();
        // 生产日期
        date.value.manufactureDate = form.manufactureDate;
        // 保质期
        date.value.nextInspectionDate = form.nextInspectionDate;
        // 如果扫码发现保质期到期了,就打开保质期弹框
        if (form.nextInspectionDateLabel === YEAR_INSPECT_STATUS.ABNORMAL) {
            // 通过弹框组件的ref去控制保质期异常弹框的打开
            xYearlyCheckErrorRef.value.open();
        }
    }

    /**
     * 待处理提交
     */
    function yearlyCheckSubmit() {
        // 此处可写待处理业务代码

        // 最终提交后,使用关闭方法
        xYearlyCheckErrorRef.value.close();
    }

    /**
     * 继续使用提交
     */
    function continueUseSubmit() {
        // 此处可写继续使用申请业务代码

        // 最终提交后,使用关闭方法
        xYearlyCheckErrorRef.value.close();
    }

    /**
     * 表获取单值
     */
    function getFormValue() {
        formValue.value = xFormRef.value.getForm();
    }

    return {
        formList,
        sacnValueHandle,
        xFormRef,
        formValue,
        xYearlyCheckErrorRef,
        yearlyCheckSubmit,
        continueUseSubmit,
        date,
    };
}

defineProps`

defineProps只能在seript setup语法糖中使用,和defineExpose一样是编译器宏,不需要导入,可以直接用。接受的值和props接收的值一样,不过defineProps可以使用类型推导。

interface yearlyDateType {
    manufactureDate: string;
    nextInspectionDate: string;
}
withDefaults(
    defineProps<{
        // 日期类型
        date: yearlyDateType;
    }>(),
    {
        date: () => {
            return {
                manufactureDate: '',
                nextInspectionDate: '',
            };
        },
    }
);

总结

总得来说,越整理发现延伸的知识越来越多,此次整理提升不小,文章有写的不好的地方接受批评和指点,嘿嘿嘿。