Skip to content

🍰🤡 Vue3.0 + Typescript + Sass + Vant 移动端 ,附属微信朋友圈demo 。访问weizhan.site

Notifications You must be signed in to change notification settings

weizhanzhan/vue3-ts-template-h5

Repository files navigation

vue3-template-h5

Vue3.0 + Typescript 初体验,打造h5,webapp移动端模板,开箱即用

🚀 Browse online

📖 Vue3.0 相关文档:

相关库名称 在线地址 🔗
Vue3.0 官方文档 链接
Vue3.0 中文文档 链接 国内链接
Composition-API手册 链接
Vue3.0 源码学习 链接
Vue-Router 官方文档 链接
Vuex4.0(目前在 beta 阶段) Github

🚦注意

遇到安装依赖启动报错的升级一下Typescript npm install typescript@4.5.3 --dev

🎹目录

基础搭建

  • vue3配置
# 1.安装vue-cli next
npm install --global @vue/cli@next

# 2.创建项目,创建选择模板的时候,选择“Manually select features",下面有我的options,仅供参考
vue create my-project-name

# 如果已经有了一个cli项目不是TypeScript,可以增加一个cli的插件
vue add typescript

My Vue CLI Option

Vue CLI v4.5.4

  1. Please pick a preset: Manually select features
  2. Check the features needed for your project: Choose Vue version, Babel, TS, Router, Vuex, CSS Pre-processors, Linter
  3. Choose a version of Vue.js that you want to start the project with 3.x (Preview)
  4. Use class-style component syntax? Yes
  5. Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
  6. Use history mode for router? (Requires proper server setup for index fallback in production) Yes
  7. Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
  8. Pick a linter / formatter config: Prettier
  9. Pick additional lint features: Lint on save
  10. Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys) In dedicated config files

vue3的新特性和改动

v-model

2.x语法

在 2.x 中,在组件上使用 v-model 相当于绑定 value prop 和 input 事件:

<child-component v-model="title" />
<!-- 语法糖 默认mode prop:value  event:input-->
<child-component :value="title" @input="title = $event"/>

使用v-bind:sync vue是单向数据流,为了对prop进行"双向绑定",可以是用sync来实现

<child-component :title.sync="title" />
<!-- 语法糖 -->
<child-component :title="title" @update:title="title = $event"/>

子组件内通过下面方式通知父父组件

this.$emit('update:title',value)

3.x语法

在3.x中自定义组件的v-model,是传递了modelValueprop并接受抛出的update:modelValue事件,和sync很像

<child-component v-model="title" />
<!-- 语法糖 -->
<child-component :modelValue="title" @update:modelValue="title = $event"/>

v-model参数 若需要更改 model 名称,而不是更改组件内的 model 选项,而是将一个 argument 传递给 model

<child-component v-model:title="pageTitle" />
<!-- 简写: -->
<child-component :title="title" @update:title="title = $event" />

因此我们直接可以2.x的sync改成现在这种写法

<child-component :title.sync="title" />
<!-- 替换为 -->
<child-component v-model:title="title" />

并且一个子组件我们可以写多个v-model

<child-component v-model:title="pageTitle" v-model:content="content"/>

v-for

v-for的变动最主要体现在key上面,我们在使用<template v-for>的时候,2.x语法的key值不可以加在template标签上面,要加到子节点上,而在3.x中则可以加到template上,并且不需要在子节点上添加key

<!-- Vue 2.x -->
<template v-for="item in list">
  <div :key="item.id">...</div>
  <span :key="item.id">...</span>
</template>

<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>

ref

在2.0中,在v-for中绑定ref,我们通过$ref,获取的是一个ref数组,在3.x中则不会自动创建数组,我们需要绑定一个函数,自己处理并接受它

<div v-for="item in list" :ref="setItemRef"></div>
export default {
  setup() {
    //itemRefs 不必是数组:它也可以是一个对象,其 ref 会通过迭代的 key 被设置。
    let itemRefs = []
    const setItemRef = el => {
      itemRefs.push(el)
    }
    return {
      itemRefs,
      setItemRef
    }
  }
}

也可以

<template>
  <div ref="box">
    I am div
  </div>
</template>

<script>
import { onMounted, ref } from 'vue';


