uniapp新项目基建太麻烦? 那就搭建一个自己的启动模板(cli模式)

56 阅读2分钟

1. 环境安装

npm install -g @vue/cli

2. 创建项目

npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project

3. 运行项目(以h5为例)

npm install
npm run dev:H5

4. ui框架

4.1 wot-design-uni

文档地址: wot-design-uni.cn/

npm install wot-design-uni

4.2 如何使用

方式一:easycom组件规范 (推荐)

传统vue组件,需要安装、引用、注册,三个步骤后才能使用组件。easycom将其精简为一步,如果不了解easycom,可先查看 官网文档

pages.json 中 添加配置,确保路径引入正确:

// pages.json
{
	"easycom": {
		"autoscan": true,
		"custom": {
		  "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue"
		}
	},
	
	// 此为本身已有的内容
	"pages": [
		// ......
	]
}

方式二: 基于vite配置自动引入组件 (个人不推荐)

如果不熟悉easycom,也可以通过@uni-helper/vite-plugin-uni-components实现组件的自动引入。

  • 推荐使用@uni-helper/vite-plugin-uni-components@0.0.9及以上版本,因为在0.0.9版本开始其内置了wot-design-uniresolver
  • 如果使用此方案时控制台打印很多Sourcemap for points to missing source files,可以尝试将vite版本升级至4.5.x以上版本。
npm i @uni-helper/vite-plugin-uni-components -D
// vite.config.ts
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";

import Components from '@uni-helper/vite-plugin-uni-components'
import { WotResolver } from '@uni-helper/vite-plugin-uni-components/resolvers'


export default defineConfig({
  plugins: [
    // make sure put it before `Uni()`
    Components({
      resolvers: [WotResolver()]
    }), uni()],
});

如果你使用 pnpm ,请在根目录下创建一个 .npmrc 文件,参见issue

// .npmrc
public-hoist-pattern[]=@vue*
// or
// shamefully-hoist = true

4.3. 安装sass

Wot Design Uni 依赖 sass ,因此在使用之前需要确认项目中是否已经安装了 sass,如果没有安装,可以通过以下命令进行安装:

npm install sass@1.78.0 -D

Dart Sass 3.0.0 废弃了一批API,而组件库目前还未兼容,因此请确保你的sass版本为1.78.0及之前的版本。

重新运行项目,在页面上引入一个按钮测试一下

5. 请求库

5.1. 请求封装

const targetUrl = "https://xxx.com/api";

class Request {
  private baseUrl: string;
  private defaultHeaders: { [key: string]: string };
  private config: { [key: string]: any };
  // 构造函数
  constructor(baseUrl?: string, defaultHeaders?: { [key: string]: string }) {
    this.baseUrl = baseUrl || targetUrl;
    this.defaultHeaders = {
      "Content-Type": "application/json",
      ...defaultHeaders,
    };
    this.config = {
      method: "GET",
      dataType: "json",
      responseType: "text",
      timeout: 10000,
    };
  }

  // 设置请求头
  private setHeaders(headers: { [key: string]: string }) {
    // 合并默认请求头和传入的请求头
    const mergedHeaders = Object.assign({}, this.defaultHeaders, headers || {});

    // 获取本地存储的ticket或从store中获取
    const ticket = uni.getStorageSync("ticket");
    if (ticket) {
      mergedHeaders["ticket"] = ticket;
    }
    return mergedHeaders;
  }
  // 请求拦截器
  private requestInterceptor(config: any) {
    config.header = this.setHeaders(config?.header || {});
    config.url = this.baseUrl + config.url;
    return config;
  }
  // 响应拦截器
  private responseInterceptor(response: any) {
    if (response.statusCode === 401) {
      uni.navigateTo({ url: "/pages/user/login" });
      throw new Error("用户未登录");
    }
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return response.data.data;
    } else {
      throw new Error(`请求失败,状态码:${response.statusCode}`);
    }
  }

  // 统一错误处理
  private handleError(error: Error) {
    uni.showToast({
      icon: "none",
      title: error.message,
    });
    console.error(error);
    throw error;
  }
  // 请求方法
  private request(options: any) {
    const mergedConfig = { ...this.config, ...options };

    const interceptedRequestConfig = this.requestInterceptor(mergedConfig);

    return new Promise((resolve, reject) => {
      uni.request({
        ...interceptedRequestConfig,
        success: (res) => {
          try {
            const data = this.responseInterceptor(res);
            resolve(data);
          } catch (error) {
            this.handleError(error);
          }
        },
        fail: (err) => {
          this.handleError(new Error("网络请求失败"));
        },
      });
    });
  }

  public get(url: string, options: any = {}) {
    return this.request({ url, method: "GET", ...options });
  }

  public post(url: string, data?: any, options: any = {}) {
    return this.request({ url, method: "POST", data, ...options });
  }

  public put(url: string, data?: any, options: any = {}) {
    return this.request({ url, method: "PUT", data, ...options });
  }

  public delete(url: string, data?: any, options: any = {}) {
    return this.request({ url, method: "DELETE", data, ...options });
  }
}

