likes
comments
collection
share

搭建 vite + vue3 + ts + pinia 项目框架

作者站长头像
站长
· 阅读数 86

一、创建项目

1. 安装vite

npm i vite -g

2. 创建项目

  • 一步创建
# npm 6.x
npm create vite@latest my-vue-app --template vue-ts
​
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue-ts
​
# yarn
yarn create vite my-vue-app --template vue-ts
​
# pnpm
pnpm create vite my-vue-app --template vue-ts
  • 配置创建
npm init vue@latest

搭建 vite + vue3 + ts + pinia 项目框架

如果安装依赖后运行 npm run dev 报以下错误

搭建 vite + vue3 + ts + pinia 项目框架

解决方法: 更新node版本

nodejs.org/zh-cn/

二、项目基本配置

1. 项目icon

在 public目录 下,添加一个 favicon.icon 图片

2. 项目标题

在 index.html 文件的 title标签 中配置

3. 配置 tsconfig.json

能让 代码提示 变得更加友好

{
  "compilerOptions": {
    // 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
    "allowSyntheticDefaultImports": true,
    // 解析非相对模块名的基准目录
    "baseUrl": ".",
    // 模块加载兼容模式,可以是呀import from语法导入commonJS模块
    "esModuleInterop": true,
    // 从 tslib 导入辅助工具函数(比如 __extends, __rest等)
    "importHelpers": true,
    // 指定生成哪个模块系统代码
    "module": "esnext",
    // 决定如何处理模块。
    "moduleResolution": "node",
    // 启用所有严格类型检查选项。
    // 启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict,
    // --strictNullChecks和 --strictFunctionTypes和--strictPropertyInitialization。
    "strict": true,
    "noImplicitAny": false, //关闭implicitly has an 'any' type
    // 支持jsx语法
    "jsx": "preserve",
    // 生成相应的 .map文件。
    "sourceMap": true,
    // 忽略所有的声明文件( *.d.ts)的类型检查。
    "skipLibCheck": true,
    // 指定ECMAScript目标版本
    "target": "esnext",
    // 要包含的类型声明文件名列表
    "types": [
      "node"
    ],
    "typeRoots": [
      "../node_modules/@types"
    ],
    // isolatedModules 设置为 true 时,如果某个 ts 文件中没有一个import or export 时,ts 则认为这个模块不是一个 ES Module 模块,它被认为是一个全局的脚本,
    "isolatedModules": true,
    // 模块名到基于 baseUrl的路径映射的列表。
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "vueCompilerOptions": {
      "experimentalDisableTemplateSupport": true //去掉volar下el标签红色波浪线问题
    },
    // 编译过程中需要引入的库文件的列表。
    "lib": [
      "ESNext",
      "DOM",
      "DOM.Iterable",
      "ScriptHost"
    ]
  },
  // 解析的文件
  "include": [
    "env.d.ts",
    "src/**/*",
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "src/*.js",
    "src/**/*.jsx"
  ],
  "exclude": [
    "node_modules"
  ],
  "references": [
    {
      "path": "./tsconfig.node.json"
    }
  ]
}

4. 设置 .prettierrc.json 文件