export default {
  name: 'App',
  setup() {
    let box = ref(null);

    // 由于 template 中的 div 属性 ref 引用了一个对象 box,因此 box 将与这个 div 执行绑定。
    // 但由于 setup 执行时期,还未创建实际的 div,所以如果要进行与 box 的交互,必须在生命周期中间执行获取。
    // onMounted() 中的行为会在声明周期 mounted 中执行。
    onMounted(() => {
      console.log('box.value', box.value);
    })


    return {box};
  }
}
</script>

整理中...(敬请期待😄)

CompositionApi

VUE 3 COMPOSITION API CHEAT SHEET

<template>
  <div>
    <p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p>
    <h2>Attending</h2>
    <ul>
      <li v-for="(name, index) in attending" :key="index">
        {{ name }}
      </li>
    </ul>
    <button @click="increaseCapacity()">Increase Capacity</button>
  </div>
</template> <script>
// If using Vue 2 with Composition API plugin configured/ 在Vue2中使用 Composition API :  import { ref, computed } from "@vue/composition-api";
import { ref, computed } from "vue";
export default {
  setup() {
    //数据响应式 将数据包装在对象中以跟踪更改
    const capacity = ref(4);
    const attending = ref(["Tim", "Bob", "Joe"]);
    //Computed属性
    const spacesLeft = computed(() => {
      //通过调用.value访问响应式引用的值
      return capacity.value - attending.value.length;
    });
    // 定义方法
    function increaseCapacity() {
      //ref进行响应式的变量 需要修改变量的话则需要对其.value操作
      capacity.value++;
    }
    // 使我们的模板可以访问这些对象和功能
    return { capacity, attending, spacesLeft, increaseCapacity };
  }
};
</script>

你也可以这样写

import { reactive, computed, toRefs } from "vue";
export default {
  setup() {
    //reactive接受一个对象并返回一个响应式对象
    const event = reactive({
      capacity4,
      attending["Tim", "Bob", "Joe"],
      spacesLeftcomputed(() => { return event.capacity - event.attending.length; })
    });
    function increaseCapacity() {
      // reactive返回的响应式对象不需要使用.value操作
      event.capacity++;
    }
    //...toRefs 解构event中的对象,使模板中直接可以使用capacity或者attending,不需要event.attending
    return { ...toRefs(event), increaseCapacity };
  }
};

整理中...(敬请期待😄)

其他

getCurrentInstance

在setup中,是没有办法通过this获取到vue,我们可以通过getCurrentInstance获取vue实例

Axios配置及接口数据类型定义

axios封装配置

//目前不完善,后续会补充,整体结构没问题
import axios from "axios";
const baseURL = "xxxx";
const requestTimeout = 10000;
const successCode = [200, 0];
const instance = axios.create({
  baseURL,
  timeout: requestTimeout,
  headers: {
    "Content-Type": "application/json;charset=UTF-8"
  }
});