const http  = new Request();

export default http;

5.2. 使用示例

import http from  '@/request/http';

export const getListApi = () => {
    return http.get("/test/banner");
  };

5.3. 使用vue-request管理接口示例

npm install vue-request
<template>
  <view class="content">
    <wd-button @click="refresh">测试刷新api</wd-button>
    <text>列表</text>
    <view v-if="loading">加载中...</view>
    <template v-else>
      <view v-for="item in list" :key="item._id">
        <text>{{ item.url }}</text>
      </view>
    </template>
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { getListApi } from "@/api/wallpaper";

// // 使用useRequest
import { useRequest } from 'vue-request';
const { data: list, runAsync: getList, loading, refresh } = useRequest(getListApi);

// 不使用useRequest
// const list = ref([])
// const loading = ref(false)
// const getList = async () => {
//   loading.value = true
//   const res = await getListApi().finally(() => {
//     loading.value = false
//   })
//   list.value = res
// }
// const refresh = () => {
//   getList()
// }


</script>

6. 状态管理

6.1. 安装pinia

npm install pinia

6.2. 安装持久化

npm install pinia-plugin-unistorage 

也可以选择使用 pinia-plugin-persistedstate

6.3. 在src下创建一个store文件夹并创建 index.ts 文件

import { createPinia } from "pinia";
import { createUnistorage } from "pinia-plugin-unistorage";

const pinia = createPinia();
pinia.use(createUnistorage());
export default pinia;

6.4. 在main.ts中引入

import { createSSRApp } from "vue";
import App from "./App.vue";
import pinia from '@/store'  // 从store文件夹中引入
export function createApp() {
  const app = createSSRApp(App);
  app.use(pinia)  // 使用
  return {
    app,
  };
}

6.5. 测试缓存效果

  1. 在store下创建 user.ts 文件
// useUserStore.ts
import { defineStore } from "pinia";
import { ref } from "vue";

export const useUserStore = defineStore(
  "userStore",
  () => {
    const userInfo = ref({
      name: "Tom",
      age: 18,
    });

    const getUserInfo = async () => {
      setTimeout(() => {
        // 随机返回一个用户信息
        // 生成一个随机数,拼接到用户信息中
        const random = Math.floor(Math.random() * 100);
        userInfo.value = {
          name: `Tom${random}`,
          age: random,
        };
      });
    };
    return {
      userInfo,
      getUserInfo,
    };
  },
  {
    unistorage: true, // 开启后对 state 的数据读写都将持久化
  }
);

2. 在页面上使用

<template>
  <view class="content">
    <image class="logo" src="/static/logo.png" />
    <view class="text-area">
      <text class="title">{{ title }}</text>
      <text class="title">{{  userInfo.name }}</text>
    </view>
    <wd-button @click="getUserInfo">获取用户信息</wd-button>
  </view>
</template>

<script setup lang="ts">
  import { ref } from 'vue'
  const title = ref('zero-starter')
  import { useUserStore } from "@/store/user";
  import { storeToRefs } from "pinia";
  const userStore = useUserStore()
  const { userInfo } = storeToRefs(useUserStore());
  // 测试store缓存,用户信息
  const getUserInfo = () => {
    userStore.getUserInfo()
  }

</script>

7. 提效神器

7.1. autoprefixer

autoprefixer 是一个自动为 CSS 添加浏览器前缀的工具,可以确保你的 CSS 在不同浏览器中兼容。在 vite.config.ts 中添加 autoprefixer 插件配置如下:

TypeScript复制

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

export default defineConfig({
  plugins: [uni()],
  css: {
    postcss: {
      plugins: [
        require("autoprefixer")(),
      ],
    },
  },
});

