实践:RealWorld(Vue3.0 + TypeScript)

Vue3TypeScriptExample
案例

注意

目前项目仅完成了基本的ts支持、首页文章列表及登录,基本内容已覆盖

# 技术栈:

  • Vue.js 3
  • Vue Router
  • Vuex
  • TypeScript
  • axios

# 项目链接

# 创建项目

  1. 安装vue-cli,npm i -g @vue/cli,确认版本vue --version
  2. 创建项目vue create 3-5-realworld
? Please pick a preset: (Use arrow keys)
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features(自定义特性)

? Check the features needed for your project:
 (*) Choose Vue version
 (*) Babel
 (*) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
 (*) Vuex
 ( ) CSS Pre-processors
>(*) Linter / Formatter(格式化校验)
 ( ) Unit Testing
 ( ) E2E Testing
 
? Choose a version of Vue.js that you want to start the project with
  2.x
3.x (Preview)

# 是否使用class风格的语法
? Use class-style component syntax? (y/N) N

# 是否使用babel和typescript结合到一起(用于现代模式、自动polyfill,编译转换jsx)
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Y

# 是否使用history路由模式?
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) n

# 选择eslint校验规范
? Pick a linter / formatter config: (Use arrow keys)
  ESLint with error prevention only
  ESLint + Airbnb config
ESLint + Standard config
  ESLint + Prettier
  TSLint (deprecated)

# 啥时候进行代码格式校验
? Pick additional lint features:
 (*) Lint on save (保存的时候)
>(*) Lint and fix on commit (代码提交的时候)

# 配置信息保存到哪里?
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
In dedicated config files (单独文件,推荐)
  In package.json (合并到package.json中)

是否保存成预置模板?  
? Save this as a preset for future projects? (y/N) n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  1. 进入项目打开vscode
cd .\3-5-vue3-realworld\
code .
1
2

# 观察Vue3项目

相同点:

  • 目录结构是一样的
  • API的用法大体是相同的

不同点:

  • router
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'

