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

1. 环境安装

bash 复制代码
npm install -g @vue/cli

2. 创建项目

bash 复制代码
npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project

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

arduino 复制代码
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 中 添加配置,确保路径引入正确:

php 复制代码
// 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/[email protected]及以上版本,因为在0.0.9版本开始其内置了wot-design-uniresolver
  • 如果使用此方案时控制台打印很多Sourcemap for points to missing source files,可以尝试将vite版本升级至4.5.x以上版本。
css 复制代码
npm i @uni-helper/vite-plugin-uni-components -D
javascript 复制代码
// 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

java 复制代码
// .npmrc
public-hoist-pattern[]=@vue*
// or
// shamefully-hoist = true

4.3. 安装sass

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

css 复制代码
npm install [email protected] -D

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

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

5. 请求库

5.1. 请求封装

typescript 复制代码
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. 使用示例

javascript 复制代码
import http from  '@/request/http';

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

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

vbscript 复制代码
npm install vue-request
xml 复制代码
<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 文件

javascript 复制代码
import { createPinia } from "pinia";
import { createUnistorage } from "pinia-plugin-unistorage";

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

6.4. 在main.ts中引入

javascript 复制代码
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 文件
javascript 复制代码
// 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 的数据读写都将持久化
  }
);
  1. 在页面上使用
xml 复制代码
<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复制

php 复制代码
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);,它可能会自动转换为以下代码,以确保在不同浏览器中都能正确显示:

css 复制代码
-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 中添加该插件配置如下:

javascript 复制代码
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复制

css 复制代码
.element {
  width: 100px;
  height: 50px;
  font-size: 16px;
}

它可能会被转换为:

css复制

css 复制代码
.element {
  width: 13.33333vmin;
  height: 6.66667vmin;
  font-size: 2.13333vmin;
}

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

7.3. unplugin-auto-import

arduino 复制代码
npm i -D unplugin-auto-import

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

javascript 复制代码
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"

perl 复制代码
{
  "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. 安装

css 复制代码
 npm i unocss unocss-applet -D

8.2. 配置uno.config.ts

在根目录新建 uno.config.ts

php 复制代码
// 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中增加
arduino 复制代码
import 'virtual:uno.css'
  1. 在vite.config.ts中增加
javascript 复制代码
import UnoCSS from 'unocss/vite'

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

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

解决方法:

javascript 复制代码
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 如何使用

ini 复制代码
    <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

bash 复制代码
npm install qs
npm install @types/qs -D

10.2. 封装

  • 在src目录下新建 utils 文件夹
  • 在utils新建index.ts
typescript 复制代码
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

ini 复制代码
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

css 复制代码
: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中引入

scss 复制代码
@import "@/styles/common.scss";

常见问题

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

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

解决方案

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

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

误区

千万不要把所有 json 文件都关联到 jsonc 中,你感觉在 json 中都能写注释了,比以前更好用了,其实不然,json 就是 json,jsonc 就是 jsonc,严格 json 文件写了注释就会报错。

例如,package.json 写了注释在编译的时候,是会报错的,因为 package.json 就是严格 json 文件,不能写注释。

相关推荐
猫猫不是喵喵.3 小时前
vue 路由
前端·javascript·vue.js
bin91534 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例12,TableView16_12 拖拽动画示例
前端·javascript·vue.js·ecmascript·deepseek
拉不动的猪4 小时前
vue自定义“权限控制”指令
前端·javascript·vue.js
咸虾米_4 小时前
uniapp微信小程序获取用户手机号uniCloud云开发版
微信小程序·小程序·uni-app·unicloud·获取手机号
魔云连洲5 小时前
Vue2和Vue3响应式的基本实现
开发语言·前端·javascript·vue.js
JSON_L6 小时前
Vue 组件通信 - Ref组件通信
javascript·vue.js·ecmascript
努力的搬砖人.6 小时前
Vue 2 和 Vue 3 有什么区别
前端·vue.js·经验分享·面试
Fri_7 小时前
Vue 使用 xlsx 插件导出 excel 文件
javascript·vue.js·excel
萌萌哒草头将军7 小时前
🔥🔥🔥4 月 1 日尤雨溪突然宣布使用 Go 语言重写 Rolldown 和 Oxc!
前端·javascript·vue.js