项目概述与搭建

项目概述:使用Vue.js实现多人共享博客

吹逼:一个基于 Vue3 + Pinia + TypeScript + Vite2 + ant-design-vue 完整技术路线开发的项目,秒级开发更新启动、新的vue3 composition api 结合 setup纵享丝滑般的开发体验、全新的 pinia状态管理器和优秀的设计体验(1k的size)、antd无障碍过渡使用UI组件库 ant-design-vue、安全高效的 TypeScript 类型支持

  • 该项目使用 Vue3 实现了一个在线博客分享的平台。包含首页、用户文章列表、个人管理等页面,实现了登录、注册、编辑、发布等功能。
  • 项目使用 Vite2 为编译打包工具创建项目模版。之后考虑升级到 Vite3
  • 使用 SCSS 作 CSS 预处理,使用 Grid 作页面布局,使用 Ant-Design-Vue 作UI组件
  • 通过 Vue Router 实现路由的跳转、异步加载、权限验证等
  • 通过 Pinia 实现状态管理
  • 用 Axios 获取数据,并对接口进行了封装

介绍文字

  • 一个基于 Vue 3 / TypeScript / SCSSVite 2 构建的单页面应用
  • 这是我从自己记录学习笔记的需求出发,设计出的极简博客应用, 特点是使用 markdown 语法快速记录技术博客,并且实现不同用户博客共享
  • 主要功能包括 前端页面的登录、注册、首页文章列表展示、博客详情展示、博客创建、发布、编辑和删除(markdown格式)、我的页面和其他用户文章列表展示等。 实现的步骤已总结为 博客
  • 使用了开源的后端API接口,并用 Axios 封装请求,实现对应的 拦截器JWT 身份认证功能 并且使用了 ApiPost 接口测试工具,导出详细的 接口文档 ,让我对鉴权和接口封装有了一定的认识和实践
  • 该项目使用基于 Composition API 实现的 Vue3 Hooks ,让我对 hooks 有了深刻的理解和应用。 项目中的 hooks,代替之前项目中使用的 mixin 写法 解决mixin缺点
  • 尝试完全使用 TSXCSS module 语法代替 模板SFC,将使用心得总结为博客
  • 使用了 Pinia 作为统一的状态管理,对组件通信有了 新的理解

项目信息

  • 设计稿
  • 项目预览地址: https://xmasuhai.xyz/vite-share-blog-website/#/
  • 项目功能包含:
    • 首页:展示所有作者设置到首页的博客列表
    • 详情:展示博客详情
    • 登录、注册: 用户登录注册
    • 用户页面: 展示某个用户的所有博客列表
    • 我的: 展示个人主页
    • 编辑、删除、创建博客,博客是以markdown格式编写
  • 项目使用 Vue.js 技术栈的 单页面应用,版本为Vue 3.2
    • Vue全家桶:
      • 脚手架vite2.X代替vue-cliwebpack
      • 路由vue-router4
      • 状态管理pinia 代替vuex4
      • vitest
    • 使用JSX语法与组合式API
    • es6 语法
    • 封装axios
    • UI框架采用ant-design-vue
      • 后期更换为自己造的轮子UI框架
    • axiosapi接口封装jwt
    • Hooks库使用vueUse
  • 后端使用饥人谷的共享博客后端接口,共享博客接口文档
    • 后期自己搭建服务器,使用Node.js编写后端代码

大纲链接 §

[toc]


项目搭建

初始化项目

  • Node.js@16
  • pnpm代替yarn
  • 在安装时项目时
    • 如果使用 pnpm create vite 就一直使用 pnpm
    • 如果使用 yarn create vite 就一直使用 yarn

使用vite搭建vue-ts项目

1
2
3
4
cd ~
# yarn create vite <project-name>
# pnpm create vite <project-name>
pnpm create vite share-blog
  • vue-ts
1
2
3
4
5
6
# 安装依赖
pnpm install
# 开发预览
pnpm dev
# 打包
pnpm build

配置智能提示

使用 defineConfig 工具函数,这样不用 jsdoc 注解也可以获取类型提示:

  • Vite 直接支持 TS 配置文件。可以在 vite.config.ts 中使用 defineConfig 工具函数
1
2
3
4
5
import { defineConfig } from 'vite'

export default defineConfig({
  // ...
})

配置 ip 访问项目

  • vite 启动后出现 Network: use --host to expose
1
2
3
4
vite v2.7.13 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose
  • 是因为 IP 没有做配置
  • 所以不能从 IP 启动
  • 需要在 vite.config.js 做相应配置
  • vite.config.js 中添加 server.host0.0.0.0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    host: '0.0.0.0'
  }
})
  • 重新启动pnpm dev后显示
1
2
3
4
5
6
7
8
9
vite v2.7.13 dev server running at:

  > Network:  http://172.27.128.1:3000/
  > Network:  http://192.168.0.109:3000/
  > Local:    http://localhost:3000/
  > Network:  http://172.25.176.1:3000/
  > Network:  http://172.25.224.1:3000/

  ready in 692ms.

vite创建的初始项目目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
share-blog-vite
├─ .gitignore
├─ index.html
├─ package.json
├─ pnpm-lock.yaml
├─ public
│  └─ favicon.ico
├─ README.md
├─ src
│  ├─ App.vue
│  ├─ assets
│  │  └─ logo.png
│  ├─ components
│  │  └─ HelloWorld.vue
│  ├─ env.d.ts
│  └─ main.ts
├─ tsconfig.json
├─ vite.config.ts
└─ yarn.lock

vite创建代码简单分析

  • 可以看到index.html中的入口文件导入方式 <script type="module" src="/src/main.ts"></script>的类型为type="module"支持模块化导入
  • 框架代码中main.ts中就可以使用ES6 Module方式组织代码,不用再转译为低版本的模块化方式