// 这里是路由表的类型RouteRecordRaw
const routes: Array<RouteRecordRaw= [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // which is lazy-loaded when the route is visited.
    component: () =import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

// 之前创建router实例是这样new VueRouter()
const router = createRouter({
  // 这里是使用history模式
  history: createWebHashHistory(),
  routes
})

export default router

// 其他的都是一样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • vuex
import { createStore } from 'vuex'
// 以前创建store实例 new Vuex.Store({ state: { xxx }})
// 现在使用createStore去创建
export default createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • Home.vue
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>

<!--使用ts,标注lang-->
<script lang="ts">
// 里面使用 defineComponent 定义组件
import { defineComponent } from 'vue'
import HelloWorld from '@/components/HelloWorld.vue' // @ is an alias to /src

// 里面如果使用options API选项是一样的
export default defineComponent({
  name: 'Home',
  components: {
    HelloWorld
  },
  data () {},
  methods: {},
  mounted () {}
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 以前创建 vue 根实例用 new Vue(),现在用 createApp
createApp(App)
  .use(store) // use是挂载路由实例
  .use(router)
  .mount('#app') // 作用到根节点,这句代码要放在最最后

1
2
3
4
5
6
7
8
9
10
11

也可以使用

const app = createApp(App)

app.use(store)
app.use(router)

// app.provide('属性名', 属性值)
// 注册全局组件
// app.component()

// 注册全局指令
// app.directive()

app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
  • shims-vue.d.ts

typeScript类型声明文件,如果加载.vue文件,其类型component。

/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
1
2
3
4
5
6
  • ts.config.json

typeScript编译相关的文件,要了解一下

# 配置模板

  1. 下载样式 main.css (opens new window) 到 放public/css/main.css 中,在 public/index.html 中加载样式文件。
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<link href="https://cdn.jsdelivr.net/npm/ionicons@2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
<link
      href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"
      rel="stylesheet" type="text/css">
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
<link rel="stylesheet" href="/css/main.css">
1
2
3
4
5
6
7
  1. src目录下创建layout文件夹,创建AppHeader.vue文件
<template>
  <nav class="navbar navbar-light">
    <div class="container">
      <a class="navbar-brand" href="index.html">conduit</a>
      <ul class="nav navbar-nav pull-xs-right">
        <li class="nav-item">
          <!-- Add "active" class when you're on that page" -->
          <a class="nav-link active" href="">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="">
            <i class="ion-compose"></i>&nbsp;New Post
          </a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="">
            <i class="ion-gear-a"></i>&nbsp;Settings
          </a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="">Sign up</a>
        </li>
      </ul>
    </div>
  </nav>
</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. 同目录下创建AppFooter.vue文件
<template>
   <footer>
    <div class="container">
      <a href="/" class="logo-font">conduit</a>
      <span class="attribution">
        An interactive learning project from <a href="https://thinkster.io">Thinkster</a>. Code &amp; design licensed under MIT.
      </span>
    </div>
  </footer>
</template>

1
2
3
4
5
6
7
8
9
10
11
  1. 同目录下创建AppLayout.vue文件
<template>
  <!-- Vue3 的模板不需要根节点 -->
  <AppHeader/>
  <AppFooter/>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import AppHeader from './AppHeader.vue' // 使用了ts 需要加上模块后缀 .vue
import AppFooter from './AppFooter.vue'

export default defineComponent({
  components: {
    AppHeader,
    AppFooter
  }
})

</script>

<style>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 把App.vue中的默认样式去掉,只保留一个跟路由出口
<template>
  <!-- 根路由出口 -->
  <router-view/>
</template>
1
2
3
4
  1. 在src/views/home/index.vue文件中添加首页模板
<template>
  <div class="home-page">
    <div class="banner">
      <div class="container">
        <h1 class="logo-font">conduit</h1>
        <p>A place to share your knowledge.</p>
      </div>
    </div>

    <div class="container page">
      <div class="row">
        <div class="col-md-9">
          <div class="feed-toggle">
            <ul class="nav nav-pills outline-active">
              <li class="nav-item">
                <a class="nav-link disabled" href="">Your Feed</a>
              </li>
              <li class="nav-item">
                <a class="nav-link active" href="">Global Feed</a>
              </li>
            </ul>
          </div>

          <div class="article-preview">
            <div class="article-meta">
              <a href="profile.html"
                ><img src="http://i.imgur.com/Qr71crq.jpg"
              /></a>
              <div class="info">
                <a href="" class="author">Eric Simons</a>
                <span class="date">January 20th</span>
              </div>
              <button class="btn btn-outline-primary btn-sm pull-xs-right">
                <i class="ion-heart"></i29
              </button>
            </div>
            <a href="" class="preview-link">
              <h1>How to build webapps that scale</h1>
              <p>This is the description for the post.</p>
              <span>Read more...</span>
            </a>
          </div>

          <div class="article-preview">
            <div class="article-meta">
              <a href="profile.html"
                ><img src="http://i.imgur.com/N4VcUeJ.jpg"
              /></a>
              <div class="info">
                <a href="" class="author">Albert Pai</a>
                <span class="date">January 20th</span>
              </div>
              <button class="btn btn-outline-primary btn-sm pull-xs-right">
                <i class="ion-heart"></i32
              </button>
            </div>
            <a href="" class="preview-link">
              <h1>
                The song you won't ever stop singing. No matter how hard you
                try.
              </h1>
              <p>This is the description for the post.</p>
              <span>Read more...</span>
            </a>
          </div>
        </div>

        <div class="col-md-3">
          <div class="sidebar">
            <p>Popular Tags</p>

            <div class="tag-list">
              <a href="" class="tag-pill tag-default">programming</a>
              <a href="" class="tag-pill tag-default">javascript</a>
              <a href="" class="tag-pill tag-default">emberjs</a>
              <a href="" class="tag-pill tag-default">angularjs</a>
              <a href="" class="tag-pill tag-default">react</a>
              <a href="" class="tag-pill tag-default">mean</a>
              <a href="" class="tag-pill tag-default">node</a>
              <a href="" class="tag-pill tag-default">rails</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
  1. 在src/views/login/index.vue文件中添加登录模板
<template>
  <div class="auth-page">
    <div class="container page">
      <div class="row">
        <div class="col-md-6 offset-md-3 col-xs-12">
          <h1 class="text-xs-center">Sign up</h1>
          <p class="text-xs-center">
            <a href="">Have an account?</a>
          </p>

          <ul class="error-messages">
            <li>That email is already taken</li>
          </ul>

          <form>
            <fieldset class="form-group">
              <input
                class="form-control form-control-lg"
                type="text"
                placeholder="Your Name"
              />
            </fieldset>
            <fieldset class="form-group">
              <input
                class="form-control form-control-lg"
                type="text"
                placeholder="Email"
              />
            </fieldset>
            <fieldset class="form-group">
              <input
                class="form-control form-control-lg"
                type="password"
                placeholder="Password"
              />
            </fieldset>
            <button class="btn btn-lg btn-primary pull-xs-right">
              Sign up
            </button>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  1. 在router/index.ts中写路由配置
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

const routes: Array<RouteRecordRaw= [
  {
    path: '/',
    component: () =import('@/layout/AppLayout.vue'),
    children: [
      {
        path: '', // 默认子组件
        name: 'home',
        component: () =import('@/views/home/index.vue')
      },
      {
        path: 'login',
        name: 'login',
        component: () =import('@/views/login/index.vue')
      }
    ]
  }
]

// new VueRouter()
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  1. 在AppLayout.vue中添加子路由出口
<template>
  <!-- Vue3 的模板不需要根节点 -->
  <AppHeader/>
  <!-- 子路由出口 -->
  <router-view/>
  <AppFooter/>
</template>
1
2
3
4
5
6
7
  1. 输入npm run serve就可以浏览页面了,http://localhost:8080/#/是首页,http://localhost:8080/#/login是登录页面。

# 封装请求模块

  1. 安装axiosnpm i axios,axios对ts的支持很友好

npm小知识

npm i xxx 在npm 5 之后默认会加 --save

--save 和 -S 是一个东西

npm 5 之前不会保存依赖信息,npm 5 之后会自动写入 package.json 的 dependencies ,生产依赖,yarn add 也是默认加入生产依赖

--save-dev 是保存到开发依赖,简写是 -D

如果要保存在devDependencies 中,必须加上 --save-dev 或者 -D

安装全局包(命令行工具),需要使用 --global,简写是 -g, 全局包一般都是工具类的,比如 serve、gulp、http-server、Vue CLI ,安装一次就不用重复安装了

全局包一般都提供一个命令给你使用。

也可以把vue和jquery安装到全局,但是么有意义,项目中也不能直接引用。

  1. 配置环境变量,根目录下创建.env.production
VUE_APP_API_BASEURL=https://conduit.productionready.io/
1

根目录下创建.env.development

VUE_APP_API_BASEURL=https://conduit.productionready.io/
1

参考:模式和环境变量 (opens new window)

  1. 在src目录下创建utils/request.ts
import axios from 'axios'

export const request = axios.create({
  // VUE_APP 开头才能在应用中读取到 vue的webpack只会把这种开头的进行读取
  baseURL: process.env.VUE_APP_API_BASEURL
})

// 请求拦截器

// 响应拦截器

1
2
3
4
5
6
7
8
9
10
11
  1. 在src目录下创建api/user.ts
import { request } from '../utils/request'

// 设置一下login中data的类型,不然会报错,如果传入的data不是这个类型,就会报错
// 定义接口 登录输入类型
interface LoginInput {
  user: {
    email: string,
    password: string
  }
}

// 定义接口,登录返回类型User
interface User {
  email: string
  token: string
  image: string
  bio: string
  username: string
}
// 给返回值也定义类型,不然返回的内容是any,没有类型推断
// 定义接口,登录返回类型
interface LoginPayload {
  // 里面的user是User类型
  user: User
}

// 输入的参数data定义类型LoginInput
export const login = (data: LoginInput) => {
  // 调用post方法的时候设置一个泛型,把返回的内容设置成LoginPayload类型
  return request.post<LoginPayload>('/api/users/login', data)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 用户登录

  1. 在login/index.vue中写登录逻辑
<script lang="ts">
import { defineComponent, reactive } from 'vue'
import { login } from '@/api/user'

export default defineComponent({
  name: 'loginPage',
  setup () {
    const user = reactive({ // 支持类型推断
      email: '',
      password: ''
    })

    // 提交方法
    const handleSubmit = async () => {
      try {
        const { data } = await login({ user })
        console.log(data)
      } catch (err) {
        console.log('登录失败', err)
      }
    }

    // 返回的内容可以在模板中绑定使用
    return {
      user,
      handleSubmit
    }
  }
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  1. 在模板中修改
<!--表单提交-->
<form @submit.prevent="handleSubmit">
    <!-- <fieldset class="form-group">
      <input
        class="form-control form-control-lg"
        type="text"
        placeholder="Your Name"
      />
    </fieldset> -->
    <fieldset class="form-group">
    <!--绑定email-->
      <input
        v-model="user.email"
        class="form-control form-control-lg"
        type="text"
        placeholder="Email"
      />
    </fieldset>
    <!--绑定password-->
    <fieldset class="form-group">
      <input
        v-model="user.password"
        class="form-control form-control-lg"
        type="password"
        placeholder="Password"
      />
    </fieldset>
    <button class="btn btn-lg btn-primary pull-xs-right">
      Sign up
    </button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  1. 启动服务可以看到终端有报错

ERROR

error Expected a semicolon @typescript-eslint/member-delimiter-style

去.eslintrc.js中配置一下rules
'@typescript-eslint/member-delimiter-style': 'off'

  1. 再次启动服务,简单输入邮箱和密码,可以登录成功,可以拿到返回的信息,可以看到后台返回的东西比我们定义的多,那需要我们补齐,这里先忽略掉。

  2. 登录成功之后跳转首页,这里不能使用this,那么这样使用

...
// 导入vue-router的useRouter模块直接使用
import { useRouter } from 'vue-router'

export default defineComponent({
  name: 'loginPage',
  setup () {
    // 获得路由
    const router = useRouter()
    ...
    const handleSubmit = async () => {
      try {
        const { data } = await login({ user })
        console.log(data)
        // 登录成功之后跳转到首页
        router.push({
          name: 'home'
        })
      } catch (err) {
        ...
      }
    }
    ...
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  1. 进行组件的提取
// 登录逻辑单独定义一个函数
const useLogin = () => {
  // 获得路由
  const router = useRouter()
  const user = reactive({ // 支持类型推断
    email: '',
    password: ''
  })

  const handleSubmit = async () => {
    try {
      const { data } = await login({ user })
      console.log(data)
      // 登录成功之后跳转到首页
      router.push({
        name: 'home'
      })
    } catch (err) {
      console.log('登录失败', err)
    }
  }

  return {
    user,
    handleSubmit
  }
}
export default defineComponent({
  name: 'loginPage',
  setup () {
    return {
      // 组合到当前组件中
      ...useLogin()
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 让Vuex支持ts

  1. 在store/index.ts中重新引入代码支持ts
import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'

// define your typings for the store state 定义state类型
export interface State {
  count: number
}

// define injection key 定义注入的key
export const key: InjectionKey<Store<State>> = Symbol()

// 创建store容器,容器中通过泛型只听State的类型
export const store = createStore<State>({
  state: { // 必须符合泛型参数 State 类型要求
    count: 0
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 在入口模块main.ts中把key导入并挂载
import { store, key } from './store'

createApp(App)
  .use(store, key) // 把key传入
1
2
3
4
  1. 去login/index.vue中不用key是没有类型推断的,
import { useStore } from 'vuex'

const useLogin = () => {
  // 没有类型推断
  const store = useStore()
  ...
}
1
2
3
4
5
6
7

需要将key导入

import { useStore } from 'vuex'
import { key } from '@/store'

const useLogin = () => {
  // const store: Store<State> 有了类型推断
  const store = useStore(key)
  ...
}
1
2
3
4
5
6
7
8
  1. 如果觉得这样比较麻烦,可以在store/index.ts中进行进一步的封装
import { createStore, Store, useStore as baseUseStore } from 'vuex'

export const key: ...
...
export function useStore () {
    // 这里传入那login/index.vue就不用传key了
    return baseUseStore(key)
}
1
2
3
4
5
6
7
8

这样在login/index.vue中就可以直接用store的useStore,且不用传key

import { useStore } from '@/store'

const useLogin = () => {
  // 获得路由
  const router = useRouter()
  const store = useStore()

1
2
3
4
5
6
7

# 身份认证

设置登录完成之后保存登录信息

  1. api/user.ts中将User类型导出
// 定义接口,登录返回类型User
export interface User {
  email: string
  token: string
  image: string
  bio: string
  username: string
}
1
2
3
4
5
6
7
8
  1. 在store/index.ts中设置值
import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'
// 将api中设置的User类型导入
import { User } from '@/api/user'

export interface State {
  count: number
  // 可以置空
  user: User | null
}
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
  state: {
    count: 0,
    // 默认从本地存储获取,如果没有给一个字符串的null,不然会有类型错误
    user: JSON.parse(window.localStorage.getItem('user') || 'null')
  },

  mutations: {
    // 设置user
    setUser (state, user: User) {
      state.user = user
      // 持久化到本地存储
      window.localStorage.setItem('user', JSON.stringify(state.user))
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. 在login/index.vue中登录完成之后进行存储。
const handleSubmit = async () => {
    try {
      const { data } = await login({ user })
      // 成功之后将user传递进去
      store.commit('setUser', data.user)
      // 登录成功之后跳转到首页
      router.push({
        name: 'home'
      })
    } catch (err) {
      console.log('登录失败', err)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. npm run serve的时候会有报错

ERROR

error Expected Symbol to have a description symbol-description

这是因为store/index.ts中key定义了一个Symbol类型,这个根据ES6语法里面要传一个描述符字符串,可以随便加一个名字

export const key: InjectionKey<Store<State>> = Symbol('vue')
1

也可以不加直接在.eslintrc.js的rules里面关闭检查

'symbol-description': 'off'
1
  1. 这个时候点击登录,可以看到已经保存到localStorage中了。
  2. 下面请求的时候,如果要加验证的话,需要在request.ts中添加拦截器,这样就可以了。
// 请求拦截器
request.interceptors.request.use(config => {
  const user = store.state.user
  if (user) {
    config.headers.Authorization = `Token ${user.token}`
  }
  return config
}, err => {
  return Promise.reject(err)
})
1
2
3
4
5
6
7
8
9
10

# 获取文章列表

  1. 创建api/article.ts,里面写
import { request } from '../utils/request'

// 入参,可选
interface ArticlesParams {
  tag?: string
  author?: string
  favorited?: string
  limit?: number
  offset?: number
}

// 返回值,只要有对象的就提出来,方便重用
interface ArticleAuthor {
  username: string
  bio: string
  image: string
  following: boolean
}

// 导出,为了首页初始化数据的时候使用
export interface Article {
  slug:string
  title: string
  description: string
  body: string
  tagList: string[]
  createdAt: string
  updatedAt: string
  favorited: boolean
  favoritesCount: number
  author: ArticleAuthor
}

// articles里面是Article类型的数组
interface ArticlesPayload {
  articles: Article[]
  articlesCount: number
}

// 入参和返回值都设置了类型
export const getArticles = (params?: ArticlesParams) => {
  return request.get<ArticlesPayload>('/api/articles')
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  1. 在home/index.vue中写请求文章列表代码
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'
import { Article, getArticles } from '@/api/article'

const useArticles = () => {
  // 本地创建了响应式数据,并把Article类型导入,初始是一个空数组,里面的值应该是Article类型
  const articles = ref<Article[]>([])
  // 初始的articlesCount应该是响应式的,初始为0
  const articlesCount = ref(0)
  // 加载文章列表并赋值
  const loadArticles = async () => {
    const { data } = await getArticles()
    articles.value = data.articles
    articlesCount.value = data.articlesCount
  }
  // 在DOM初始化完执行
  onMounted(loadArticles)
  return {
    articles,
    articlesCount
  }
}
export default defineComponent({
  setup () {
    return {
      ...useArticles()
    }
  }
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  1. 修改模板,找到类名是article-preview的div,删除一个之后渲染文章列表
<!--循环列表-->
<div class="article-preview" v-for="article in articles" :key="article.slug">
    <div class="article-meta">
      <!--下载头像,修改路由跳转-->
      <router-link to="/"
        ><img :src="article.author.image"
      /></router-link>
      <div class="info">
        <!--作者姓名,修改路由跳转-->
        <router-link to="/" class="author">{{ article.author.username }}</router-link>
        <!--创作时间-->
        <span class="date">{{ article.createdAt }}</span>
      </div>
      <!--点赞样式,数量-->
      <button class="btn btn-outline-primary btn-sm pull-xs-right"
       :class="{
         active: article.favorited
       }">
        <i class="ion-heart"></i> {{ article.favoritesCount }}
      </button>
    </div>
    <!--文章跳转、标题、描述-->
    <router-link to="/" class="preview-link">
      <h1>{{ article.title }}</h1>
      <p>{{ article.description }}</p>
      <span>Read more...</span>
    </router-link>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  1. 启动服务,可以看到内容已经渲染上去了。
  2. 例子就先做到这里,首页和登录基本完成。
更新时间: 2022-01-10 09:52