instance.interceptors.request.use(
  config => {
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);
instance.interceptors.response.use(
  response => {
    const res = response.data;
    const { data } = response;
    const { code, msg } = data;

    // 操作成功
    if (successCode.indexOf(code) !== -1) {
      return res;
    } else {
      return Promise.reject();
    }
  },
  error => {
    const { response } = error;
    if (response && response.data) {
      return Promise.reject(error);
    } else {
      const { message } = error;
      return Promise.reject(error);
    }
  }
);

export default instance;

数据类型配置

TypeScript中Axios默认返回类型为any,那么我们如果要自己定义数据类型该怎么做呢?

  1. 首先我们在声明文件中重新生命axios模块,vue-cli自带shims-vue.d文件,如果没有就新建一个,在src目录下
import axios from 'axios'

declare module 'axios' {
  export interface AxiosInstance {
    <T = any>(config: AxiosRequestConfig): Promise<T>;
    request<T = any> (config: AxiosRequestConfig): Promise<T>;
    get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
  }
}

接下来就可以使用了

interface ResourceOption{
  title:string;
  logo:string;
}

export const getResouceList = () => {
  return request<ResourceOption[]>({
    url: "api/resource/list",
    method: "get"
  });
};

关于Vue3中使用Typescript的注意点

1.Props定义数据类型

为了类型推论,让我们在使用属性的时候获取更丰富的类型提示,比如在这里我们定义了一个属性 list,使用 vue 默认的 Array,只能确定它是一个数组类型,不能确定数组里面的每一项到底是什么样子的。你在 setup 中,看 props.list 就是一个any数组,但是如果使用PropType <ColumnProps[]> 这个时候,props.list 就变成一个 ColumnProps 的数组,你使用它的时候不论在 ts 中还是模版中都能获得类型的推断和自动补全等等

<script lang='ts'>
import {defineComponent, PropType} from 'vue'
export interface ColumnProps{
    id: string;
    title: string;
    avatar: string;
    description: string;
}
export default defineComponent({
    name:'ColumnList',
    props:{
        list:{
            type:Array as PropType<ColumnProps[]>,
            required:true
        }
    }
})
</script>
<script lang="ts">
import { ResourceOption } from "@/entities/resource";
import { defineComponent, PropType } from "vue";

export default defineComponent({
  props: {
    data: {
      type: Object as PropType<ResourceOption>
    }
  }
});
</script>

2.axios数据类型配置

详细见上面Axios配置及接口数据类型定义

Vant配置

  • 安装
# 通过 npm 安装
npm i vant@next -S

# 通过 yarn 安装
yarn add vant@next
  • 使用 ts-import-plugin 实现vant按需引入 如果本地找不到这个这两个包就分别安装
//vue.config.js

// eslint-disable-next-line @typescript-eslint/no-var-requires
const merge = require("webpack-merge");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tsImportPluginFactory = require("ts-import-plugin");


module.exports = {
  chainWebpack: config => {
    config.module
      .rule("ts")
      .use("ts-loader")
      .tap(options => {
        options = merge(options, {
          transpileOnly: true,
          getCustomTransformers: () => ({
            before: [
              tsImportPluginFactory({
                libraryName: "vant",
                libraryDirectory: "es",
                style: true
              })
            ]
          }),
          compilerOptions: {
            module: "es2015"
          }
        });
        return options;
      });
  }
};
  • 移动端适配(vw/vh方案)
# 安装依赖
npm install postcss-px-to-viewport -D
// vue.config.js
const pxtoviewport = require("postcss-px-to-viewport");

const autoprefixer = require("autoprefixer");

module.exports = {
  css: {
    loaderOptions: {
      postcss: {
        plugins: [
          autoprefixer(),
          pxtoviewport({
            viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
            minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
            unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
          })
        ]
      }
    }
  }
}

重新运行,px变vw,ok✌~~

  • 注册使用的vant全局组件,统一管理,避免重复引用
// plugins/vant.ts

import { App as VM } from "vue";
import { Button, List, Cell, Tabbar, TabbarItem } from "vant";

const plugins = [Button, List, Cell, Tabbar, TabbarItem];

export const vantPlugins = {
  install: function(vm: VM) {
    plugins.forEach(item => {
      vm.component(item.name, item);
    });
  }
};
//main.ts 使用

import { createApp } from 'vue'
import { vantPlugins } from './plugins/vant'

createApp(App)
  ...//其他配置
  .use(vantPlugins)
  .mount('#app')

vant主题色

//详见/src/theme/var.less
// Color Palette
@black: #000;
@white: #fff;
@gray-1: #f7f8fa;
@gray-2: #f2f3f5;
@gray-3: #ebedf0;
@gray-4: #dcdee0;
@gray-5: #c8c9cc;
@gray-6: #969799;
@gray-7: #646566;
@gray-8: #323233;
@red: #ee0a24;
@blue: #1989fa;
@orange: #ff976a;
@orange-dark: #ed6a0c;
@orange-light: #fffbe8;
@green: #07c160;
@green1:#4fc08d;
// Gradient Colors
@gradient-red: linear-gradient(to right, #ff6034, #ee0a24);
@gradient-orange: linear-gradient(to right, #ffd01e, #ff8917);

// Component C
  • 1.引入样式文件 新增上述文件,并引入,由于上面vant配置中已经引入了,我们要调整一下指定样式的路径
//vue.config.js
module.exports = {
   chainWebpack: config => {
    config.module
      .rule("ts")
      .use("ts-loader")
      .tap(options => {
        options = merge(options, {
          transpileOnly: true,
          getCustomTransformers: () => ({
            before: [
              tsImportPluginFactory({
                libraryName: "vant",
                libraryDirectory: "es",
                // --> 指定样式的路径
                style: name => `${name}/style/less`
              })
            ]
          }),
          compilerOptions: {
            module: "es2015"
          }
        });
        return options;
      });
  }
};
  • 2.修改样式变量
//vue.config.js
module.exports = {
  ...
  css: {
    loaderOptions: {
      //配置less主题
      less: {
        lessOptions: {
          modifyVars: {
            // 直接覆盖变量
            "text-color": "#111",
            "border-color": "#eee",
            // 或者可以通过 less 文件覆盖(文件路径为绝对路径)
            hack: `true; @import "./src/theme/var.less";`
          }
        }
      },
    }
  }
}

关于样式穿透

以往的样式穿透方法>>>/deep/在vue3中会报一下警告信息 the >>> and /deep/ combinators have been deprecated. Use :deep() instead 可以改为

// 第一种
:v-deep .ant-table-tbody > tr > td,
:v-deep .ant-table-thead > tr > th {
	vertical-align: middle !important;
}
// 第二种
::v-deep(.ant-table-tbody > tr > td),
::v-deep(.ant-table-thead > tr > th) {
	vertical-align: middle !important;
}
// 第三种
:v-deep(.ant-table-tbody > tr > td),
:v-deep(.ant-table-thead > tr > th) {
	vertical-align: middle !important;
}
// 第四种
:deep(.ant-table-tbody > tr > td),
:deep(.ant-table-thead > tr > th) {
	vertical-align: middle !important;
}

浏览器样式重置

重置浏览器标签的样式表,因为浏览器的品种很多,每个浏览器的默认样式也是不同的,比如button标签,在IE浏览器、Firefox浏览器以及Safari浏览器中的样式都是不同的,所以,通过重置button标签的CSS属性,然后再将它统一定义,就可以产生相同的显示效果。开始一个项目前,先创建一个reset.css,可以规避很多浏览器差异问题

/* http://meyerweb.com/eric/tools/css/reset/
   v5.0.1 | 20191019
   License: none (public domain)
*/

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, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, menu, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
	margin: 0;
	padding: 0;
	border: 0;
	font-size: 100%;
	font: inherit;
	vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
	display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
    display: none;
}
body {
	line-height: 1;
}
menu, ol, ul {
	list-style: none;
}
blockquote, q {
	quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
	content: '';
	content: none;
}
table {
	border-collapse: collapse;
	border-spacing: 0;
}

移动端1px边框

  • 问题分析:有些手机的屏幕分辨率较高,是2-3倍屏幕。css样式中border:1px solid red;在2倍屏下,显示的并不是1个物理像素,而是2个物理像素。解决方案如下:
  • 利用 css 的 伪元素::after + transfrom 进行缩放 为什么用伪元素? 因为伪元素::after或::before是独立于当前元素,可以单独对其缩放而不影响元素本身的缩放

伪元素大多数浏览器默认单引号也可以使用,和伪类一样形式,而且单引号兼容性(ie)更好些 我是用scss写的mixins,其他与编译器道理道理都差不多

/*单条border样式*/
@mixin border-1px ($color, $direction) {
  position: relative;
  border: none;
  &::after{
    content: '';
    position: absolute;
    background: $color;
    @if $direction == left {
      left: 0;
      top: 0;
      height: 100%;
      width: 2px;
      transform: scaleX(0.5);
      transform-origin: left 0;
    }
    @if $direction == right {
      right: 0;
      top: 0;
      height: 100%;
      width: 2px;
      transform: scaleX(0.5);
      transform-origin: right 0;
    }
    @if $direction == bottom {
      bottom: 0;
      left: 0;
      width: 100%;
      height: 2px;
      transform: scaleY(0.5);
      transform-origin: 0 bottom;
    }
    @if $direction == top {
      top: 0;
      left: 0;
      width: 100%;
      height: 2px;
      transform: scaleY(0.5);
      transform-origin: 0 top;
    }
  }
}

/*四条border样式*/
@mixin all-border-1px ($color, $radius) {
  position: relative;
  border: none;
  &::after{
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    border: 2px solid $color;
    border-radius: $radius * 2;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    width: 200%;
    height: 200%;
    -webkit-transform: scale(0.5);
    transform: scale(0.5);
    -webkit-transform-origin: left top;
    transform-origin: left top;
  }

}
  • 使用
@import "@assets/style/mixin.scss";//引入

.box{
  @include all-border-1px(#eeeeee, 0); //使用
}

vuex的配置与使用

  • Vuex

这边要注意的是 const store = useStore() ,它的定义要写在setup中的外层,不要定义到某个方法里面,不然执行的时候获取不到

import { toRefs, reactive } from "vue";
import { useStore } from "vuex";
export default {

  setup() {
    const state = reactive({
      name: ''
    })  
    const store = useStore()

    state.name = store.state.Name

    return {
      ...toRefs(state)
    }
  }
};
  • 替代方案 provide、inject

声明一次,全局可访问,将需要共享的数据事先在 Vue 的根节点 App.vue 中通过 provide 声明。 首先建立一个store

// src/store/store.ts
const planList = Symbol()
export default {
  planList,
}

在外层组件注入,比如 App.vue 中 provide

// src/App.vue
<script lang="ts">
import Store from "./store/store"

import { defineComponent, provide, ref } from "@vue/composition-api"
export default defineComponent({
  setup() {
    provide(Store.planList, ref([]))
  }
})
</script>

在需要的组件内inject接受

// src/views/Plan.vue
<script lang="ts">
import Store from "./store/store"

import { defineComponent, provide, ref } from "@vue/composition-api"
export default defineComponent({
  setup() {
    const planList = inject(Store.planList)
    return {
      planList
    }
  }
})
</script>

vue路由配置和缓存

  • keep-alive写法改变
 <router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>

tsconfig配置

把compileOnSave和sourceMap 设置成false,如果为true的话,在保存ts文件的时候会自动生成js和map文件

{
  "compileOnSave": false,
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": false,
    "baseUrl": ".",
    "types": [
      "webpack-env"
    ],
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

语法检测自动格式代码

  • eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/typescript/recommended",
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint"
  ],
  parserOptions: {
    ecmaVersion: 2020
  },
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
  }
};
  • vscode settings.json
{
    // vscode默认启用了根据文件类型自动设置tabsize的选项
    "editor.detectIndentation": false,
    // 重新设定tabsize
    "editor.tabSize": 2,
    // #每次保存的时候自动格式化 
    "editor.formatOnSave": true,
    // #每次保存的时候将代码按eslint格式进行修复
    "eslint.autoFixOnSave": true,
    // 添加 vue 支持
    "eslint.validate": [
      "javascript",
      "javascriptreact",
      {
        "language": "vue",
        "autoFix": true
      }
    ],
    // #让prettier使用eslint的代码格式进行校验 
    "prettier.eslintIntegration": true,
    // #去掉代码结尾的分号 
    "prettier.semi": false,
    // #使用带引号替代双引号 
    "prettier.singleQuote": true,
    // #让函数(名)和后面的括号之间加个空格
    "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
    // #这个按用户自身习惯选择 
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    // #让vue中的js按编辑器自带的ts格式进行格式化 
    "vetur.format.defaultFormatter.js": "vscode-typescript",
    "vetur.format.defaultFormatterOptions": {
      "js-beautify-html": {
        "wrap_line_length": 120,
        "wrap_attributes": "auto"
        // #vue组件中html代码格式化样式
      }
    },
    // 格式化stylus, 需安装Manta's Stylus Supremacy插件
    "stylusSupremacy.insertColons": false, // 是否插入冒号
    "stylusSupremacy.insertSemicolons": false, // 是否插入分号
    "stylusSupremacy.insertBraces": false, // 是否插入大括号
    "stylusSupremacy.insertNewLineAroundImports": false, // import之后是否换行
    "stylusSupremacy.insertNewLineAroundBlocks": false,
    "explorer.confirmDelete": false // 两个选择器中是否换行
  }

发布&部署

  • 网站工具:https://vercel.com/
  • 用github账号登录(我项目是部署在guthub上的)
  • 点击import project -> import git repository
  • 输入自己的项目的git地址 https://xxx/xxx/xxx
  • 点击continue就会自动部署啦!部署好后会生成地址可以直接访问🍾

关于我

Image text 加我微信,邀你进入技术交流群,交流学习 😄 共同进步
如果喜欢请给我一个小♥♥ ⭐ (づ ̄3 ̄)づ

感谢

vue-h5-template