*区别于使用vue/cli搭建vue-ts项目

  • 使用古老的@vue/cli搭建vue-ts项目,不推荐使用pnpm
  • 使用yarn即可
 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
cd ~
vue create share-world
# Choose # Manually select features
----
# Choose  Choose Vue version
 (*) Babel
 (*) TypeScript
 (*) Progressive Web App (PWA) Support
 (*) Router
 (*) Vuex
 (*) CSS Pre-processors
 (*) Linter / Formatter
>(*) Unit Testing
 ( ) E2E Testing
----
# Choose # 3.x
----
# Choose # y
----
# Choose # y
----
# Choose # n
----
# Choose # Sass/SCSS (with dart-sass)
----
# Choose # ESLint with error prevention only
----
# Choose #  ( ) Lint on save
>(*) Lint and fix on commit
----
# Choose # Jest
----
# Choose # In dedicated config files
----
# Choose # N

cd share-world
yarn serve
  • 注意,使用@vue/cli构建的项目,许多依赖已经停止维护deprecated
 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
λ pnpm install
 WARN  deprecated eslint-loader@2.2.1: This loader has been deprecated. Please use eslint-webpack-plugin
 WARN  deprecated html-webpack-plugin@3.2.0: 3.x is no longer supported                                 
 WARN  deprecated @hapi/joi@15.1.1: Switch to 'npm install joi'        
 WARN  deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
 WARN  deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x 
fewer dependencies
 WARN  deprecated @hapi/bourne@1.3.2: This version has been deprecated and is no longer supported or maintained
 WARN  deprecated @hapi/address@2.1.4: Moved to 'npm install @sideway/address'                                 
 WARN  deprecated @hapi/topo@3.1.6: This version has been deprecated and is no longer supported or maintained
 WARN  deprecated @hapi/hoek@8.5.1: This version has been deprecated and is no longer supported or maintained
 WARN  deprecated har-validator@5.1.5: this library is no longer supported                                   
 WARN  deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain cir
cumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
 WARN  deprecated @hapi/joi@15.1.1: Switch to 'npm install joi'
 WARN  deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain cir
cumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
 WARN  deprecated fsevents@1.2.13: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fse
vents 2.
 WARN  deprecated @hapi/hoek@8.5.1: This version has been deprecated and is no longer supported or maintained
 WARN  deprecated querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API
 instead.
 WARN  deprecated source-map-resolve@0.5.3: See https://github.com/lydell/source-map-resolve#deprecated
 WARN  deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x 
fewer dependencies
 WARN  deprecated svgo@1.3.2: This SVGO version is no longer supported. Upgrade to v2.x.x.
 WARN  deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated     
 WARN  deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated   
 WARN  deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
 WARN  deprecated core-js@2.6.12: core-js@<3.4 is no longer maintained and not recommended for usage due to the number of 
issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even i
f nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.


配置 scss 全局样式文件

  • 需要先配置别名

虽然 vite 原生支持 less/sass/scss/stylus,但是你必须手动安装他们的预处理器依赖

  • 安装依赖dart-sass,但包的名字为 sass
  • 不要安装为dart-sassdeprecated dart-sass@1.25.0: This package has been renamed to 'sass'.
1
2
# yarn add sass --dev
pnpm add sass -D

src/assets 下新增 style 文件夹,用于存放全局样式文件

  • 新建 src/assets/style/main.scss, 设置一个用于测试的颜色变量 :
1
$test-color: red;
  • 配置 vite.config.ts,让 vite 识别 scss 全局变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
css:{
    // 指定传递给 CSS 预处理器的选项
    preprocessorOptions:{
      scss:{
        additionalData:'@import "@/assets/style/main.scss";'
      }
    }
  },
...

在任意组件中使用,不需要任何引入可以直接使用全局scss定义的变量

1
2
3
.test{
  color: $test-color;
}

移动端适配 px to viewport(vw/vh)

使用postcss-px-to-viewport,将px单位转换为视口单位的 (vw, vh, vmin, vmax) 的 PostCSS 插件

  • 样式需要做根据视口大小来调整宽度,这个脚本可以将你CSS中的px单位转化为vw,1vw等于1/100视口宽度

换用 @jonny1994/postcss-px-to-viewport 支持TS

引入配置文件vite.config.ts

1

参考


TS的相关配置

vue/cli项目中shimes-vue.d.ts 文件的作用

  • 为了typescript做的适配定义文件,因为.vue 文件不是一个常规的文件类型
  • ts 是不能理解 .vue 文件是干嘛的 加这个文件是是告诉 ts,.vue 文件是这种类型的。

vite项目中env.d.ts 文件的作用

  • 为了声明 在vite中使用env环境的配置 可以通过 import.meta.env.<变量名称来访问>

vue-tsctsc

  • tsc 只能验证 ts 代码类型
  • vue-tsc 可以验证 ts + Vue Template 中的类型(基于 Volar)

JSX支持

Vue 3 JSX 相关语法

使用tsx风格代替template模板方式,需要注意相应改变的地方:

  1. src/router/ 引入页面文件 .tsx 代替 .vue 文件
  2. src/main.ts 修改引入 App 文件 .tsx 代替 .vue文件

安装依赖插件

1
2
# yarn add @vitejs/plugin-vue-jsx -D
pnpm add @vitejs/plugin-vue-jsx -D

配置tsconfig.json,在.tsx文件里支持JSX

 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
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",// 在.tsx文件里支持JSX
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "lib": [
      "esnext",
      "dom"
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }
  },
  "include": [
    "src/**/*.js",
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx", // 在.tsx文件里支持JSX
    "src/**/*.jsx",
    "src/**/*.vue"
  ]
}