eslint 配置格式化选项说明

  // 1.一行代码的最大字符数,默认是80(printWidth: <int>)
  printWidth: 80,
  // 2.tab宽度为2空格(tabWidth: <int>)
  tabWidth: 2,
  // 3.是否使用tab来缩进,我们使用空格(useTabs: <bool>)
  useTabs: false,
  // 4.结尾是否添加分号,false的情况下只会在一些导致ASI错误的其工况下在开头加分号,我选择无分号结尾的风格(semi: <bool>)
  semi: false,
  // 5.使用单引号(singleQuote: <bool>)
  singleQuote: true,
  // 6.object对象中key值是否加引号(quoteProps: "<as-needed|consistent|preserve>")as-needed只有在需求要的情况下加引号,consistent是有一个需要引号就统一加,preserve是保留用户输入的引号
  quoteProps: 'as-needed',
  // 7.在jsx文件中的引号需要单独设置(jsxSingleQuote: <bool>)
  jsxSingleQuote: false,
  // 8.尾部逗号设置,es5是尾部逗号兼容es5,none就是没有尾部逗号,all是指所有可能的情况,需要node8和es2017以上的环境。(trailingComma: "<es5|none|all>")
  trailingComma: 'es5',
  // 9.object对象里面的key和value值和括号间的空格(bracketSpacing: <bool>)
  bracketSpacing: true,
  // 10.jsx标签多行属性写法时,尖括号是否另起一行(jsxBracketSameLine: <bool>)
  jsxBracketSameLine: false,
  // 11.箭头函数单个参数的情况是否省略括号,默认always是总是带括号(arrowParens: "<always|avoid>")
  arrowParens: 'always',
  // 12.range是format执行的范围,可以选执行一个文件的一部分,默认的设置是整个文件(rangeStart: <int>  rangeEnd: <int>)
  rangeStart: 0,
  rangeEnd: Infinity,
  // 18. vue script和style标签中是否缩进,开启可能会破坏编辑器的代码折叠
  vueIndentScriptAndStyle: false,
  // 19.    endOfLine: "<lf|crlf|cr|auto>" 行尾换行符,默认是lf,
  endOfLine: 'lf',
  // 20.embeddedLanguageFormatting: "off",默认是auto,控制被引号包裹的代码是否进行格式化
  embeddedLanguageFormatting: 'off',
{
  "singleQuote": true,
  "tabWidth": 4,
  "semi": false,
}

5. 设置 vite.config.ts 文件

安装 gzip 和 mock 依赖

npm i vite-plugin-compression vite-plugin-mock -D
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
​
import path from 'path'
// gzip插件
import viteCompression from 'vite-plugin-compression'
// mock插件
import { viteMockServe } from 'vite-plugin-mock'const resolve = (dir) => path.resolve(__dirname, dir)
​
export default defineConfig({
    base: './', //打包路径
    publicDir: resolve('public'), //静态资源服务的文件夹
    plugins: [
        vue(),
        vueJsx(),
        // gzip压缩 生产环境生成 .gz 文件
        viteCompression({
            verbose: true,
            disable: false,
            threshold: 10240,
            algorithm: 'gzip',
            ext: '.gz',
        }),
        //mock
        viteMockServe({
            mockPath: './mocks', // 解析,路径可根据实际变动
            localEnabled: true, // 此处可以手动设置为true,也可以根据官方文档格式
        }),
    ],
    // 配置别名
    resolve: {
        alias: {
            '@': resolve('src'),
        },
        // 导入时想要省略的扩展名列表
        extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
    },
    css: {
        // css预处理器
        preprocessorOptions: {
            scss: {
                additionalData:
                    '@import "@/assets/styles/common.scss";@import "@/assets/styles/reset.scss";',
            },
        },
    },
    //启动服务配置
    server: {
        host: '0.0.0.0',
        port: 8000,
        open: true, // 自动在浏览器打开
        proxy: {},
    },
    // 打包配置
    build: {
        //浏览器兼容性  "esnext"|"modules"
        target: 'modules',
        //指定输出路径
        outDir: 'build',
        //生成静态资源的存放路径
        assetsDir: 'assets',
        //启用/禁用 CSS 代码拆分
        cssCodeSplit: true,
        sourcemap: false,
        assetsInlineLimit: 10240,
        // 打包环境移除console.log, debugger
        minify: 'terser',
        terserOptions: {
            compress: {
                drop_console: true,
                drop_debugger: true,
            },
        },
        rollupOptions: {
            input: {
                main: resolve('index.html'),
            },
            output: {
                entryFileNames: `js/[name]-[hash].js`,
                chunkFileNames: `js/[name]-[hash].js`,
                assetFileNames: `[ext]/[name]-[hash].[ext]`,
            },
        },
    },
})
​