插件效果:当你在项目中编写 CSS 代码时,autoprefixer 会自动检测你的 CSS 属性,并根据目标浏览器的兼容性要求,为这些属性添加必要的浏览器前缀。例如,如果你编写了 transform: rotate(45deg);,它可能会自动转换为以下代码,以确保在不同浏览器中都能正确显示:

-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);

7.2. postcss-px-to-viewport-8-plugin

postcss-px-to-viewport-8-plugin 是一个将 px 单位转换为视口单位(如 vw、vh、vmin 等)的插件,有助于实现响应式设计。在 vite.config.ts 中添加该插件配置如下:

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

export default defineConfig({
  plugins: [uni()],
  css: {
    postcss: {
      plugins: [
        require("postcss-px-to-viewport-8-plugin")({
          unitToConvert: "px", // 需要转换的单位,默认为"px"
          viewportWidth: 750, // 视窗的宽度,对应设计稿的宽度
          unitPrecision: 5, // 单位转换后保留的精度
          propList: ["*"], // 能转化为视口单位的属性列表
          viewportUnit: "vmin", // 希望使用的视口单位
          fontViewportUnit: "vmin", // 字体使用的视口单位
          selectorBlackList: ["is-checked"], // 需要忽略的CSS选择器
          minPixelValue: 1, // 设置最小的转换数值
          mediaQuery: false, // 媒体查询里的单位是否需要转换
          replace: true, // 是否直接更换属性值,而不添加备用属性
          exclude: /(/|\)(node_modules|uni_modules)(/|\)/, // 忽略某些文件夹下的文件
        }),
      ],
    },
  },
});

插件效果:当你在项目中编写以 px 为单位的 CSS 属性时,该插件会根据配置自动将其转换为视口单位。例如,如果你编写了以下代码:

css复制

.element {
  width: 100px;
  height: 50px;
  font-size: 16px;
}

它可能会被转换为:

css复制

.element {
  width: 13.33333vmin;
  height: 6.66667vmin;
  font-size: 2.13333vmin;
}

这样,当用户在不同设备上查看页面时,元素的尺寸会根据视口大小自动调整,从而实现更好的响应式效果。

7.3. unplugin-auto-import

npm i -D unplugin-auto-import

在 vite.config.ts中添加 AutoImport 配置

import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import AutoImport from "unplugin-auto-import/vite";
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    uni(),
    AutoImport({
      include: [
        /.[tj]sx?$/, // .ts, .tsx, .js, .jsx
        /.vue$/,
        /.vue?vue/, // .vue
      ],
      imports: [
        "vue",
        "uni-app",
        "pinia",
        {
          "vue-request": ["useRequest"],
        },
      ],
      dts: "./auto-imports.d.ts",
      dirs: ["./src/utils/**"], // 自动导入 utils 中的方法
      eslintrc: {
        enabled: false, // Default `false`
        // provide path ending with `.mjs` or `.cjs` to generate the file with the respective format
        filepath: "./.eslintrc-auto-import.json", // Default `./.eslintrc-auto-import.json`
        globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
      },
      vueTemplate: true, // default false
    }),
  ],
  css: {
    postcss: {
      plugins: [
        require("postcss-px-to-viewport-8-plugin")({
          unitToConvert: "px", // 需要转换的单位,默认为"px"
          viewportWidth: 750, // 视窗的宽度,对应pc设计稿的宽度,一般是1920
          // viewportHeight: 1080,// 视窗的高度,对应的是我们设计稿的高度
          unitPrecision: 5, // 单位转换后保留的精度
          propList: [
            // 能转化为vw的属性列表
            "*",
          ],
          viewportUnit: "vmin", // 希望使用的视口单位
          fontViewportUnit: "vmin", // 字体使用的视口单位
          selectorBlackList: ["is-checked"], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。cretae
          minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
          mediaQuery: false, // 媒体查询里的单位是否需要转换单位
          replace: true, // 是否直接更换属性值,而不添加备用属性
          exclude: /(/|\)(node_modules|uni_modules)(/|\)/, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
        }),
        require("autoprefixer")(),
      ],
    },
  },
});

7.3.1 可能存在的问题

1. 解决eslint报错,在tsconfig.json文件的 "include"数组中添加 "./auto-imports.d.ts"