改造App.vueApp.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from '@/components/HelloWorld.vue';
import {defineComponent} from 'vue';
import appClass from '@/styles/app.module.scss' // css modules
import logoImg from '@/assets/logo.png'; // static assets

// 用defineComponent定义组件且要导出
// noinspection JSXNamespaceValidation
export default defineComponent({
  render: () => (
    <>
      <img alt="Vue logo" src={logoImg}/>
      <HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>
      <router-link to="/">Go to Home</router-link>
      &nbsp;
      <router-link to="/about">Go to About</router-link>
      &nbsp;
      <router-view></router-view>
    </>
  ),
});
  • 运行命令pnpm dev查看效果

*.tsx文件中使用css modules

  • 只有在 *.tsx 文件中可以正常使用

配置vit.config.ts

 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
import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path'; // @types/node
import vueJsx from '@vitejs/plugin-vue-jsx';
import ...

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    VueSetupExtend(),
    autoComponents({...}),
    AutoImport({...}),
  ],
  server: {...},
  resolve: {
    alias: {...},
  },
  css: {
    // 配置 css modules 的行为
    modules: {
      scopeBehaviour: 'local', // 作用域
      // generateScopedName: // 使用默认配置,
      localsConvention: 'camelCase', // 识别样式命名方式
    },
    preprocessorOptions: {
      scss: {
        additionalData: `...`,
      }
    }
  },
});

  • 定义一个 *.module.scss 或者 *.module.css 文件
    • 创建src/styles/app.module.scss
1
2
3
4
5
6
7
8
.app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
  • *.tsx 中引入使用
    • 改写App.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import HelloWorld from '@/components/HelloWorld.vue';
import {defineComponent} from 'vue';
import appClass from '@/styles/app.module.scss' // css modules
import logoImg from '@/assets/logo.png'; // static assets

// 用defineComponent定义组件且要导出
export default defineComponent({
  render: () => (
    <main class={appClass.app}>
      <img alt="Vue logo" src={logoImg}/>
      <HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>
      <router-link to="/">Go to Home</router-link>
      &nbsp;
      <router-link to="/about">Go to About</router-link>
      &nbsp;
      <router-view></router-view>
    </main>
  ),
});
  • 样式文件xxx.module.scss中定义类样式.yyy {...}
  • 引入到*.tsx文件后,在模板中使用<main class={xxx.yyy}>...</main>

如何优雅的在TSX语法中切换多个className

1
2
3
4
5
6
7
<div className={
    this.state.isHide?styles.hide+' ':''+
    this.state.isActive?styles.active+' ':''+
    this.state.isAlarm?styles.alarm+' ':''
    }>
    {...data}
</div>
  • 使用第三方包classnames
  • 安装pnpm add classnames
  • 引入import classname from 'classnames'

上述代码可替换为:

1
2
3
4
5
6
7
8
9
dataClass: {
    isHide: true,
    isActive: true,
    isAlarm: true
}

<div className={classname(dataClass)}>
    {...data}
</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
29
30
// tsx
...
// multiClass
import classnames from 'classnames';

// CSS module
import basic from '@/styles/basic.module.scss';
import test1 from '@/styles/test1.module.scss';
import test2 from '@/styles/test2.module.scss';
const btnClass = [test1.test1, test2.test2]

export default defineComponent({
  name: 'BlogIndex',
  props: {},
  setup(/*props, ctx*/) {
    return {
    };
  },
  render() {
    return (
      <>
        <Button class={classnames(...btnClass, basic.blogBtn)}>
          博客首页
        </Button>
      </>
    );
  }

});

和普通样式写在一起

1
2
<section class={classNames([cssDetail.article, 'article'])}>
</section>

参考


TSX基础语法

替换v-bind语法

1
2
3
4
5
// vue
<template>
  <van-circle v-bind:radius="d/2"/>
  <van-circle :radius="d/2" />
</template>
1
2
3
4
5
...
  return (
    <van-circle radius={this.d/2}/>
  )
...

替换v-model语法

1
2
3
4
5
// vue
<template>
  <van-circle v-model:current-rate="currentRate" />
  <van-circle :current-rate="currentRate" />
</template>
1
2
3
// tsx
...
<van-circle  v-model={[state.currentRate, 'current-rate']} />

替换v-for语法

1
2
3
4
5
6
7
8
9
// vue
<script>
/* key尽量使用 item.id 。迫不得已使用 index */
  const list = [1,2,3]
</script>

<template>
  <li v-for="item in [1,2,3]" :key="item">{{item}}</li>
</template>
1
2
3
4
5
6
7
// jsx
{
  list.map(item => (
    <li key={item}> { item } </li>
  ))
}

  • 此处如果item是一个对象,可以用解构语法来取出对象属性
  • 需要指定key属性,来优化diff
    • key属性使用数据中的唯一id作为值

替换v-ifv-else语法

1
2
3
4
5
6
7
8
9
// vue
<script>
const flag = true;
</script>

/* v-if */
<template>
  <div v-if="flag" /> 
</template>
1
2
// tsx
{ flag && <div /> }

/* v-else */

1
2
3
4
5
// vue
<template>
  <div v-if="flag" />
  <div v-else />
</template>
1
2
3
4
// tsx
{
  flag ? <div></div> : <div></div>
}
  • 更多条件判断使用表

替换@click等事件

1
2
3
4
5
// vue
<template>
  <div @click="handleClick" />
  <div @change="handleChange">
</template>
1
2
3
// tsx
<input onClick={handleClick} />
<input onChange={handleChange} />
  • 监听事件想到用 onChange, onClick
  • 需要注意的是
    • 传参数不能使用 onClick={this.removePhone(params)}
    • 这样子会每次 render 的时候都会自动执行一次方法
    • 应该使用 bind,或者箭头函数来传参
  • 原生事件必须监听到存在对应事件的标签上,比如input标签的input事件,否则报错