三、项目目录结构划分

    1. assets 存放 => 静态资源
    • css => 样式重置
    • img => 图片文件
    • font => 字体文件
    1. components 存放 => 公共组件
    1. hooks 存放 => 公共常用的hook
    1. mock 存放 => 模拟接口数据
    1. router 存放 => 路由管理
    1. service 存放 => 接口请求
    1. stores 存放 => 状态管理
    1. utils 存放 => 插件、第三方插件
    1. views 存放 => 视图、页面

四、css 样式重置

自定义的css公共文件放置在assets中的css文件中即可

1. normalize.css

01 - 安装

npm i normalize.css

02 - 引入

// 在 main.js 中引入
import 'normalize.css';

2. reset.css

01 - 代码

html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
font,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
caption {
    margin: 0;
    padding: 0;
    border: 0;
    outline: 0;
    font-size: 100%;
    vertical-align: baseline;
    background: transparent;
}
​
table,
tbody,
tfoot,
thead,
tr,
th,
td {
    margin: 0;
    padding: 0;
    outline: 0;
    font-size: 100%;
    vertical-align: baseline;
    background: transparent;
}
​
button,
input,
textarea {
    margin: 0;
    padding: 0;
}
​
/* form elements 表单元素 */
body,
button,
input,
select,
textarea {
    font: normal 12px/1.5 '\5FAE\8F6F\96C5\9ED1', tahoma, arial;
}
​
/*设置的字体,行高*/
h1,
h2,
h3,
h4,
h5,
h6,
th {
    font-size: 100%;
    font-weight: normal;
}
​
/*重置标题*/
address,
cite,
dfn,
var {
    font-style: normal;
}
​
/* 将斜体扶正 */
code,
kbd,
pre,
samp {
    font-family: 'courier new', courier, monospace;
}
​
/* 统一等宽字体 */
small {
    font-size: 12px;
}
​
/* 小于 12px 的中文很难阅读,让 small 正常化 */
ul,
ol {
    list-style: none;
}
​
/* 重置列表元素 */
button,
input[type="submit"],
input[type="button"] {
    cursor: pointer;
}
​
input[type="radio"],
input[type="checkbox"],
input[type="submit"],
input[type="reset"] {
    vertical-align: middle;
    cursor: pointer;
    border: none;
}
​
/** 重置文本格式元素 **/
a {
    text-decoration: none;
}
a:hover {
    text-decoration: underline;
}
a:focus {
    outline: 0;
}
sup {
    vertical-align: text-top;
}
​
/* 重置,减少对行高的影响 */
sub {
    vertical-align: text-bottom;
}
​
/** 重置表单元素 **/
legend {
    color: #000;
}
​
/* for ie6 */
fieldset,
img {
    border: 0;
}
​
/* img 搭车:让链接里的 img 无边框 */
button,
input,
select,
textarea {
    background: transparent;
    font-size: 100%;
    outline: 0;
}
​
/* 使得表单元素在 ie 下能继承字体大小 */
/* 注:optgroup 无法扶正 */
table {
    border-collapse: collapse;
    border-spacing: 0;
}
​
td,
th {
    vertical-align: middle;
}
​
/** 重置表格元素 **/
/* 重置 HTML5 元素 */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section,
summary,
time,
mark,
audio,
video {
    display: block;
    margin: 0;
    padding: 0;
}
​
/*回复标签重置*/
blockquote,
q {
    quotes: none;
}
​
blockquote:before,
blockquote:after,
q:before,
q:after {
    content: '';
    display: none;
}

02 - 引入

// 在 main.js 中引入
import './assets/css/reset.css';

3. common.css

01 - 代码

// 清除浮动
.clearfix {
  *zoom: 1;
}
 
......

02 - 引入

// 在 main.js 中引入
import './assets/css/common.css';