{
  "extends": "@vue/tsconfig/tsconfig.json",
  "compilerOptions": {
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "lib": ["esnext", "dom"],
    "types": ["@dcloudio/types"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","./auto-imports.d.ts"]
}

8. unocss

8.1. 安装

 npm i unocss unocss-applet -D

8.2. 配置uno.config.ts

在根目录新建 uno.config.ts

// uno.config.ts
import {
    type Preset,
    type SourceCodeTransformer,
    presetUno,
    defineConfig,
    presetAttributify,
    presetIcons,
    transformerDirectives,
    transformerVariantGroup,
  } from 'unocss'
  
  import {
    presetApplet,
    presetRemRpx,
    transformerApplet,
    transformerAttributify,
  } from 'unocss-applet'
  
  // @see https://unocss.dev/presets/legacy-compat
//   import presetLegacyCompat from '@unocss/preset-legacy-compat'
  
  const isMp = process.env?.UNI_PLATFORM?.startsWith('mp') ?? false
  
  const presets: Preset[] = []
  const transformers: SourceCodeTransformer[] = []
  if (isMp) {
    // 使用小程序预设
    presets.push(presetApplet(), presetRemRpx())
    transformers.push(transformerApplet())
  } else {
    presets.push(
      // 非小程序用官方预设
      presetUno(),
      // 支持css class属性化
      presetAttributify(),
    )
  }
  export default defineConfig({
    presets: [
      ...presets,
      // 支持图标,需要搭配图标库,eg: @iconify-json/carbon, 使用 `<button class="i-carbon-sun dark:i-carbon-moon" />`
      presetIcons({
        scale: 1.2,
        warn: true,
        extraProperties: {
          display: 'inline-block',
          'vertical-align': 'middle',
        },
      }),
      // 将颜色函数 (rgb()和hsl()) 从空格分隔转换为逗号分隔,更好的兼容性app端,example:
      // `rgb(255 0 0)` -> `rgb(255, 0, 0)`
      // `rgba(255 0 0 / 0.5)` -> `rgba(255, 0, 0, 0.5)`
    //   presetLegacyCompat({
    //     commaStyleColorFunction: true,
    //   }) as Preset,
    ],
    /**
     * 自定义快捷语句
     * @see https://github.com/unocss/unocss#shortcuts
     */
    shortcuts: [
      ['center', 'flex justify-center items-center'],
      ['text-primary', 'text-yellow'],
    ],
    transformers: [
      ...transformers,
      // 启用 @apply 功能
      transformerDirectives(),
      // 启用 () 分组功能
      // 支持css class组合,eg: `<div class="hover:(bg-gray-400 font-medium) font-(light mono)">测试 unocss</div>`
      transformerVariantGroup(),
      // Don't change the following order
      transformerAttributify({
        // 解决与第三方框架样式冲突问题
        prefixedOnly: true,
        prefix: 'fg',
      }),
    ],
    rules: [
      [
        'p-safe',
        {
          padding:
            'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)',
        },
      ],
      ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }],
      ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }],
    ],
  })
  
  /**
   * 最终这一套组合下来会得到:
   * mp 里面:mt-4 => margin-top: 32rpx
   * h5 里面:mt-4 => margin-top: 1rem
   */

8.3 引入

  1. 在 main.ts中增加
import 'virtual:uno.css'

2. 在vite.config.ts中增加

import UnoCSS from 'unocss/vite'

export default defineConfig({
  plugins: [
    uni(),
    UnoCSS(),
    ]
    // 省略其他配置项
 })

以上写可能会出现报错,原因如下: unocss0.59.x已经不支持commonjs了,仅仅支持ESM(只使用 ESM 来管理模块依赖,不再支持 CommonJS 或 AMD 等其他模块化方案),可以查看《unocss的发布变更日志》:

解决方法:

import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import AutoImport from "unplugin-auto-import/vite";