1
2
3
4
5
6
7
8
9
// tsx
<button type="button"
        onClick={this.handleClick.bind(this, 11)}>
    使用bind传参
</button>
<button type="button"
        onClick={(/*event: MouseEvent*/) => this.handleClick(11)}>
    使用箭头函数
</button>

JSX 自定义事件

  • 需要在子组件的选项中声明 emits: ['diyEvent'],
  • 父组件中使用 on + 驼峰式命名 的形式,例如<Son onDiyEvent={...}/>

使用自定义事件

  • 要声明一个自定义事件:onHandleSubmit
  • 子组件
    • 声明选项emits: ['handleSubmit',],
    • setup选项中传参setup(props, ctx) {}
      • 定义 发布方法 const clickHandler = () => {ctx.emit('handleSubmit');}
      • 返回return {clickHandler}
    • render方法中使用render() {return <button onClick={this.clickHandler}></button>}
  • 父组件
    • setup选项中
      • 定义 处理逻辑的方法 const clickHandler = () => {}
    • render方法中使用自定义事件 <UserSubmitBtnTip onHandleSubmit={this.clickHandler}/>

判断键盘抬起时,键盘值

  • 自定义组件中有<input/>,事件keyup不能绑定在自定义组件上,而是绑定在内部的的<input/>
  • 通过props传一个keyUpHandler到内部
  • 无修饰符,只能使用event: KeyboardEvent中的event.key来判断,是否按下Enter
    • ["Enter"].includes(event.key)

UserInput.tsx

1

Login.tsx

参考

用监听事件来实现 v-modal 双向绑定

1
2
3
4
5
6
7
8
// tsx
const doInput = (event: MouseEvent) => { this.text = e.target.value }
...

<input type="text"
       value={this.text}
       onInput={doInput}/>

事件修饰符

和指令一样,除了个别的之外,JSX 中大部分的事件修饰符都无法在像模板那样直接使用

替代方案:

  • .stop : 阻止事件冒泡,在TSX中使用event.stopPropagation() 来代替
  • .prevent:阻止默认行为,在TSX中使用event.preventDefault() 来代替
  • .self.enter:只当事件是从侦听器绑定的元素本身触发时才触发回调,使用下面的条件判断进行代替
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// tsx
if (event.target !== event.currentTarget){
  return ...

}

// .enter与keyCode: 在特定键触发时才触发回调

if(event.keyCode === 13) {
  // 执行逻辑
}

使用vue自带的withModifiers()方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { withModifiers } from 'vue'

const handleClick = withModifiers() => {
    // ...
  }, ['stop', 'prevent'])

// tsx
<button type="button"
        onClick={this.handleClick}>
    使用bind传参
</button>

替换插槽v-slot

1
2
3
4
5
6
7
8
// vue
<template>
  <van-nav-bar title="标题" left-text="返回" left-arrow>
    <template #right>
      <van-icon name="search" size="18" />
    </template>
  </van-nav-bar>
</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
// tsx
<van-nav-bar title="标题"
             left-text="返回"
             left-arrow
             v-slots={{ 'left': () => <van-icon name="search" size="18" /> }}
...

// 子组件
export default defineComponent({
    name: "Test",
    render() {
        return (
            <>
                <span>I'm Child</span>
                { this.$slots.default?.() }
                { this.$slots.header?.() }
            </>
        )
    }
})

// 父组件
import TestComponent from './TestComponent'

export default defineComponent({
    name: "Test",
    components: {
        TestComponent
    },
    render() {
        return (
            <TestComponent v-slots={{
                default: () => (
                    <>这是默认插槽</>
                ),
                header: () => (
                    <>这是header插槽</>
                )
            }}>
            </TestComponent>
        )
    }
})
  • 父组件通过 v-slots 属性去定义插槽,子组件直接在 render 中通过 this.$slots[name] 去获取
作用域插槽
  • 作用域插槽在JSX中的写法也一样,只需要将scope作为参数传递即可
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// vue
// 子组件
<template>
    <div>
        <span>I'm Child</span>
        <slot name="content" :value="value"></slot>
    </div>
</template>

<script setup>
const value = ref({name: 'xzw'})
</script>

// 父组件
<template>
    <TestComponent>
        <template #content="scope">
            {{ scope.value.name }}
        </template>
    </TestComponent>
</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
// tsx
// 子组件
import { defineComponent } from "@vue/runtime-core";

export default defineComponent({
    name: "Test",
    setup() {
        return {
            value: {
                name: 'xzw'
            }
        }
    },
    render() {
        return (
            <>
              <span>I'm Child</span>
              { this.$slots.content?.(this.value) }
            </>
        )
    }
})

// 父组件
import TestComponent from './TestComponent'

export default defineComponent({
    name: "Test",
    components: {
        TestComponent
    },
    render() {
        return (
            <TestComponent v-slots={{
                content: scope => (
                    <>{scope.name}</>
                )
            }}>
            </TestComponent>
        )
    }
})
  • 作用域插槽
    • this.$slots.default?.(this.value)
    • this.$slots.content?.(this.value)

参考


vite 常用插件

自动导入插件 unplugin-auto-import/vite

  • setup语法让我们不用再一个一个的把变量和方法都return出去就能在模板上使用,大大的解放了我们的双手
  • 然而对于一些常用的Vue API,比如ref、computed、watch等,还是每次都需要在页面上手动进行import
  • 可以通过unplugin-auto-import实现自动导入,无需import即可在文件里使用Vue的API

参考文章:

自动导入组件unplugin-vue-components

安装

1
pnpm add unplugin-vue-components -D

配置vite.config.ts

 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