五、vue-router 路由配置

一步创建需要安装依赖、配置路由, 引入mian.ts, 配置创建则已自动生成

1. 安装

npm i vue-router

2. 配置

// 1. 导入
import { createRouter, createWebHashHistory } from 'vue-router';
​
// 2. 创建路由对象
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: () => import('xxx/home.vue')
        }
    ]
});
​
// 3. 导出
export default router;

3. 引入

// main.js
 
import { createApp } from 'vue';
import App from './App.vue';
// 1. 导入
import router from './router';
 
import 'normalize.css';
import './assets/css/reset.css';
import './assets/css/common.css';
 
// 2. 使用
createApp(App).use(router).mount('#app');

4. 使用

在该用的地方加上

六、pinia 状态管理

一步创建需要安装依赖、配置路由, 引入mian.ts, 配置创建则已自动生成

1. 安装

 npm i pinia

2. 引入

// main.js
import { createApp } from 'vue';
import { createPinia } from "pinia";
​
import App from './App.vue';
// 1. 导入
import router from './router';
​
import 'normalize.css';
import './assets/css/reset.css';
import './assets/css/common.css';
​
// 2. 使用
createApp(App).use(createPinia()).use(router).mount('#app');

3. 模块

// 1. 导入
import { defineStore } from 'pinia';
​
// 2. 使用
const useDemoStore = defineStore('demoStore', {
    state: () => ({
        arrList: []
    }),
    actions: {},
    getters: {}
});
​
// 3. 导出
export default useDemoStore;

七、集成 Axios HTTP 工具

安装依赖

npm i axios

请求配置

在 utils 目录下创建 request.ts 文件,配置好适合自己业务的请求拦截和响应拦截

import axios, { AxiosRequestConfig, Method } from 'axios';

// 创建请求实例  
const instance = axios.create({
    baseURL: '/api',
    // 指定请求超时的毫秒数  
    timeout: 10000,
    // 表示跨域请求时是否需要使用凭证  
    withCredentials: false,
});

// 设置请求头
instance.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8';
instance.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded';
// instance.defaults.headers.put['Content-Type'] = 'application/json';


// 取消重复请求
const pending = [];

// 定义接口
interface PendingType {
    url?: string;
    method?: Method;
    params: any;
    data: any;
    cancel: any;
}

// 移除重复请求
const removePending = (config: AxiosRequestConfig) => {
    for (const key in pending) {
        const item: number = +key;
        const list: PendingType = pending[key];
        // 当前请求在数组中存在时执行函数体
        if (list.url === config.url && list.method === config.method && JSON.stringify(list.params) === JSON.stringify(config.params) && JSON.stringify(list.data) === JSON.stringify(config.data)) {
            // 执行取消操作
            list.cancel('操作太频繁,请稍后再试');
            // 从数组中移除记录
            pending.splice(item, 1);
        }
    }
};


// 请求拦截器(发起请求之前的拦截)  
instance.interceptors.request.use(
    (config): AxiosRequestConfig<any> => {
        removePending(config);
        config.cancelToken = new axios.CancelToken(c => {
            pending.push({ url: config.url, method: config.method, params: config.params, data: config.data, cancel: c });
        });
        /**  
         * 在这里一般会携带前台的参数发送给后台,比如下面这段代码:  
         * const token = getToken()  
         * if (token) {  
         *  config.headers.token = token  
         * }  
         */
        return config;
    },
    (error) => {
        return Promise.reject(error);
    },
);

// 响应拦截器(获取到响应时的拦截)  
instance.interceptors.response.use(
    (response) => {
        removePending(response.config);
        /**  
         * 根据你的项目实际情况来对 response 和 error 做处理  
         * 这里对 response 和 error 不做任何处理,直接返回  
         */
        return response;
    },
    (error) => {
        return Promise.reject(error);
    },
);