export default async () => {
  const UnoCSS = (await import('unocss/vite')).default

  return defineConfig({
    plugins: [
      uni(),
      UnoCSS(),
      ...

8.4 安装 iconify

在使用 iconify 之前需要安装对应的图标库,安装格式如下:

npm i -D @iconify-json/[the-collection-you-want]

这里选择的是 carbon 

执行 npm i -D @iconify-json/carbon 即可。

8.4.1 如何使用

    <view class="i-carbon-user-avatar"/>
    <view class="i-carbon-hexagon-outline text-orange"/>

8.5 unocss相关文档

9. 工具库

9.1 安装常用工具库 dayjs , radash

npm install dayjs radash

10. 统一管理路由跳转

10.1. 安装qs

npm install qs
npm install @types/qs -D

10.2. 封装

  • 在src目录下新建 utils 文件夹
  • 在utils新建index.ts
import qs from 'qs';

export const showLoading = (title?: string, options?: UniNamespace.ShowLoadingOptions) => {
  uni.showLoading({ title, mask: true, ...options })
}

export const hideLoading = () => {
  uni.hideLoading()
}

export const showToast = (title: string, options?: UniNamespace.ShowToastOptions) => {
  uni.showToast({ title, icon: 'none', duration: 1800, mask: false, ...options })
}

export const appendQueryString = (url: string, obj?: Record<string, any>): string => {
  // 检查对象是否为空
  if (!obj || Object.keys(obj).length === 0) {
    return url;
  }
  const queryString = qs.stringify(obj);
  // 检查URL是否已经包含查询字符串
  const separator = url.includes('?') ? '&' : '?';
  return `${url}${separator}${queryString}`;
}

10.3. 统一页面跳转管理,在utils新建router.ts

import { throttle } from "radash";

type ParamsType = Record<string, string | number>;
type RedirectType = "push" | "replace" | "reload" | "switchTab";

const redirect = throttle(
    {
        interval: 500,
    },
    (url: string, options?: {
        params?: ParamsType;
        type?: RedirectType;
    } & Omit<UniNamespace.NavigateToOptions, "url">) => {
        const params = options?.params;
        const type = options?.type || "push";

        // 可以在此增加跳转拦截




        if (type === "push") {
            return uni.navigateTo({
                url: appendQueryString(url, params),
                ...options,
            });
        }

        if (type === "replace") {
            return uni.redirectTo({
                url: appendQueryString(url, params),
                ...options,
            });
        }

        if (type === "reload") {
            return uni.reLaunch({
                url: appendQueryString(url, params),
                ...options,
            });
        }

        if (type === "switchTab") {
            return uni.switchTab({
                url: appendQueryString(url, params),
                ...options,
            });
        }
    }
);

export const $$goBack = (
    delta: number = 1,
    options?: UniNamespace.NavigateBackOptions
) => {
    return uni
        .navigateBack({
            delta,
            ...options,
        })
        .catch((err) => {
            return $$goHome();
        });
};
export const $$goHome = () => {
    return uni.switchTab({
        url: "/pages/index/index",
    });
};
export const $$goWebview = (targetUrl: string) => {
    return redirect("/pages/webview/index", {
        params: {
            url: targetUrl,
        },
    });
};

11. 公共样式

11.1. 在src目录下新建styles文件夹

11.2. 在styles下新建common.scss

:not(not) {
    /* *所有选择器 参考 https://ask.dcloud.net.cn/article/36055  */
    box-sizing: border-box;
  }
  // 重置微信小程序的按钮样式
  button {
    background-color: transparent;
  }
  button:after {
    border: none;
  }
  
  image {
    display: block;
  }
  
  page {
    font-size: 32px;
    color: #333333;
    background: #f8f8f8;
  }

11.3. 在uni.scss中引入

@import "@/styles/common.scss";

12. 源码地址

ext.dcloud.net.cn/plugin?id=2…

常见问题

1. 用 VsCode 开发 uni-app 项目时,报错JSON 中不允许有注释

VsCode 开发 uni-app 项目时,我们打开 pages.jsonmanifest.json,发现会报红,这是因为在 json 中是不能写注释的,而在 jsonc 是可以写注释的。
jsoncc 就是 comment 【注释】的意思。

解决方案

我们把 pages.jsonmanifest.json 这两个文件指定使用 jsonc 的语法即可,然后就以写注释了。在设置中打开 settings.json,添加配置:

// 配置语言的文件关联
  "files.associations": {
    "pages.json": "jsonc",
    "manifest.json": "jsonc",
  },

误区

千万不要把所有 json 文件都关联到 jsonc 中,你感觉在 json 中都能写注释了,比以前更好用了,其实不然,json 就是 json,jsonc 就是 jsonc,严格 json 文件写了注释就会报错。
例如,package.json 写了注释在编译的时候,是会报错的,因为 package.json 就是严格 json 文件,不能写注释。