import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
import vueJsx from '@vitejs/plugin-vue-jsx';
// 自动引入组件
import autoComponents from 'unplugin-vue-components/vite';
import {
  ElementPlusResolver,
  AntDesignVueResolver,
  VantResolver,
  HeadlessUiResolver,
  ElementUiResolver
} from 'unplugin-vue-components/resolvers';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    // unplugin-vue-components/vite
    autoComponents({
      // ui库解析器,也可以自定义
      // resolvers: [ElementPlusResolver()],
      resolvers: [
        ElementPlusResolver(),
        AntDesignVueResolver(),
        VantResolver(),
        HeadlessUiResolver(),
      ],
      // 指定组件位置,默认是src/components
      dirs: ['src/components'],
      // valid file extensions for components.
      // 组件的有效文件扩展名。
      extensions: ['vue', 'tsx'],
      // 配置文件生成位置
      dts: 'src/components/components.d.ts',
      // search for subdirectories
      // 搜索子目录
      deep: true,
      // Allow subdirectories as namespace prefix for components.
      // 允许子目录作为组件的命名空间前缀。
      directoryAsNamespace: false,
      // filters for transforming targets
      include: [/.vue$/, /.vue?vue/],
      exclude: [/[\/]node_modules[\/]/, /[\/].git[\/]/, /[\/].nuxt[\/]/],
    })
  ],
  ...
});
  • 直接写组件名即可,插件会自动引入进来 注意别重名

自动导入vue3的hooks

  • 原本 vue 的 api 需要自行 import
  • 借助 unplugin-auto-import/vite 这个插件
  • 支持vue, vue-router, vue-i18n, @vueuse/head, @vueuse/core等方法自动按需引入

效果

1
2
3
4
5
6
7
8
// 引入前
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)

//引入后
const count = ref(0)
const doubled = computed(() => count.value * 2)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 引入前
import { useState } from 'react'
export function Counter() {
  const [count, setCount] = useState(0)
  return <div>{ count }</div>
}

// 引入后
export function Counter() {
  const [count, setCount] = useState(0)
  return <div>{ count }</div>
}

目前模板支持自动引入 api 的库列表包括:

  • vue
  • pinia
  • vueuse
  • vue-router

安装

1
2
# yarn add unplugin-auto-import -D
pnpm add unplugin-auto-import -D

vite.config.ts配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
...
export default defineConfig({
  ...,
  plugins: [
    AutoImport({
      imports: ['vue', 'vue-router', 'vue-i18n', '@vueuse/head', '@vueuse/core'],
      // 可以选择auto-import.d.ts生成的位置,使用ts建议设置为'src/auto-import.d.ts'
      // dts: 'src/auto-import.d.ts'
    }),
    ...
  ]
})
  • 原理: 安装的时候会自动生成 auto-imports.d 文件(默认是在根目录下src/auto-imports.d)
  • 可以选择auto-import.d.ts生成的位置,使用ts建议设置为src/auto-import.d.ts
  • 其他插件 vue-router, vue-i18n, @vueuse/head, @vueuse/core等自动引入的自动引入请查看文档

vue-global-api 解决未显示导入依赖导致 eslint 报错

  • 在页面没有显示引入的情况下,使用unplugin-auto-import/vite来自动引入hooks
  • 在项目中肯定会报错的,这时需要在eslintrc.js中的extends引入vue-global-api
  • 这个插件是vue3hooks的,其他自己找找,找不到的话可以手动配置一下globals
  • vue-global-api文档

自动UI库引入样式插件vite-plugin-style-import

  • 当你使用unplugin-vue-components来引入ui库的时,message, notification 等引入样式不生效
  • 安装vite-plugin-style-import即可
  • vite-plugin-style-import文档

setup name 增强插件

  • 使用setup语法糖带来的第一个问题就是无法自定义name属性
  • 而使用keep-alive往往是需要定义name
  • 解决这个问题通常是通过写两个script标签来解决,一个使用setup,一个不使用
  • 但这样必然是不够优雅的,见 5个知识点,让 Vue3 开发更加丝滑
  • 借助插件vite-plugin-vue-setup-extend可以更优雅的解决这个问题
  • 不用写两个script标签,可以直接在script标签上定义name

安装vite-plugin-vue-setup-extend

1
2
# yarn add vite-plugin-vue-setup-extend -D
pnpm add vite-plugin-vue-setup-extend -D

配置vite.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// vite.config.ts
import { defineConfig } from 'vite'
...
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

export default defineConfig({
  ...,
  plugins: [
    ...,
    VueSetupExtend(),
    ...,
  ],
  ...
})

setup语法糖中使用name属性

1
2
3
4
5
6
7
<script lang="ts" setup name="OrderList">
import { onMounted } from 'vue'

onMounted(() => {
  console.log('mounted===')
})
</script>

引入svg图标

使用现成的vite插件,有三个选择:

  1. 推荐 vite-plugin-svg-icons
  2. unplugin-icons (改名前身为vite-plugin-icons,默认使用Iconify图标库)
  3. Vite SVG loader vite-svg-loader 单独引入*.svg文件文件,封装为Vue组件使用
  4. 使用 svgosvgstore 自己写vite插件

参考 使用 阿里巴巴 iconfont

手动自制vite插件src/plugins/svgBuilder.js

第三方图标库 Iconify