interface ResType<T> {
    code: number;
    data?: T;
    msg?: string;
    message?: string;
    err?: string;
}

interface Http {
    post<T>(url: string, data?: unknown, params?: unknown,): Promise<ResType<T>>;
    get<T>(url: string, params?: unknown): Promise<ResType<T>>;
    put<T>(url: string, data?: unknown, params?: any): Promise<ResType<T>>;
    _delete<T>(url: string, params?: unknown): Promise<ResType<T>>;
}

// 导出常用函数  
const http: Http = {
    post(url, data, params) {
        return new Promise((resolve, reject) => {
            instance
                .post(url, JSON.stringify(data), params)
                .then((res) => {
                    resolve(res.data);
                })
                .catch((err) => {
                    reject(err.data);
                });
        });
    },
    get(url, params) {
        return new Promise((resolve, reject) => {
            instance
                .get(url, { params })
                .then((res) => {
                    resolve(res.data);
                })
                .catch((err) => {
                    reject(err.data);
                });
        });
    },
    put(url, data, params) {
        return new Promise((resolve, reject) => {
            instance
                .put(url, data, params)
                .then((res) => {
                    resolve(res.data);
                })
                .catch((err) => {
                    reject(err.data);
                });
        });
    },
    _delete(url, params) {
        return new Promise((resolve, reject) => {
            instance
                .delete(url, params)
                .then((res) => {
                    resolve(res.data);
                })
                .catch((err) => {
                    reject(err.data);
                });
        });
    }
}

export default http;

之后在 api 文件夹中以业务模型对接口进行拆分,举个例子,将所有跟用户相关接口封装在 User 类中,此类称作用户模型。

在 User 类中比如有登录、注册、获取用户信息等方法,如果有业务逻辑变动,只需要修改相关方法即可。

import { post } from '@/utils/request';  
  
export default class User {  
  /**  
   * 登录  
   * @param {String} username 用户名  
   * @param {String} password 密码  
   * @returns  
   */  
  static async login(username: string, password: string) {  
    return post('/login', {  
      username,  
      password,  
    });  
  }  
}

把每个业务模型独立成一个 js 文件,声明一个类通过其属性和方法来实现这个模型相关的数据获取,这样可以大大提升代码的可读性与可维护性。

模拟演示

在需要使用接口的地方,引入对应的业务模型文件,参考如下

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


const username = ref('')
const password = ref('')

const login = async () => {  
      const res = await User.login(username.value, password.value);  
      console.log(res);  
},
</script>

八、使用scss, 并定义全局scss变量

首先我们先安装sass和sass-loader:

npm i sass sass-loader -D

然后我们需要在vite.config.ts中配置css预处理器

export default defineConfig({
    css: {
        preprocessorOptions: {
            scss: {
                additionalData: '@import "@/assets/styles/global.scss";@import "@/assets/styles/reset.scss";',
            },
        }
    }
})

我们这里默认加载global.scss中的样式,那么我们就需要创建一个这样的文件:

// src/assets/style/global.scss
$primary-color: #5878e2; // 主题色

最后在main.ts中引入即可:

import "./assets/style/global.scss";

然后在组件中使用时,就可以直接使用:

<script setup lang="ts">
import {GlobalStore} from '@/store'
const global = GlobalStore();
</script>
<template>
    <div>{{global.token}}</div>
</template>
<style scoped lang="scss">
div {
    color: $primary-color; // 主题色
}
</style>

样式穿透 在 Vue3 中,改变了以往样式穿透的语法,如果继续使用 ::v-deep/deep/>>> 等语法的话,会出现一个警告,下面是新的语法:

/* 深度选择器 */  
:deep(selector) {  
  /* ... */  
}  
  
/* 插槽选择器 */  
:slotted(selector) {  
  /* ... */  
}  
  
/* 全局选择器 */  
:global(selector) {  
  /* ... */  
}
转载自:https://juejin.cn/post/7193610176425885733
评论
请登录