https://juejin.cn/post/7063415079928070157 | 前端工程化之SvgIcon组件 - 掘金 https://juejin.cn/post/6990159719558021133 | 工程化使用 SVG 图标最佳实践 - 掘金 https://juejin.cn/post/7055878408365932557 | 2022年大厂前端必备,开箱即用的Vue3+Vite2最强模板 - 掘金 https://juejin.cn/post/7062681169627709448 | iconfont的原理与实践 - 掘金 https://juejin.cn/post/7000560093804625957 | icones:支持即时搜索的图标资源管理器,尤雨溪等开发者赞助支持 - 掘金 https://juejin.cn/post/7067418893899268103 | 采用 @svgr/webpack 支持 svg 作为组件引入 - 掘金 https://juejin.cn/post/6863414887931084814 | Vue项目中使用svg小图标,可修改大小颜色 - 掘金 https://juejin.cn/post/7017657405265674271 | vue工程使用svg图片,svg-sprite-loader缺陷,你们的按需加载svg是错误的。 - 掘金 https://juejin.cn/post/6975380991762235422 | 当webpack有了vite的速度你会喜欢吗? - 掘金


markdown 插件,用户文本输入文章,插件渲染成网页

安装marked

1
2
pnpm add marked
pnpm add -D @types/marked

引入


参考

其他markdown方案


其他插件


vite项目中安装vue-router@4

  • 需要先配置别名alias,将src/映射为@,详见插件部分

在 src 文件下新增 router 文件夹 => index.ts 文件,内容如下:

  • 使用hash模式的createWebHashHistory方法生成实例,原理为监听window.onhashchange事件,无需后端配置
  • 使用history模式的createWebHistory方法生成实例,原理为监听window.onpopstate事件,需要后端配置(由history.pushState()history.replaceState()触发,popstate
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import {createRouter, /*createWebHistory, hash 模式 */ createWebHashHistory, RouteRecordRaw} from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Login',
    component: () => import('@/pages/login/Login.vue'), // 注意这里要带上 文件后缀.vue
  },
];

const router = createRouter({
  history: createWebHashHistory(), // hash 模式
  routes,
});

export default router;

修改入口文件 mian.ts :

1
2
3
4
5
6
7
import {createApp} from 'vue';
import App from './App.vue';
import router from './router/index';

const app = createApp(App);
app.use(router);
app.mount('#app');

到这里路由的基础配置已经完成了,更多配置信息可以查看 vue-router 官方文档:

  • vue-router: https://next.router.vuejs.org/zh/guide/
  • vue-router4.x 支持 typescript,配置路由的类型是 RouteRecordRaw,这里 meta 可以让我们有更多的发挥空间,这里提供一些参考:
    • title:string; 页面标题,通常必选。
    • icon?:string; 图标,一般配合菜单使用。
    • auth?:boolean; 是否需要登录权限。
    • ignoreAuth?:boolean; 是否忽略权限。
    • roles?:RoleEnum[]; 可以访问的角色
    • keepAlive?:boolean; 是否开启页面缓存
    • hideMenu?:boolean; 有些路由我们并不想在菜单中显示,比如某些编辑页面。
    • order?:number; 菜单排序。
    • frameUrl?:string; 嵌套外链。

keep-alive缓存路由


axios的封装

安装

导入

使用


安装pinia

安装

导入

使用

代替安装vuex

安装

导入

使用


vite其他配置

  • 别名配置
  • 配置 scss 全局样式文件
  • css modules
  • mockjs模拟数据配置
  • 配置多环境变量
  • 动态导入环境配置
  • esbuild一些打包配置

别名配置

  • 导入 path 的时候提示报错,需要安装 @types/node 类型检测
1
pnmp add @types/node -D

修改 vite.config.ts 文件配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 导入 path 的时候提示报错,需要安装 @types/node 类型检测
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  ...
})

修改 tsconfig.json 文件配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "baseUrl": ".",
    "paths": {
      "@/*":["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

mockjs模拟数据配置

安装

1
pnpm add mockjs vite-plugin-mock cross-env -D

配置package.json字段script > dev

1
2
3
...
"dev": "cross-env NODE_ENV=development vite"
...

配置vite.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { viteMockServe } from 'vite-plugin-mock'

plugins: [
  vue(),
  viteMockServe({
    mockPath: './mock',
    supportTs: true
  }),
  ...
  ],

  • supportTs: true,是否用了ts,可以根据自己选择true or false
  • 要将 watchFiles: true, 文件夹监听打开,这样更改mock的时候,不需要重新启动编译

使用

  • 在与node_modules 同级 目录建立mock目录
  • mock目录下建立mock文件 user.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { MockMethod } from 'vite-plugin-mock'
export default [
  {
    url: '/api/getUser',
    method: 'get',
    response: (req: Record<string, unknown> /*拿到请求的数据*/) => {
      // console.log('body>>>>>>>>')
      return {
        code: 0,
        message: 'ok',
        data: ['aa', 'bb']
      }
    }
  }
] as MockMethod[]

使用

1
2
3
4
5
6
// 代码请求
axios.get('/api/getUser')
  .then(res => {
  console.log(res)
})

  • 这里就配置完成了

参考

按照接口文档模拟数据

入口 api-mock.ts


https://juejin.cn/post/7032926228055556126 | vite + typesctipt+mockjs搭建mock环境 - 掘金 https://juejin.cn/post/6910014283707318279 | 备战2021:vite工程化实践,建议收藏 - 掘金 https://juejin.cn/post/7039879176534360077 | 关于 vite.config.js 相关配置,拿走不谢 - 掘金 https://juejin.cn/post/7053717060295065614 | vue3+vite+ts+vuex+vue-router+Element-plus+tailwindcss+mock 搭建完整项目 - 掘金 https://juejin.cn/post/7028481421022855175 | vite.config.js常用配置 - 掘金 https://www.cnblogs.com/linbudu/p/11375775.html | Vue项目中引入Mock.js & Mock.js语法整理 - 林不渡 - 博客园 https://github.com/nuysoft/Mock/wiki/Mock.mock() | Mock.mock() · nuysoft/Mock Wiki · GitHub https://github.com/enjoycoding/vite-plugin-mock-server | GitHub - enjoycoding/vite-plugin-mock-server: A mock server plugin for Vite. https://juejin.cn/post/6901615700364918791 | vue中mock.js的使用 - 掘金 https://juejin.cn/search?query=vite-plugin-mock-server | vite-plugin-mock-server - 搜索 - 掘金 https://juejin.cn/post/6993740289605124126 | vite-plugin常用的插件 - 掘金 https://juejin.cn/post/7000283590642630687 | Vue3 + Vite2 + Vue-Router 4.x + Vuex 4.x + Element-Plus + Axios + Mockjs 项目搭建 - 掘金 https://juejin.cn/post/6936771237452464165 | VUE3 项目开发实战入门系列 (7.-Mock接口) - 掘金 https://juejin.cn/post/6844903492235034632 | Mockjs,再也不用追着后端小伙伴要接口了 - 掘金 https://github.com/Tencent/APIJSON | GitHub - Tencent/APIJSON: 🚀 零代码、热更新、全自动 ORM 库,后端接口和文档零代码,前端(客户端) 定制返回 JSON 的数据和结构。 🚀 A JSON Transmission Protocol and an ORM Library for automatically providing APIs and Docs. https://juejin.cn/post/7048916480032768013 | 「前端该如何优雅地Mock数据🏃」每个前端都应该学会的技巧 - 掘金 https://juejin.cn/post/7043420433613324325 | Vite + Vu3项目中 Mockjs 插件的学习使用 - 掘金 https://juejin.cn/post/7040701806887993381 | 两个项目实例+常用语法解析带你掌握Mock.js✨ - 掘金 https://www.cnblogs.com/student007/p/15180190.html | (o゚v゚)ノ Hi - vite使用mock插件(vite-plugin-mock)记录 https://www.jianshu.com/p/72fffae58761 | vite2+vue3项目中引入vite-plugin-mock - 简书 https://www.cnblogs.com/leiting/p/15125707.html | vite mock 数据插件:vite-plugin-easy-mock - c-137Summer - 博客园 https://www.npmjs.com/package/cross-env | cross-env - npm https://www.jianshu.com/p/e8ba0caa6247 | cross-env使用 - 简书


配置开发/生产环境变量

基本概念

import.meta.url

不单单是开发环境或生产环境,api 请求的域名会根据不同环境而不同

  • 线上环境测试环境 在打包策略有所不同「如线上要隔离sourceMap、屏蔽vue|react devtools等…」
  • 前端 SPA 组件根据不同环境做出不同逻辑

环境变量和多环境适配的 Vite 实现

  • dev 开发环境
  • test 测试环境
  • beta 公测版环境
  • release 生产发布环境

  • 环境变量 .env
  • 把系统配置放到 config/constant.ts 管理了
  • 为了方便管理不同环境的接口和参数配置
  • 可以使用环境变量 .env
  • .env.development.env.production
  • 配合 vite + import.meta.env 使用

参考

动态导入环境配置

安装

导入

使用


esbuild一些打包配置


vite.config.ts完整配置


项目结构与组件样式

布局

1
2
3
4
5
6
7
<div>
  <BlogHeader></Header>
  <main>
    <router-view/>
  </main>
  <BlogFooter></Footer>
</div>

App.tsx 根组件

 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
import Layout from '@/components/Layout';
import {defineComponent, provide} from 'vue';

// Comps ant
import {message} from 'ant-design-vue';

// CSS Style
import 'ant-design-vue/es/button/style/index.css';
import 'ant-design-vue/es/message/style/index.css';

// 用defineComponent定义组件且要导出
export default defineComponent({
  name: 'App',
  // components: {BlogHeader, BlogFooter, Button}, // 无需注册组件,components是用于template模板中
  setup(/*props, ctx*/) {
    // 将 message 方法挂载到全局
    provide('$message', message);
  },
  render: () => {
    const renderLayout = () => { return (<Layout/>)}
    return (
      renderLayout()
    );
  },
});

  • App.tsx 根组件按需引入UI组件库样式
  • App.tsx 根组件使用provide提供全局 message 方法
  • App.tsx 根组件引入布局组件Layout.tsx

布局组件Layout.tsx

 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
import {defineComponent,} from 'vue';

// Comps
import BlogHeader from '@/components/BlogHeader';
import BlogFooter from '@/components/BlogFooter';

// css modules
import layoutClass from '@/styles/layout.module.scss';

const LayoutProps = {
  isSHow: Boolean
};
export default defineComponent({
  name: 'Layout',
  props: LayoutProps,
  setup(/*props, ctx*/) {

    return {};
  },
  render() {
    const renderBlogHeader = () => {return (<BlogHeader class={layoutClass.blogHeader}/>);};
    const renderBlogFooter = () => {return (<BlogFooter class={layoutClass.blogFooter}/>);};
    return (
      <div class={layoutClass.app}>
        {renderBlogHeader()}
        <main class={layoutClass.blogMain}>
          <router-view/>
        </main>
        {renderBlogFooter()}
      </div>
    );
  }

});

layout.module.scss

 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
// layout 布局
.app {
  display: grid;
  grid-template-areas: "header header header"
                       ".      main  ."
                       "footer footer footer";
  grid-template-columns: 12% auto 12%;
  grid-template-rows: auto 1fr auto;
  height: 100vh;

  .blog-header {
    grid-area: header;
    padding-left: 12%;
    padding-right: 12%;
  }

  .blog-main {
    grid-area: main;
  }

  .blog-footer {
    align-self: end;
    grid-area: footer;
    padding-left: 12%;
    padding-right: 12%;
  }

}

@media (max-width: 768px) {
  .app {
    grid-template-columns: 10px auto 10px;

    .blog-header,
    .blog-footer {
      padding-left: 10px;
      padding-right: 10px;
    }
  }

}

  • layout 布局只控制页面结构,组件具体的样式在各自组件中写

<BlogHeader/>组件中,判断用户是否登录,从而展示不同的登录样式

 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
// BlogHeader.tsx
import {computed, defineComponent, ref} from 'vue';
import {Button,} from 'ant-design-vue';

// CSS module
import classNames from 'classnames';
import blogClass from '@/styles/blog.module.scss';
import basic from '@/styles/basic.module.scss';

const BlogHeaderProps = {
  isSHow: Boolean
};

export default defineComponent({
  name: 'BlogHeader',
  props: BlogHeaderProps,
  components: {},
  setup(/*props, ctx*/) {
    const isLogin = ref(false);

    const isLoginClass = computed(() => {
      console.log(blogClass.login);
      return (
        isLogin
          ? [blogClass.blogHeader, blogClass.login]
          : [blogClass.blogHeader]);
    });
    return {
      isLogin,
      isLoginClass
    };
  },
  render() {
    const renderBtn = (btnString: string) => (<Button class={basic.blogBtn}>{btnString}</Button>);
    const renderUnLogin = () => (
      <div class={blogClass.btns}>
        {renderBtn('立即登录')}
        {renderBtn('注册账号')}
      </div>
    );

    const renderLogin = () => (
      <>
        <i>123</i>
        <img src="" alt=""/>
      </>
    );

    return (
      <header class={classNames(...this.isLoginClass)}>
        <h1>Let's share</h1>
        <p>精品博客汇聚</p>
        {this.isLogin
          ? renderLogin()
          : renderUnLogin()
        }
      </header>
    );
  }
});

src/styles/blog.module.scss

 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
@import 'src/assets/style/variables';

%h1-style {
  color: $white;
  font-size: 40px;
  text-transform: uppercase;
}

.blog-header {
  // 默认为未登录状态
  background: $bg-color-veg;
  display: grid;
  justify-items: center;
  padding: 0 12% 30px;

  .slogan {
    @extend %h1-style;
    margin: 60px 0 0;
  }

  .tips {
    color: $white;
    margin: 15px 0 0;
  }

  .btns {
    margin-top: 20px;
  }

  button {
    margin: 20px 5px 0;
  }

}

// 切换为登录状态 覆盖未登录状态样式
.login {
  align-items: center;
  background: $bg-color-veg;
  display: flex;

  .slogan {
    @extend %h1-style;
    flex: 1;
    margin: 0;
    padding: 0;
  }

  .user {
    .edit-icon {
      color: $white;
      font-size: 30px;
    }

    .avatar {
      border: 1px solid $white;
      border-radius: 50%;
      height: 40px;
      margin-left: 15px;
      width: 40px;
    }
  }
}

.blog-footer {
  background-color: $bg-color-footer;
  color: $font-color-footer;
  font-size: 13px;
  padding: 10px;
  text-align: center;
}


其他静态页面

src/pages/blog/index/BlogIndex.tsx

 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
import {message} from 'ant-design-vue';
import {defineComponent, inject} from 'vue';
// CSS module
// import basic from '@/styles/basic.module.scss';
import blogIndex from '@/styles/blog-index.module.scss';

// multiClass
// const btnClass = [basic.blogBtn];

import blogApi from '@/api/blog';

const {getBlogs} = blogApi;

export default defineComponent({
  name: 'BlogIndex',
  props: {},
  setup(/*props, ctx*/) {
    const popMessage = inject<typeof message>('$message');

    const getBlogList = async () => {
      const BlogDataList = await getBlogs();
    };

    return {
      popMessage
    };
  },
  render() {
    return (
      <>
        <section class={blogIndex.blogPost}>
          <div class={blogIndex.item}>
            <figure class={blogIndex.avatar}>
              <img class={blogIndex.img} src="" alt=""/>
              <figcaption class={blogIndex.info}>姓名</figcaption>
            </figure>

            <h3 class={blogIndex.title}>文章标题
              <span class={blogIndex.date}>时间</span>
            </h3>
            <p class={blogIndex.article}>正文,最多显示前200</p>
          </div>

          <div class={blogIndex.item}>
            <figure class={blogIndex.avatar}>
              <img class={blogIndex.img} src="" alt=""/>
              <figcaption class={blogIndex.info}>姓名</figcaption>
            </figure>

            <h3 class={blogIndex.title}>文章标题
              <span class={blogIndex.date}>时间</span>
            </h3>
            <p class={blogIndex.article}>正文,最多显示前200</p>
          </div>

          <div class={blogIndex.item}>
            <figure class={blogIndex.avatar}>
              <img class={blogIndex.img} src="" alt=""/>
              <figcaption class={blogIndex.info}>姓名</figcaption>
            </figure>

            <h3 class={blogIndex.title}>文章标题
              <span class={blogIndex.date}>时间</span>
            </h3>
            <p class={blogIndex.article}>正文,最多显示前200</p>
          </div>
        </section>
      </>
    );
  }

});

``

 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
@import 'src/assets/style/variables';

.blog-post {
  .item {
    display: grid;
    grid: auto auto / 80px 1fr;
    margin: 20px 0;

    .avatar {
      grid-column: 1;
      grid-row: 1 / span 2;
      justify-self: center;
      margin-left: 0;
      text-align: center;

      .img {
        width: 60px;
        height: 60px;
        border-radius: 50%;
      }

      .info {
        font-size: 12px;
        color: $theme-lighter-color;
      }
    }

    .title {
      grid-column: 2;
      grid-row: 1;

      & > .date {
        color: $theme-lighter-color;
        font-size: 12px;
        font-weight: normal;
      }
    }

    .article {
      grid-column: 2;
      grid-row: 2;
      margin-top: 0;
    }


  }
}



参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名