0%

说明

  • 这篇文章 使用类视图实现了orm中常见的一对一,一对多,多对多。本次使用DRF的方式来实现

环境搭建

  • 发现本地的环境有问题,重新搭建个

  • 下载一个python 3.10 64位的,然后pycharm使用虚拟环境

  • 设置默认下载镜像

1
2
3
4
C:\Users\Administrator\AppData\Roaming\pip\pip.ini

[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
  • env环境安装各种依赖
1
2
3
4
5
6
pip install Django
pip install mysqlclient # 之前版本用的是pymysql,还要再init.py文件中加入内容,现在用这个没有问题
pip install djangorestframework
pip install django-filter # 搜索用的
pip install djangorestframework-simplejwt # jwt鉴权使用

  • 项目的setting加入
1
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

消除该警告:表明 Django 模型未显式定义主键类型,系统自动使用了默认的 AutoField

然后执行下面生效

(venv) E:\proj\StudyDjangoAdmin>python manage.py migrate DRFDjango

python manage.py makemigrations DRFDjango

  • 看看之前的环境是否可以启动
1
(venv) E:\proj\StudyDjangoAdmin> python manage.py runserver 0.0.0.0:8000

image-20251012165148267

DRF实践

  • 新建一个应用
1
django-admin startapp DRFDjango
  • 项目下的setting注册应用
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
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'TestModel', #
'DRFDjango',
'rest_framework',
'django_filters',

]

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
# 如果需要保留其他认证方式,可以添加如下
# 'rest_framework.authentication.SessionAuthentication',
# 'rest_framework.authentication.BasicAuthentication',
),

# 设置全局默认权限策略
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', # 默认所有视图需要认证
],

'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}

用默认的表做鉴权
AUTH_USER_MODEL = 'auth.User'

# JWT 配置 过期时间

from datetime import timedelta

SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': 'your-secret-key',
'VERIFYING_KEY': None,
}

  • model,这里作者详情和作者优化了,之前要先创建作者详情后再创建作者信息,不合理
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
from django.contrib.auth.models import AbstractUser
from django.db import models


# Create your models here.


class Book(models.Model):
title = models.CharField(max_length=32)
price = models.DecimalField(max_digits=5, decimal_places=2)
pub_date = models.DateField()
# 关联出版社一对多,意思就是一个出版社可以有印刷多本书
publish = models.ForeignKey("Publish", on_delete=models.CASCADE, default=None)
# 多对多,意思就是多个作者可以编写多本书
authors = models.ManyToManyField("Author")

def __str__(self):
return self.title


# 出版商
class Publish(models.Model):
name = models.CharField(max_length=32)
city = models.CharField(max_length=64)
email = models.EmailField()

def __str__(self):
return self.name


class Author(models.Model):
name = models.CharField(max_length=100, null=True, blank=True)
age = models.IntegerField()
# 改为可为空了
au_detail = models.OneToOneField(
'AuthorDetail',
on_delete=models.SET_NULL,
null=True, # 允许数据库存储NULL
blank=True, # 允许表单验证为空
related_name='author'
)


class AuthorDetail(models.Model):
GENDER_CHOICES = (
(0, '女'),
(1, '男'),
(2, '保密')
)

gender = models.SmallIntegerField(choices=GENDER_CHOICES)
tel = models.CharField(max_length=20)
addr = models.CharField(max_length=100)
birthday = models.DateField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

  • 序列化代码serializers.py
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
88
89
90
91
92
93
94
95
96
97
98
99
100
from rest_framework import serializers

from .models import Book, Publish, Author, AuthorDetail

class AuthorDetailSerializer(serializers.ModelSerializer):
class Meta:
model = AuthorDetail
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')


class AuthorSerializer(serializers.ModelSerializer):
au_detail = AuthorDetailSerializer(required=False, allow_null=True)
au_detail_id = serializers.IntegerField(
write_only=True,
required=False,
allow_null=True
)

class Meta:
model = Author
fields = ['id', 'name', 'age', 'au_detail', 'au_detail_id']
extra_kwargs = {
'au_detail': {'required': False, 'allow_null': True},
}

def validate(self, data):
# 确保至少提供一种形式的au_detail数据
if 'au_detail' not in data and 'au_detail_id' not in data:
data['au_detail'] = None # 明确设置为None
return data

def create(self, validated_data):
au_detail_data = validated_data.pop('au_detail', None)
au_detail_id = validated_data.pop('au_detail_id', None)

# 创建Author实例(不包含au_detail)
author = Author.objects.create(**validated_data)

# 处理AuthorDetail
if au_detail_data:
author.au_detail = AuthorDetail.objects.create(**au_detail_data)
author.save()
elif au_detail_id is not None: # 明确检查是否为None
author.au_detail_id = au_detail_id
author.save()

return author

def update(self, instance, validated_data):
au_detail_data = validated_data.pop('au_detail', None)
au_detail_id = validated_data.pop('au_detail_id', None)

# 更新Author字段
for attr, value in validated_data.items():
setattr(instance, attr, value)

# 处理AuthorDetail
if au_detail_data:
if instance.au_detail:
# 更新现有AuthorDetail
detail_serializer = AuthorDetailSerializer(
instance.au_detail,
data=au_detail_data,
partial=True
)
detail_serializer.is_valid(raise_exception=True)
detail_serializer.save()
else:
# 创建新AuthorDetail
instance.au_detail = AuthorDetail.objects.create(**au_detail_data)
elif au_detail_id is not None:
instance.au_detail_id = au_detail_id

instance.save()
return instance


class PublishSerializer(serializers.ModelSerializer):
class Meta:
model = Publish
fields = '__all__'


class BookSerializer(serializers.ModelSerializer):
publish = PublishSerializer(read_only=True)
authors = AuthorSerializer(many=True, read_only=True)

# 用于写入的字段
publish_id = serializers.PrimaryKeyRelatedField(
queryset=Publish.objects.all(), source='publish', write_only=True
)
author_ids = serializers.PrimaryKeyRelatedField(
queryset=Author.objects.all(), many=True, source='authors', write_only=True
)

class Meta:
model = Book
fields = '__all__'
extra_fields = ['publish_id', 'author_ids']
  • view.py 内容
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
from rest_framework import viewsets, pagination
from rest_framework.response import Response

from .models import Book, Publish, Author, AuthorDetail
from .serializers import BookSerializer, PublishSerializer, AuthorSerializer, AuthorDetailSerializer
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend


class StandardPagination(pagination.PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100


class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all().order_by('-id')
serializer_class = BookSerializer
pagination_class = StandardPagination

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=201, headers=headers)

def perform_create(self, serializer):
book = serializer.save()
author_ids = self.request.data.get('author_ids', [])
book.authors.set(author_ids)


class PublishViewSet(viewsets.ModelViewSet):
queryset = Publish.objects.all().order_by('-id')
serializer_class = PublishSerializer
pagination_class = StandardPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
# 修正后的搜索字段配置
search_fields = ['name', 'city'] # 可以搜索名称和城市

# 修正后的过滤字段配置
filterset_fields = {
'name': ['exact', 'icontains'], # 精确匹配或包含
'city': ['exact'],
}

def get_queryset(self):
queryset = super().get_queryset()
# 可以在这里添加自定义的查询逻辑
return queryset


class AuthorViewSet(viewsets.ModelViewSet):
queryset = Author.objects.all().order_by('-id')
serializer_class = AuthorSerializer
pagination_class = StandardPagination


class AuthorDetailViewSet(viewsets.ModelViewSet):
queryset = AuthorDetail.objects.all().order_by('-id')
serializer_class = AuthorDetailSerializer
pagination_class = StandardPagination

  • 路由url
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
from rest_framework.routers import DefaultRouter

from DRFDjango.views import BookViewSet, PublishViewSet, AuthorViewSet, AuthorDetailViewSet

from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)

router = DefaultRouter()
router.register(r'books', BookViewSet)
router.register(r'publishes', PublishViewSet)
router.register(r'authors', AuthorViewSet)
router.register(r'authordetails', AuthorDetailViewSet)

urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/logout/', LogoutView.as_view(next_page='/login/'), name='logout'),
path('accounts/login/', LoginView.as_view(template_name='login.html'), name='login'),
path('', include('TestModel.urls')),
path('api/', include((router.urls, 'api'), namespace='api')),
# JWT 认证路由
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

  • 测试代码
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
BASE_URL = 'http://localhost:8000/api/'
AUTH_URL = BASE_URL + "token/"

HEADER = {
"Content-Type": "application/json"
}
def get_jwt_token():
"""获取JWT Token"""
# 测试用户凭证
USER_CREDENTIALS = {
"username": "admin", # 替换为你的测试用户名
"password": "admin" # 替换为测试密码
}
response = requests.post(AUTH_URL, json=USER_CREDENTIALS)
if response.status_code == 200:
print(response.json())
HEADER["Authorization"] = "Bearer "+ response.json()["access"]
return response.json()["access"]
raise Exception(f"获取Token失败: {response.text}")

def get_publishes():
get_jwt_token()
params = {'page': 1, 'page_size': 2, "search": "北京2121"}
url = f"{BASE_URL}publishes"
response = requests.get(url, params=params, headers=HEADER)
if response.status_code == 200:
print("Publishers retrieved successfully:")
for publish in response.json()["results"]:
print(publish)
else:
print(f"Failed to retrieve publishers. Status code: {response.status_code}")
  • 更加详测试代码就不贴了,更新到代码仓库中

总结

  • 迁移过程中,发现有些表字段迁移失败
1
2
python manage.py migrate DRFDjango zero
python manage.py migrate DRFDjango
  • 本次分别使用用DRF结合jwt,orm常见的结构进行了实践。

说明

  • 这篇文章介绍了django中简单的类视图单表操作,本次开始进行基于orm的一对多,一对一,多对多的实践
  • 关于orm的具体介绍,可以查看这篇文章的笔记

一对多

model

1
2
3
4
5
6
7
8
9
10
11
12
class Book(models.Model):
title = models.CharField(max_length=32)
price = models.DecimalField(max_digits=5, decimal_places=2)
pub_date = models.DateField()
# 关联出版社一对多,意思就是一个出版社可以有印刷多本书
publish = models.ForeignKey("Publish", on_delete=models.CASCADE, default=None)

# 出版商
class Publish(models.Model):
name = models.CharField(max_length=32)
city = models.CharField(max_length=64)
email = models.EmailField()
  • 生效
1
2
3
python manage.py makemigrations TestModel# 让 Django 知道我们在我们的模型有一些变更
python manage.py migrate TestModel # 创建表

image-20250921164906417

view&html

  • 这里的配置和book的代码一模一样
  • 运行代码新增出版商的数据

image-20250921151545436

改造book

  • forms/book,加入了publish
1
2
3
4
5
6
7
8
9
10
11
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'price', 'publish']

# 你可以在这里添加额外的验证逻辑
def clean_title(self):
title = self.cleaned_data['title']
if not title:
raise forms.ValidationError("Title is required.")
return title
  • 书籍的index.html 引用出版商
1
{{ book.publish.name }}-
  • 后续的增删改查,不用改动任何代码

多对多&一对一

model

一对一

  • 新建作者和作者详情表,也就是一个作者一个作者详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Author(models.Model):
name = models.CharField(max_length=32)
age = models.SmallIntegerField()
# 一对一,意思为一个作者对应一个作者详情
au_detail = models.OneToOneField("AuthorDetail", on_delete=models.CASCADE)

class AuthorDetail(models.Model):
gender_choices = (
(0, "女"),
(1, "男"),
(2, "保密"),
)
gender = models.SmallIntegerField(choices=gender_choices)
tel = models.CharField(max_length=32)
addr = models.CharField(max_length=64)
birthday = models.DateField()
  • 创建表单类来处理 froms.py
1
2
3
4
5
6
7
8
9
10
11
12
13
from django import forms
from ..models import Author, AuthorDetail

class AuthorForm(forms.ModelForm):
class Meta:
model = Author
fields = ['name', 'age']

class AuthorDetailForm(forms.ModelForm):
class Meta:
model = AuthorDetail
fields = ['gender', 'tel', 'addr', 'birthday']

  • 新增个人信息views.py,注意get_context_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
# Author Views
class AuthorList(LoginRequiredMixin, ListView):
model = Author
template_name = 'author_list.html'
context_object_name = 'authors'

class AuthorCreate(LoginRequiredMixin, CreateView):
model = Author
form_class = AuthorForm
template_name = 'author_form.html'
success_url = reverse_lazy('author_list')

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.method == 'POST':
context['detail_form'] = AuthorDetailForm(self.request.POST)
else:
context['detail_form'] = AuthorDetailForm()
return context

def form_valid(self, form):
context = self.get_context_data()
detail_form = context['detail_form']
if detail_form.is_valid():
author_detail = detail_form.save(commit=False)
author_detail.save() # 先保存个人明细页面
author = form.save(commit=False)
author.au_detail = author_detail
author.save() # 再保存个人信息
return super().form_valid(form)
else:
return self.render_to_response(context)
  • 个人列表author_list.html
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Author List</title>
</head>
<body>
<h1>Author List</h1>
<a href="{% url 'author_new' %}">Add New Author</a>
<ul>
{% for author in authors %}
<li>
{{ author.id }}--
<a href="{% url 'author_detail' author.id %}">{{ author.name }}</a>--
<a href="{% url 'author_edit' author.id %}">Edit</a>
<form action="{% url 'author_delete' author.id %}" method="post" style="display:inline;">
{% csrf_token %}
<button type="submit" onclick="return confirm('确定要删除这个作者吗?')">Delete</button>
</form>
</li>
{% endfor %}
</ul>
</body>
</html>

  • 新增个人信息author_form.html,注意这里和编辑共用的同一个html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% if object %}Edit{% else %}Add{% endif %} Author</title>
</head>
<body>
<h1>{% if object %}Edit{% else %}Add{% endif %} Author</h1>
<form method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{{ form.as_p }}
{{ detail_form.non_field_errors }}
{{ detail_form.as_p }}
<button type="submit">{% if object %}Save changes{% else %}Save{% endif %}</button>
</form>
<a href="{% url 'author_list' %}">Back to list</a>
</body>
</html>
  • veiw加入明细、修改、删除
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
class AuthorDetail(LoginRequiredMixin, DetailView):
model = Author
template_name = 'author_detail.html'
context_object_name = 'author'

class AuthorUpdate(LoginRequiredMixin, UpdateView):
model = Author
form_class = AuthorForm
template_name = 'author_form.html'
success_url = reverse_lazy('author_list')

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.method == 'POST':
context['detail_form'] = AuthorDetailForm(self.request.POST, instance=self.object.au_detail)
else:
context['detail_form'] = AuthorDetailForm(instance=self.object.au_detail)
return context

def form_valid(self, form):
context = self.get_context_data()
detail_form = context['detail_form']
if detail_form.is_valid():
author_detail = detail_form.save(commit=False)
author_detail.save() # Save AuthorDetail first to update it
author = form.save(commit=False)
author.au_detail = author_detail
author.save()
return super().form_valid(form)
else:
return self.render_to_response(context)

class AuthorDelete(LoginRequiredMixin, DeleteView):
model = Author
template_name = 'author_confirm_delete.html'
success_url = reverse_lazy('author_list')
  • 最终的url加入
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
from django.urls import path

from . import views
from .views import BookAdd, BookList, BookDetail, BookEdit, BookDelete,PublishList,PublishAdd

urlpatterns = [
path('book/', BookList.as_view(), name='book_list'),
# path("event_add/", views.event_add, name="event_add"),
path('book/add/', BookAdd.as_view(), name='book_add'),
path('book/edit/<int:pk>/', BookEdit.as_view(), name='book_edit'),
path('book/<int:pk>/', BookDetail.as_view(), name='book_detail'),
path('book/delete/<int:pk>/', BookDelete.as_view(), name='book_delete'), # 新增删除路径
path("publish_add/",PublishAdd.as_view() , name="publish_add"),
path("publish/", PublishList.as_view(), name="publish_list"),


# Author URLs
path('authors/', views.AuthorList.as_view(), name='author_list'),
path('author/<int:pk>/', views.AuthorDetail.as_view(), name='author_detail'),
path('author/new/', views.AuthorCreate.as_view(), name='author_new'),
path('author/<int:pk>/edit/', views.AuthorUpdate.as_view(), name='author_edit'),
path('author/<int:pk>/delete/', views.AuthorDelete.as_view(), name='author_delete'),


]
  • author_detail.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Author Detail</title>
</head>
<body>
<h1>{{ author.name }} Details</h1>
<p>Name: {{ author.name }}</p>
<p>Age: {{ author.age }}</p>
<p>Gender: {{ author.au_detail.get_gender_display }}</p>
<p>Tel: {{ author.au_detail.tel }}</p>
<p>Address: {{ author.au_detail.addr }}</p>
<p>Birthday: {{ author.au_detail.birthday }}</p>
<a href="{% url 'author_list' %}">Back to list</a>
</body>
</html>

多对多

  • book表再次优化
1
2
3
4
5
6
7
8
class Book(models.Model):
title = models.CharField(max_length=32)
price = models.DecimalField(max_digits=5, decimal_places=2)
pub_date = models.DateField()
# 关联出版社一对多,意思就是一个出版社可以有印刷多本书
publish = models.ForeignKey("Publish", on_delete=models.CASCADE)
# 多对多,意思就是多个作者可以编写多本书
authors = models.ManyToManyField("Author")
  • 生成表结构
1
2
python manage.py makemigrations TestModel# 让 Django 知道我们在我们的模型有一些变更
python manage.py migrate TestModel

image-20250921190310674

  • 注意这里多对多自动生成了一个中间表

  • 然后在from/book.py fields加入authors就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django import forms

from TestModel.models import Book


class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'price', 'pub_date', 'authors']
# 你可以在这里添加额外的验证逻辑
def clean_title(self):
title = self.cleaned_data['title']
if not title:
raise forms.ValidationError("Title is required.")
return title
  • 在书籍详情页html,加了个循环读取作者信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>详情页面</title>
</head>
<body>
<div>
<p>id:{{ book.id }}</p>
<p>标题:{{ book.title }}</p>
<p>价格:{{ book.price }}</p>
<p>日期:{{ book.pub_date }}</p>
<p>Authors:
{% for author in book.authors.all %}
<a href="{% url 'author_detail' author.id %}">{{ author.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
</div>
<a href="{% url 'book_list' %}">Back to List</a>

</body>
</html>

说明

  • 上一篇文章django-admin 主要介绍了利用admin创建用户,然后结合模板、html等实现了增删改查,本次介绍使用类视图来优化

登录的优化

  • 根目录的url,用系统自带的视图
1
2
3
4
5
6
7
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/logout/', LogoutView.as_view(next_page='/login/'), name='logout'),
path('accounts/login/', LoginView.as_view(template_name='login.html'), name='login'),

]

  • setting加入了登录、退出登录的url
1
2
3
4
5
6
7
8
# 指定登录URL
LOGIN_URL = '/accounts/login/'

# 指定注销后重定向的URL
LOGOUT_REDIRECT_URL = '/login/'

# 指定登录后重定向的URL
LOGIN_REDIRECT_URL = '/book/'

书籍列表

  • 看下应用下的url优化
1
2
3
4
5
6
7
urlpatterns = [
path('book/', BookList.as_view(), name='book_list'),
path('book/add/', BookAdd.as_view(), name='book_add'),
path('book/edit/<int:pk>/', BookEdit.as_view(), name='book_edit'),
path('book/<int:pk>/', BookDetail.as_view(), name='book_detail'),
path('book/delete/<int:pk>/', BookDelete.as_view(), name='book_delete'), # 新增删除路径
]
  • vew代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lass BookList(LoginRequiredMixin, ListView):
model = Book
template_name = 'index.html'
context_object_name = 'latest_book_list'
paginate_by = 5

def get_queryset(self):
title = self.request.GET.get("title", "")
if title:
return Book.objects.filter(title__contains=title).order_by("-pub_date")
else:
return Book.objects.order_by("-pub_date")

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_term'] = self.request.GET.get("title", "")
return context
  • 使用了LoginRequiredMixin登录视图
  • get_queryset 返回列表,get_context_data返回具体字段

新增书籍

  • 具体view代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14

class BookAdd(LoginRequiredMixin, CreateView):
model = Book
# fields = ['title', 'price']
form_class = BookForm

template_name = 'add.html'
success_url = reverse_lazy('book_list')
# 默认实现了 form_valid() 简单重定向至 success_url
def form_valid(self, form):
book = form.save(commit=False)
book.pub_date = now().strftime("%Y-%m-%d")
book.save()
return super().form_valid(form)
  • html文件
1
2
3
4
5
6
7
8
9
10
11
12
{% extends "base_generic.html" %}

{% block content %}
<h2>Add New Book</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save changes</button>
</form>
<a href="{% url 'book_list' %}">Back to list</a>
{% endblock %}

  • form.as_p 直接根据BookForm 生成了表单内容

  • url进行了修改,之前是跳转到add的html界面,然后提交form表单,现在可以一步完成

1
2
# path('event/add/', EventAdd.as_view(), name='event_add'),
path('book/add/', BookAdd.as_view(), name='book_add'),

删除

1
2
3
4
5
6
7
8
9
10
11
12
13
lass BookDelete(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
"""
权限控制:UserPassesTestMixin 允许你在视图级别进行自定义权限检查。
"""
model = Book
template_name = 'delete.html'
success_url = reverse_lazy('book_list')
# test_func: 这是 UserPassesTestMixin 要求实现的方法,用于定义具体的权限检查逻辑。如果 test_func 返回 True,则允许用户访问视图;否则,返回 False 并抛出 PermissionDenied 异常。
def test_func(self):
book = self.get_object()
# 确保只有书籍的所有者或管理员才能删除书籍
return self.request.user.is_superuser or self.request.user == book.owner

编辑

  • view
1
2
3
4
5
6
7
8
9
10
11
12
class BookEdit(LoginRequiredMixin, UpdateView):
model = Book
form_class = BookForm

template_name = 'edit.html'
success_url = reverse_lazy('book_list')

def form_valid(self, form):
book = form.save(commit=False)
book.pub_date = now().strftime("%Y-%m-%d")
book.save()
return super().form_valid(form)
  • html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>编辑</title>
</head>
<body>
{% extends "base_generic.html" %}

{% block content %}
<h2>Edit Book</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save changes</button>
</form>
<a href="{% url 'book_list' %}">Back to list</a>
{% endblock %}

</body>
</html>

安装

  • 安装django
1
E:\proj>python -m pip install Django
  • 使用django-admin 创建项目
1
django-admin startproject StudyDjangoAdmin
  • 查看路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    E:\proj\StudyDjangoAdmin>tree /f
    卷 软件盘 的文件夹 PATH 列表
    卷序列号为 240F-1787
    E:.
    │ manage.py

    └─StudyDjangoAdmin
    asgi.py
    settings.py
    urls.py
    wsgi.py
    __init__.py
    • 在我们创建了Django的项目后,我们在最原始的urls.py中就可以看见关于admin的路径:

image-20250909090838252

  • 启动服务
1
python manage.py runserver 0.0.0.0:8000
  • 如果要访问admin后台我们只需要输入以下网址:
1
http://127.0.0.1:8000/admin/

image-20250909091046888

数据库选择

  • 我们在项目的 settings.py 文件中找到 DATABASES 配置项,将其信息修改为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// E:\proj\StudyDjangoAdmin\StudyDjangoAdmin\settings.py
DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# }
'default':
{
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': 'runoob1', # 数据库名称
'HOST': '127.0.0.1', # 数据库地址,本机 ip 地址 127.0.0.1
'PORT': 3306, # 端口
'USER': 'root', # 数据库用户名
'PASSWORD': '123456', # 数据库密码
}
}
  • 安装mysql驱动
1
E:\proj\StudyDjango>pip3 install pymysql
  • 在与 settings.py 同级目录下的 __init__.py 中引入模块和进行配置
1
2
3
4
// E:\proj\StudyDjangoAdmin\StudyDjangoAdmin\__init__.py

import pymysql
pymysql.install
  • 默认使用的sqlite,本次选择使用mysql,(win7 mysql安装详解 )

  • 使用heidisql连接本地mysql数据库后,新建一个数据库,名称为:runoob1

  • 生成表结构

1
E:\proj\StudyDjangoAdmin>python manage.py migrate
  • 如果报错,MySQLclient 目前只支持到 Python3.4,因此如果使用的更高版本的 python,把D:\app\Python37\Lib\site-packages\django\db\backends\mysql\base.py 对应报错代码注释

  • 迁移知识点:迁移是非常强大的功能,它能让你在开发过程中持续的改变数据库结构而不需要重新删除和创建表 - 它专注于使数据库平滑升级而不会丢失数据,改变模型需要这三步:

image-20250909091526544

  • 创建超级用户
1
2
3
4
5
6
执行:
python manage.py createsuperuser

-- 提示输入创建的账号: admin
-- 邮箱:可直接回车不输
-- 密码、输两次

image-20250909093243315

修改为中文的界面

找到settings.py里的LANGUAGE_CODE 和TIME_ZONE修改:

1
2
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai

新增用户

  • 在admin后台新增用户,确保人员状态正确

image-20250911085956081

创建APP

  • Django 规定,如果要使用ORM模型,必须要创建一个 app
1
django-admin startapp TestModel

注册app

  • 接下来在 settings.py 中找到INSTALLED_APPS这一项,加入这个app
1
2
3
4
5
6
7
8
9
10
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'TestModel', # 添加此项

]

模型

  • 我们修改 TestModel/models.py 文件
1
2
3
4
5
6
7
8
from django.db import models


# Create your models here.
class Book(models.Model):
title = models.CharField(max_length=32)
price = models.DecimalField(max_digits=5, decimal_places=2)
pub_date = models.DateField()
  • 生成数据表
1
2
3
4
# 让 Django 知道我们在我们的模型有一些变更
python manage.py makemigrations TestModel
# 创建表结构
python manage.py migrate TestModel

image-20250911092703957

应用实例

  • 书籍的增删改查

  • TestModel/view

1
2
3
4
5
6
7
8
9

from django.http import HttpResponse
from django.template import loader
from TestModel import models
def book_list(request):
latest_book_list = models.Book.objects.order_by("-pub_date")[:5]
template = loader.get_template("index.html")
context = {"latest_book_list": latest_book_list}
return HttpResponse(template.render(context, request))
  • html模板文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li><a href="/book/{{ book.id }}/">{{ book.title }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
</body>
</html>
  • 要为 TestModel 应用定义一个 URLconf,创建一个名为 TestModel/urls.py 的文件,并包含以下内容:
1
2
3
4
urlpatterns = [
path("book/", views.book_list, name="book_list"),

]
  • 把TestModel应用的url引入
1
2
3
4
5
6
7
8
9
10
# StudyDjangoAdmin/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('TestModel.urls')),

]

  • 手动在book表中插入一些数据后,然后访问book

image-20250914155359641

登录限制

  • TestModel/view 加入登录检验
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
from django.contrib.auth.decorators import login_required


def login(request):
return render(request, "login.html")


def login_action(request):
if request.method == 'POST':
username = request.POST.get("username", "")
password = request.POST.get("password", "")
#接受2个参数,用户名 密码,在用户名密码正确的情况下返回一个user对象。如果不正确,返回None
user = auth.authenticate(username=username, password=password)
if user is not None:
auth.login(request, user) # 登录
request.session['user'] = username # 将session信息记录到浏览器
response = HttpResponseRedirect('/event_manage/')
return response
else:
return render(request, "login.html", {'error': '用户名或者密码错误'})

@login_required
def logout_func(request):
auth.logout(request)
return HttpResponse("退出登录成功,<a href='/login'>重新登录</a>")

@login_required
def event_manage(request):
return render(request, 'event_manage.html')

@login_required
def book_list(request):
latest_book_list = models.Book.objects.order_by("-pub_date")[:5]
template = loader.get_template("index.html")
context = {"latest_book_list": latest_book_list}
return HttpResponse(template.render(context, request))

  • event_manage.html文件
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head><title>登录管理</title></head>
<body>
<h1>登录成功</h1>
<p>进入到<a style="font-size:18px" href="/TestModel/book">主页</a><strong</p>
</body>
</html>
  • login.html
1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head><title>Django Page</title></head>
<body><h1>登录</h1>
<form method="POST" action="/login_action/">
<input name="username" type="text" placeholder="username"><br>
<input name="password" type="password" placeholder="password"><br>
{{ error }} <br/>
<button id="btn" type="submit">登录</button>
{% csrf_token %}
</form>
</body>
</html>
  • 书籍主页的html,加入了一个退出登录
1
2
3
<p><a href="/logout">退出登录</a></p>
{% if latest_book_list %}
...
  • 当没有登录的情况下,访问书籍主页,直接就调整到了登录界面

image-20250914141655730

  • 这里登录限制实现后,有个不灵活的地方,就是每次都要手动加上@login_required,之前文章里面实现过,可以参考这里

新增

  • 主页的index.html加入了新增
1
2
3
<div>
<a href="{% url 'event_add' %}">新增</a>
</div>
  • 应用中加入了两个add路由
1
2
3
4
5
6
7
8
urlpatterns = [
path("book/", views.book_list, name="book_list"),
# 新增界面
path("event_add/", views.event_add, name="event_add"),
# 提交新增数据
path("book_add/", views.book_add, name="book_add"),

]
  • view代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@login_required
def event_add(request):
return render(request, 'add.html')

@login_required
def book_add(request):
if request.method == 'POST':
title = request.POST.get("title", "")
price = request.POST.get("price", "")
d_price = float(price)
pub_date = datetime.now().strftime("%Y-%m-%d")
book = models.Book(title=title, price=d_price, pub_date=pub_date)
book.save()
# 保存成功后,调整到首页
return redirect('/book/')

image-20250914175243952

image-20250914175312086

编辑

  • index.html,加入了编辑
1
2
3
4
5
6
7
8
9
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li>{{book.id}}--<a href="#">{{book.title}}</a>--<a href="{% url 'book_edit' book.id%}">编辑</a></li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
  • view代码。如果是get请求,就读取数据并且跳转到edit.html,然后是post请求直接就修改数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@login_required
def book_edit(request, book_id):
if request.method == 'POST':
title = request.POST.get("title", "")
price = request.POST.get("price", "")
d_price = float(price)
pub_date = datetime.now().strftime("%Y-%m-%d")
book = Book.objects.get(pk=book_id)
book.price = d_price
book.pub_date = pub_date
book.title = title
book.save()
return redirect('book_list')
else:
# 处理GET请求,显示编辑表单
book = get_object_or_404(Book, pk=book_id)
return render(request, 'edit.html', {'book': book})
  • url修改
1
2
path("book_edit/<int:book_id>/", views.book_edit, name="book_edit"),

image-20250914195750503

明细页面

  • index.html修改
1
2
3
4
5
6
7
8
9
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li>{{book.id}}--<a href="{% url 'book_detail' book.id%}">{{book.title}}</a>--<a href="{% url 'book_edit' book.id%}">编辑</a></li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
  • view.py
1
2
3
4
5
6
@login_required
def book_detail(request, book_id):
book = get_object_or_404(Book, pk=book_id)
return render(request, 'detail.html', {
'book': book
})
  • url
1
path("book_detail/<int:book_id>/", views.book_detail, name="book_detail"),
  • detail.html明细页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>详情页面</title>
</head>
<body>
<div>
<p>id:{{ book.id }}</p>
<p>标题:{{ book.title }}</p>
<p>价格:{{ book.price }}</p>
<p>日期:{{ book.pub_date }}</p>
</div>
<a href="{% url 'book_list' %}">Back to List</a>

</body>
</html>

删除

  • index.html 加入了删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% if latest_book_list %}
<ul>
{% for book in latest_book_list %}
<li>{{book.id}}--<a href="{% url 'book_detail' book.id%}">{{book.title}}</a>--<a href="{% url 'book_edit' book.id%}">编辑</a>
<form action="{% url 'book_delete' book.id %}" method="post" style="display:inline;">
{% csrf_token %}
<button type="submit" onclick="return confirm('确定要删除这本书吗?')">删除</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p>No book are available.</p>
{% endif %}
  • view加入删除
1
2
3
4
5
6
@login_required
def book_delete(request,book_id):
if request.method == 'POST':
book = get_object_or_404(Book, pk=book_id)
book.delete()
return redirect('book_list')
  • url
1
path("book_delete/<int:book_id>/", views.book_delete, name="book_delete"),

查询

  • 书籍列表index.html加入搜索
1
2
3
4
5
6
7
8
9
10
11
12
<p><a href="/logout">退出登录</a></p>

<form class="navbar-form" method="get" action="{% url 'book_list' %}">
<div class="form-group">
<input type="text" name="title" value="{{ search_term }}">
</div>
<button type="submit" class="btn btn-success">
搜索
</button>
</form>
{% if latest_book_list %}
....
  • view代码修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@login_required
def book_list(request):
title = request.GET.get("title", "")

if title:
books = Book.objects.filter(
title__contains=title
).order_by("-pub_date")[:5]
else:
books = Book.objects.order_by("-pub_date")[:5]

return render(request, 'index.html', {
'latest_book_list': books,
'search_term': title
})

分页

  • view的book_list加入分页,有自带的Paginator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

@login_required
def book_list(request):
title = request.GET.get("title", "")

if title:
books = Book.objects.filter(
title__contains=title
).order_by("-pub_date")
else:
books = Book.objects.order_by("-pub_date")

# Set up pagination
paginator = Paginator(books, 5) # Show 5 books per page.
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
logger.debug(f"Page number: {page_number}, Page obj: {page_obj}")
return render(request, 'index.html', {
'latest_book_list': page_obj,
'search_term': title
})
  • index.html加入分页样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
!-- Pagination controls -->
<div class="pagination">
<span class="step-links">
{% if latest_book_list.has_previous %}
<a href="?page=1&title={{ search_term }}">« 首页</a>
<a href="?page={{ page_obj.previous_page_number }}&title={{ search_term }}">上一页</a>
{% endif %}

<span class="current">
第 {{ latest_book_list.number }} 页 / 共 {{ latest_book_list.paginator.num_pages }} 页
</span>

{% if latest_book_list.has_next %}
<a href="?page={{ latest_book_list.next_page_number }}&title={{ search_term }}">下一页</a>
<a href="?page={{ latest_book_list.paginator.num_pages }}&title={{ search_term }}">末页 »</a>
{% endif %}
</span>
</div>
  • 查看结果

image-20250914231917746

退出登录

  • index.html
1
2
<p><a href="/logout">退出登录</a></p>

  • 在根目录的url配置
1
2
url(r'^logout/', views.logout_func),

  • 在应用的view中
1
2
3
4
5
@login_required
def logout_func(request):
auth.logout(request)
return HttpResponse("退出登录成功,<a href='/login'>重新登录</a>")

短语收集

  • It is to turn 轮到你付账
  • It is said 据说
  • be getting 比较级
    • she is getting more and more beautiful(她越来越漂亮)

昨天、今天

  • last night 昨天晚上
  • yesterday 昨天白天
  • tonight 今天晚上

现在分词和过去分词

  • 现在分词:令人…,用来表示主动进行的动作。
    • This work is tiring. I need a lie down.(这工作很累人。我需要躺下歇歇。)
    • She’s so interesting. I could listen to her talk for hours!(她真有趣。我可以长时间听她讲话!)
  • 过去式:感到…,表示被动完成的动作
    • I’m tired. I need a lie down.(我累了。我需要躺下休息。)
    • I’m really interested in what she has to say.(我对她说的话很感兴趣。)

都是基于动词,英语中的分词分为以 -ing 结尾的现在分词和多以 -ed 结尾的过去分词

我们通常用以 “-ed” 结尾的过去分词表示人的内在感受,比如:“tired(累的)”、“interested(感兴趣的)”。用 “-ing” 结尾的现在分词强调是谁或什么事情给人造成了这种感受,比如 “tiring(令人感到累的)”、“interesting(有趣的)”

常见句型

  • 主+谓(主语的动作)+宾(动作承受者)+状语(地点,时间。一般时间在地点后面)+宾语补足语+主语补足语

    • Jack plays basketball every morning
      • jack:主语
      • plays:谓语
      • 宾语:basketball
      • 状语:every morning
  • 主+系(am,is,are)+表,表示没有动作就用这个

    • 常见的很,在,是出现,适合这个句型
    • Lily is a little cat.
      • a little cat:表语,说明主语是什么
      • little:定语,形容cat的特征

说明

  • 上篇文章 主要介绍了spring cloud的业务演示,本次主要介绍服务治理

服务治理介绍

前面我们实现了微服务之间的调用,但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:

  1. 一旦服务提供者地址变化,就需要手工修改代码
  2. 一旦服务提供者为多个,无法实现负载均衡功能
  3. 一旦服务变的越来越多,人工维护调用关系困难

那么应该则么解决呢?这时候就需要通过注册中心动态的实现服务治理。

什么是服务治理?

服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册和发现。

服务注册: 在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。

服务发现: 服务调用方向服务注册中心咨询服务,并获取所有服务的服务清单,实现对具体服务实例的访问。

image-20210618154855845

除了微服务,还有一个组件是服务注册中心, 他是微服务架构中非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

  1. 服务发现
    1. 服务注册: 保存服务提供者和服务调用者的信息
    2. 服务订阅: 服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
  2. 服务配置
    1. 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
    2. 配置下发:主动将配置推送给服务提供者和服务调用者
  3. 服务健康检测
    1. 检测服务提供者的健康情况,如果发现异常,执行服务剔除

常见的注册中心

Zookeeper

  • zookeeper是一个分布式服务框架,是Apache Hadoop的一个子项目,它主要用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务,状态同步服务,集群管理,分布式应用配置项的管理等

Eureka

  • Eureka是Spring Cloud Netflix中的重要组件,主要作用就是做服务注册和发现。

Consul

  • Consul是基于Go语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册,服务发现和配置管理等功能。Consul的功能都很实用,其中包括:服务注册/发现,健康检查,Key/Value存储,多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,执行对应的脚本即可。

https://www.consul.io/

Nacos

  • Nacos 致力于帮助您发现、配置和管理微服务。它是Spring Cloud Alibaba的组件之一

nacos

  • 打开官网下载2.3.1.zip的版本
1
2
3
4
## 进入解压目录,进入bin目录
cd nacos/bin
##命令启动
startup.cmd -m standalone
  • 结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
D:\exe\nacos\bin>startup.cmd -m standalone
"nacos is starting with standalone"

,--.
,--.'|
,--,: : | Nacos 2.3.1
,`--.'`| ' : ,---. Running in stand alone mode, All function modules
| : : | | ' ,'\ .--.--. Port: 8848
: | \ | : ,--.--. ,---. / / | / / ' Pid: 14064
| : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.163.198:8848/nacos/index.html
' ' ;. ;.--. .-. | / / '' | |: :| : ;_
| | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io
' : | ; .' ," .--.; |' ; :__| : | `----. \
| | '`--' / / ,. |' | '.'|\ \ / / /`--' /
' : | ; : .' \ : : `----' '--'. /
; |.' | , .-./\ \ / `--'---'
'---' `--`---' `----'

2024-03-27 19:39:12,401 INFO Tomcat initialized with port(s): 8848 (http)

2024-03-27 19:39:12,850 INFO Root WebApplicationContext: initialization completed in 3709 ms

image-20240327194023531

将商品微服务注册到Nacos

  • shop-goods模块中依赖文件加入
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 主类上添加@EnableDiscoveryClient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.goods;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class GoodsApp {

public static void main(String[] args) {
SpringApplication.run(GoodsApp.class);
}
}

  • application.yml配置中添加nacos的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: shop-goods
datasource:
password: root
username: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
server:
port: 8002

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • 启动商品模块的微服务后,查看到数据

image-20240327194825898

将用户微服务注册到Nacos

  • 步骤同上

将订单微服务注册到Nacos

  • 步骤同上

在APP应用服务中添加Nacos

  • 步骤同上

dao修改

使用nacos调用微服务

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
package xyz.shi.shop.app.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
@LoadBalanced
public RestTemplate restTemplate(ClientHttpRequestFactory requestFactory){
// return new RestTemplate(requestFactory);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}

@Bean
public ClientHttpRequestFactory requestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);//单位为ms
factory.setConnectTimeout(5000);//单位为ms
return factory;
// return new SimpleClientHttpRequestFactory();
}
}

​ @LoadBalanced 表示加上负载均衡

service修改

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
package xyz.shi.shop.app.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.order.pojo.Order;

import java.math.BigDecimal;
import java.util.Map;

@Service
public class BuyServiceImpl implements BuyService {
@Autowired
private RestTemplate restTemplate;
private String shopUserName = "shop-user";
private String shopGoodsName = "shop-goods";
private String shopOrderName = "shop-order";

@Override
public Result submitOrder(BuyParams buyParams) {
String userResult = restTemplate.getForObject("http://" + shopUserName + "/user/findUser/" + buyParams.getUserId(), String.class);
Result<UserBO> userBOResult = JSON.parseObject(userResult, new TypeReference<Result<UserBO>>() {
});
if (userBOResult == null || !userBOResult.isSuccess() || userBOResult.getData() == null) {
return Result.fail(10001, "用户不存在");
}
UserBO userBO = userBOResult.getData();

String goodsResult = restTemplate.getForObject("http://" + shopGoodsName + "/goods/findGoods/" + buyParams.getGoodsId(), String.class);
Result<GoodsBO> goodsBOResult = JSON.parseObject(goodsResult, new TypeReference<Result<GoodsBO>>() {
});

if (goodsBOResult == null || !goodsBOResult.isSuccess() || goodsBOResult.getData() == null) {
return Result.fail(10002, "商品不存在");
}
GoodsBO goodsBO = (GoodsBO) goodsBOResult.getData();
Integer goodsStock = goodsBO.getGoodsStock();
if (goodsStock < 0) {
return Result.fail(10003, "商品库存不足");
}
BigDecimal goodsPrice = goodsBO.getGoodsPrice();
BigDecimal account = userBO.getAccount();
if (account.compareTo(goodsPrice) < 0) {
return Result.fail(10004, "余额不足");
}
Order orderParams = new Order();
orderParams.setUserId(userBO.getId());
orderParams.setGoodsId(goodsBO.getId());
orderParams.setOrderPrice(goodsBO.getGoodsPrice());
String orderResult = restTemplate.postForObject("http://" + shopOrderName + "/order/createOrder", orderParams, String.class);
Result<String> orderResultString = JSON.parseObject(orderResult, new TypeReference<Result<String>>() {
});
if (orderResultString == null || !orderResultString.isSuccess()) {
return Result.fail(10005, "下单失败");
}
String orderId = orderResultString.getData();

return Result.success(orderId);
}
}

老的代码:

String userResult = restTemplate.getForObject(“http://localhost:8001/user/findUser/" + buyParams.getUserId(), String.class);

新的代码

String goodsResult = restTemplate.getForObject(“http://“ + shopGoodsName + “/goods/findGoods/“ ..);

  • 通过以上的代码,我们发现各个微服务的地址,不需要硬编码在代码中了,通过在nacos中注册的微服务名称即可访问微服务。

测试

  • 所有的微服务模块全部启动后,查看nacos

image-20240327201224830

  • 测试结果再次如下
1
2
3
4
5
6
7
8
9
10
import requests

header = {"content-type": "application/json",}
data = {"userId": 1, "goodsId": 1111}
t = requests.post("http://localhost:8004/buy/submit", json=data, headers=header)
s = t.text
print(s)


{"success":true,"code":10001,"msg":"用户不存在","data":null}

负载均衡

  • 通俗的讲,负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。

  • 根据负载均衡发生位置的不同,一般分为服务端负载均衡客户端负载均衡

    • 服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。

    • 而客户端负载均衡指的是发生在服务请求一方,也就是在发送请求之前已经选好了由哪个实例处理请求。

image-20210619162742384

  • 在微服务调用关系中,一般会选择客户端负载均衡,也就是由服务调用一方来决定由哪个服务来提供执行

基于Feign实现服务调用

  • Feign是Spring Cloud提供的一个声明式的伪Http客户端,它使得调用远程服务就像调用本地服务一样简单,只需要创建一个接口并添加一个注解即可。

  • Nacos很好的集成了Feign,Feign默认集成了Ribbon,所以在Nacos下使用Feign默认就实现了负载均衡的效果

  • shop-app模块,加入Feign的依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 在主类上添加Feign的注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package xyz.shi.shop.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients

public class App {

public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
  • 创建各个微服务的feign接口,实现feign的微服务调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.app.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;

@FeignClient("shop-goods")
public interface GoodsFeign {

//调用路径同http访问路径
@GetMapping("/goods/findGoods/{id}")
public Result<GoodsBO> findGoods(@PathVariable("id") Long id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.app.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.order.pojo.Order;

@FeignClient("shop-order")
public interface OrderFeign {

//调用路径同http访问路径
@PostMapping("/order/createOrder")
public Result<String> createOrder(@RequestBody Order orderParams);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package xyz.shi.shop.app.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.user.UserBO;

@FeignClient("shop-user")
public interface UserFeign {

//调用路径同http访问路径
@GetMapping("/user/findUser/{id}")
public Result<UserBO> findUser(@PathVariable("id") Long id);
}
  • 更改BuyService的实现,使用feign来完成调用
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
package xyz.shi.shop.app.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import xyz.shi.shop.app.feign.GoodsFeign;
import xyz.shi.shop.app.feign.OrderFeign;
import xyz.shi.shop.app.feign.UserFeign;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.order.pojo.Order;

import java.math.BigDecimal;

@Service
public class BuyServiceImpl implements BuyService {
@Autowired
private UserFeign userFeign;
@Autowired
private GoodsFeign goodsFeign;
@Autowired
private OrderFeign orderFeign;

@Override
public Result submitOrder(BuyParams buyParams) {
Result<UserBO> userBOResult = userFeign.findUser(buyParams.getUserId());
if (userBOResult == null || !userBOResult.isSuccess() || userBOResult.getData() == null){
return Result.fail(10001,"用户不存在");
}
UserBO userBO = userBOResult.getData();
System.out.println(userBO);
Result<GoodsBO> goodsBOResult = goodsFeign.findGoods(buyParams.getGoodsId());
System.out.println(goodsBOResult);

if (goodsBOResult == null || !goodsBOResult.isSuccess() || goodsBOResult.getData() == null){
return Result.fail(10002,"商品不存在");
}
GoodsBO goodsBO = goodsBOResult.getData();
Integer goodsStock = goodsBO.getGoodsStock();
if (goodsStock < 0){
return Result.fail(10003,"商品库存不足");
}
BigDecimal goodsPrice = goodsBO.getGoodsPrice();
BigDecimal account = userBO.getAccount();
if (account.compareTo(goodsPrice) < 0){
return Result.fail(10004,"余额不足");
}
Order orderParams = new Order();
orderParams.setUserId(userBO.getId());
orderParams.setGoodsId(goodsBO.getId());
orderParams.setOrderPrice(goodsBO.getGoodsPrice());
Result<String> orderResultString = orderFeign.createOrder(orderParams);
System.out.println(orderResultString);

if (orderResultString == null || !orderResultString.isSuccess()){
return Result.fail(10005,"下单失败");
}
String orderId = orderResultString.getData();

return Result.success(orderId);
}
}

老代码

String goodsResult = restTemplate.getForObject(“http://“ + shopGoodsName + “/goods/findGoods/“ ..);

新代码

Result userBOResult = userFeign.findUser(buyParams.getUserId());

  • 重启APP服务,测试,整合上feign之后,我们发现,整个微服务的开发变的开始舒服起来。

说明

  • 上篇文章 主要介绍了spring cloud的基本概念和工程结构,本编开始实践演练

业务场景

下单服务:

  • 前端传递用户id和商品id

  • 判断用户是否存在以及账户是否有足够的金额

  • 判断商品是否存在以及库存是否足够

  • 生成订单

创建APP模块

  • pom.xml
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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>shop-app</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

  • 配置文件:src-resource-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-app
datasource:
password: root
username: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8004

mybatis-plus:
global-config:
db-config:
table-prefix: shop_xxxxxxxxxx spring: application:   name: shop-app datasource:   password: root   username: 123456   driver-class-name: com.mysql.cj.jdbc.Driver   url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCserver: port: 8004mybatis-plus: global-config:   db-config:     table-prefix: shop_spring: application:   name: shop-appserver: port: 8004
  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
package xyz.shi.shop.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}

用户微服务 提供用户查询服务

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package xyz.shi.shop.user.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("xyz.shi.shop.user.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

1
2
3
4
5
6
7
8
package xyz.shi.shop.user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xyz.shi.shop.user.pojo.User;

public interface UserMapper extends BaseMapper<User> {
}

service层

1
2
3
4
5
6
7
8
package xyz.shi.shop.user.service;

import xyz.shi.shop.common.Result;

public interface UserService {

Result findUser(Long id);
}
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
package xyz.shi.shop.user.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.user.mapper.UserMapper;
import xyz.shi.shop.user.pojo.User;
import xyz.shi.shop.user.service.UserService;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public Result findUser(Long id) {
User user = userMapper.selectById(id);
if (user == null){
return Result.success(null);
}
UserBO userBO = new UserBO();
userBO.setAccount(user.getAccount());
userBO.setId(user.getId());
return Result.success(userBO);
}
}
  • common模块下:
1
2
3
4
5
6
7
8
9
package xyz.shi.shop.common.user;
import lombok.Data;
import java.math.BigDecimal;

@Data
public class UserBO {
private Long id;
private BigDecimal account;
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.user.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.user.service.UserService;

@RestController
@RequestMapping("user")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("findUser/{id}")
public Result findUser(@PathVariable("id") Long id) {
return userService.findUser(id);
}
}

商品微服务,提供商品查询服务

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package xyz.shi.shop.goods.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("xyz.shi.shop.goods.mapper")
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

1
2
3
4
5
6
7
package xyz.shi.shop.goods.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xyz.shi.shop.goods.pojo.Goods;

public interface GoodsMapper extends BaseMapper<Goods> {
}

service层

1
2
3
4
5
6
7
8
package xyz.shi.shop.goods.service;

import xyz.shi.shop.common.Result;

public interface GoodsService {

Result findGoodsById(Long id);
}
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
package xyz.shi.shop.goods.service.impl;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.goods.mapper.GoodsMapper;
import xyz.shi.shop.goods.pojo.Goods;
import xyz.shi.shop.goods.service.GoodsService;

@Service
public class GoodsServiceImpl implements GoodsService {

@Autowired
private GoodsMapper goodsMapper;

@Override
public Result findGoodsById(Long id) {
Goods goods = goodsMapper.selectById(id);
GoodsBO goodsBO = new GoodsBO();
BeanUtils.copyProperties(goods,goodsBO);
return Result.success(goodsBO);
}
}
  • common模块下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package xyz.shi.shop.common.goods;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsBO {

private Long id;

private String goodsName;

private BigDecimal goodsPrice;

private Integer goodsStock;
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.goods.Controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.goods.service.GoodsService;

@RestController
@RequestMapping("goods")
public class GoodsController {

@Autowired
private GoodsService goodsService;

@GetMapping("findGoods/{id}")
public Result findGoods(@PathVariable("id") Long id){
return goodsService.findGoodsById(id);
}
}

订单微服务,提供生成订单服务

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package xyz.shi.shop.order.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("xyz.shi.shop.order.mapper")
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
1
2
3
4
5
6
7
package xyz.shi.shop.order.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xyz.shi.shop.order.pojo.Order;

public interface OrderMapper extends BaseMapper<Order> {
}

service

1
2
3
4
5
6
7
8
9
10
package xyz.shi.shop.order.service;


import xyz.shi.shop.common.Result;
import xyz.shi.shop.order.pojo.Order;

public interface OrderService {

public Result createOrder(Order orderParams);
}
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
package xyz.shi.shop.order.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.order.mapper.OrderMapper;
import xyz.shi.shop.order.pojo.Order;
import xyz.shi.shop.order.service.OrderService;
import org.apache.commons.lang3.RandomUtils;

@Service
public class OrderServiceImpl implements OrderService {

@Autowired
private OrderMapper orderMapper;
@Override
public Result createOrder(Order orderParams) {
Order order = new Order();
order.setCreateTime(System.currentTimeMillis());
order.setGoodsId(orderParams.getGoodsId());
order.setUserId(orderParams.getUserId());
order.setOrderPrice(orderParams.getOrderPrice());
order.setOrderId(System.currentTimeMillis()+""+orderParams.getUserId()+ ""+RandomUtils.nextInt(1000,9999));
order.setOrderStatus(0);
order.setPayStatus(0);
order.setPayTime(-1L);
this.orderMapper.insert(order);
return Result.success(order.getOrderId());
}
}

下单业务

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>

dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package xyz.shi.shop.app.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory requestFactory){
return new RestTemplate(requestFactory);
}

@Bean
public ClientHttpRequestFactory requestFactory(){
return new SimpleClientHttpRequestFactory();
}
}

service

1
2
3
4
5
6
7
8
package xyz.shi.shop.app.service;

import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.common.Result;

public interface BuyService {
Result submitOrder(BuyParams buyParams);
}
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
package xyz.shi.shop.app.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;
import xyz.shi.shop.common.goods.GoodsBO;
import xyz.shi.shop.common.user.UserBO;
import xyz.shi.shop.order.pojo.Order;

import java.math.BigDecimal;
import java.util.Map;

@Service
public class BuyServiceImpl implements BuyService {
@Autowired
private RestTemplate restTemplate;

@Override
public Result submitOrder(BuyParams buyParams) {
String userResult = restTemplate.getForObject("http://localhost:8001/user/findUser/" + buyParams.getUserId(), String.class);
Result<UserBO> userBOResult = JSON.parseObject(userResult,new TypeReference<Result<UserBO>>(){});
if (userBOResult == null || !userBOResult.isSuccess() || userBOResult.getData() == null){
return Result.fail(10001,"用户不存在");
}
UserBO userBO = userBOResult.getData();

String goodsResult = restTemplate.getForObject("http://localhost:8002/goods/findGoods/" + buyParams.getGoodsId(), String.class);
Result<GoodsBO> goodsBOResult = JSON.parseObject(goodsResult,new TypeReference<Result<GoodsBO>>(){});

if (goodsBOResult == null || !goodsBOResult.isSuccess() || goodsBOResult.getData() == null){
return Result.fail(10002,"商品不存在");
}
GoodsBO goodsBO = (GoodsBO) goodsBOResult.getData();
Integer goodsStock = goodsBO.getGoodsStock();
if (goodsStock < 0){
return Result.fail(10003,"商品库存不足");
}
BigDecimal goodsPrice = goodsBO.getGoodsPrice();
BigDecimal account = userBO.getAccount();
if (account.compareTo(goodsPrice) < 0){
return Result.fail(10004,"余额不足");
}
Order orderParams = new Order();
orderParams.setUserId(userBO.getId());
orderParams.setGoodsId(goodsBO.getId());
orderParams.setOrderPrice(goodsBO.getGoodsPrice());
String orderResult = restTemplate.postForObject("http://localhost:8003/order/createOrder", orderParams, String.class);
Result<String> orderResultString = JSON.parseObject(orderResult,new TypeReference<Result<String>>(){});
if (orderResultString == null || !orderResultString.isSuccess()){
return Result.fail(10005,"下单失败");
}
String orderId = orderResultString.getData();

return Result.success(orderId);
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package xyz.shi.shop.app.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.shi.shop.app.popj.BuyParams;
import xyz.shi.shop.app.service.BuyService;
import xyz.shi.shop.common.Result;

@RestController
@RequestMapping("buy")
public class BuyController {

@Autowired
private BuyService buyService;

@PostMapping("submit")
private Result submitOrder(@RequestBody BuyParams buyParams){
return buyService.submitOrder(buyParams);
}
}

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
package xyz.shi.shop.app.popj;

import lombok.Data;

@Data
public class BuyParams {

private Long userId;

private Long goodsId;
}

测试

  • 分别把各自模块下的XXApp.java启动,在idea中就可启动

image-20240327170052020

  • 测试结果如下
1
2
3
4
5
6
7
8
9
10
11
import requests

header = {"content-type": "application/json",}
data = {"userId": 1, "goodsId": 1111}
t = requests.post("http://localhost:8004/buy/submit", json=data, headers=header)
s = t.text
print(s)


{"success":true,"code":10001,"msg":"用户不存在","data":null}

总结

  • 本次代码在这里
  • 从上述的代码 可以看出,下单业务 调用各个微服务的时候 极其不方便。一旦服务提供方 更改ip,端口,接口名称等,服务消费方 也需要跟着变,如果服务提供方有多台服务器呢?服务消费方还得需要维护这些ip信息,甚至自己实现负载均衡。这时候 就需要 服务治理

说明

  • 从本篇文章开始学习微服务的知识
  • 本次文章主要来自于这里

系统架构演变

从互联网兴起到现在,系统架构大致经历了以下几个过程:

  1. 单体应用架构
  2. 垂直应用架构
  3. 分布式应用架构
  4. SOA架构
  5. 微服务架构
  6. Service Mesh(服务网格化)
  7. 无服务化

案例准备

电商系统:搭建用户微服务,商品微服务,订单微服务

技术选型

maven:3.5+

数据库:mysql 5.7

持久层:MybatisPlus

其他:Spring Cloud Alibaba

模块设计

父工程:shop-parent

用户微服务:shop-user

商品微服务:shop-goods

订单微服务: shop-order

公共服务: shop-common

应用服务:shop-app

image-20210609203537573

下单场景

用户下单,会涉及到用户微服务,商品微服务,订单微服务

搭建工程

创建父工程

  • 依赖文件
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/>
</parent>
<packaging>pom</packaging>
<properties>
<!-- Spring Cloud -->
<spring.cloud.version>Hoxton.SR9</spring.cloud.version>
<!-- Apache Dubbo -->
<dubbo.version>2.7.8</dubbo.version>
<curator.version>4.0.1</curator.version>
<!-- Apache RocketMQ -->
<rocketmq.starter.version>2.0.2</rocketmq.starter.version>
<spring.cloud.alibaba>2.2.5.RELEASE</spring.cloud.alibaba>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

</project>

<parent> 元素用于继承父级项目的默认配置和依赖管理,而 <dependencyManagement> 元素用于集中管理项目的依赖版本,确保团队中所有成员使用相同的依赖版本。这些元素的使用可以提高项目的组织性、可维护性和一致性。

创建shop-common模块

  • 模块名:shop-common

  • 依赖文件

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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>shop-common</artifactId>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>


  • 统一返回类
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
package xyz.shi.shop.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {

private boolean success;

private int code;

private String msg;

private T data;

public static <T> Result<T> success(T data) {
return new Result<>(true,200,"success",data);
}
public static <T> Result<T> fail(int code, String msg) {
return new Result<>( true,code,msg,null);
}
}

创建用户微服务

  • 模块名:shop_user
  • 创建pom依赖文件
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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop_user</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

注意这里依赖了shop-common 模块

  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package xyz.shi.shop.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserApp {

public static void main(String[] args) {
SpringApplication.run(UserApp.class,args);
}

}

  • 配置文件,src-resources-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-user
datasource:
password: root
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8001

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • shop_user表,在shop数据库中手动创建user表
1
2
3
4
5
6
7
8
CREATE TABLE `shop`.`shop_user`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`telephone` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`account` decimal(10, 2) NOT NULL COMMENT '账户',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
  • shop_user表实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.user.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
//分布式id
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
private String password;
private String telephone;
private BigDecimal account;
}

创建商品微服务

  • 模块名:shop-goods

  • 依赖

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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-goods</artifactId>
<!-- <packaging>war</packaging>-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

注意这里依赖了shop-common 模块

  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
package xyz.shi.shop.goods;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GoodsApp {

public static void main(String[] args) {
SpringApplication.run(GoodsApp.class);
}
}

  • 配置文件 src-resources-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-goods
datasource:
password: root
username: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8002

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • 商品表
1
2
3
4
5
6
7
8
CREATE TABLE `shop`.`shop_goods`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '商品名称',
`goods_price` decimal(10, 2) NOT NULL COMMENT '商品价格',
`goods_stock` int(10) NOT NULL COMMENT '商品库存',
`create_time` bigint(20) NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package xyz.shi.shop.goods.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class Goods {

@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String goodsName;
private BigDecimal goodsPrice;
private Integer goodsStock;
private Long createTime;
}

创建订单微服

  • 模块名:shop-order
  • 依赖
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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-order</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
  • 启动类
1
2
3
4
5
6
7
8
9
10
11
12
package xyz.shi.shop.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OrderApp {

public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
}
  • 配置文件 sr-resources-application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: shop-order
datasource:
password: root
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
server:
port: 8003

mybatis-plus:
global-config:
db-config:
table-prefix: shop_
  • 创建订单表
1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `shop`.`shop_order`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`user_id` bigint(20) NOT NULL,
`goods_id` bigint(20) NOT NULL,
`order_price` decimal(10, 2) NOT NULL,
`order_status` tinyint(4) NOT NULL,
`pay_status` tinyint(4) NOT NULL,
`pay_time` bigint(20) NOT NULL,
`create_time` bigint(20) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package xyz.shi.shop.order.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.math.BigDecimal;

@Data
public class Order {

@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String orderId;
private Long userId;
private Long goodsId;
private BigDecimal orderPrice;
private Integer orderStatus;
private Integer payStatus;
private Long payTime;
private Long createTime;
}

总结

  • 整个业务工程搭建已经完成,最终目录如下

image-20240318173650355

  • 后续开展业务场景的模拟

说明

  • 紧接 vue3系列一 文章
  • 环境等信息参考上述的文章

其它Composition API

toRef与toRefs

toRef

  • 作用:创建一个ref对象,其value值指向另一个对象中的某个属性值

  • 语法:const name=toRef(obj,‘name’)

  • 应用:要将响应式对象中的某个属性单独提供给外部使用时

  • 扩展:toRefs与toRef功能一致,但可以批量创建多个ref对象,toRefs(obj)

  • 如下实例:

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
<template>
<h2>toRef与toRefs</h2>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>薪水:{{person.job.salary}}k</h2>
<button @click="person.name+='!'">修改姓名</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.job.salary++">涨点薪资</button>
</template>

<script>
import {reactive} from 'vue'
export default {
name: "toRef_component",
setup(){
let person=reactive({
name:'二郎神杨杨戬',
age:18,
job:{
salary:20
}
})
return{
person,
}
}
}
</script>

<style scoped>

</style>

示例一里面直接返回person对象,导致每次取值的时候都需要person.xxx,这样既不美观也不优雅,修改一下代码。

  • 修改代码为
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
<template>
<h2>toRef与toRefs</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪水:{{salary}}k</h2>
<button @click="name+='!'">修改姓名</button>
<button @click="age++">修改年龄</button>
<button @click="salary++">涨点薪资</button>
</template>

<script>
import {reactive,toRef} from 'vue'
export default {
name: "toRef_component",
setup(){
let person=reactive({
name:'二郎神杨杨戬',
age:18,
job:{
salary:20
}
})
return{
name:toRef(person,'name'),
age:toRef(person,'age'),
salary:toRef(person.job,'salary')
}
}
}
</script>

<style scoped>

</style>

toRefs

  • 代码
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
<template>
<h2>toRef与toRefs</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪水:{{job.salary}}k</h2>
<button @click="name+='!'">修改姓名</button>
<button @click="age++">修改年龄</button>
<button @click="job.salary++">涨点薪资</button>
</template>

<script>
import {reactive,toRef,toRefs} from 'vue'
export default {
name: "toRef_component",
setup(){
let person=reactive({
name:'二郎神杨杨戬',
age:18,
job:{
salary:20
}
})
return{
...toRefs(person)
}
}
}
</script>

<style scoped>

</style>

shallowRef和shallowReactive

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)

  • shallowRef:只处理基本数据类型的响应式,不进行对象的响应式处理

  • 什么时候用

    • 如果有一个对象数据,结构比较深,但变化时只是外层属性变化用shallowReactive

    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换用shallowRef

  • 感觉没有什么用,不做演示

readonly和shallowReadonly

  • readonly:让一个响应式数据变为只读的(深只读)

  • shallowReadonly:让一个响应式变为只读的(浅只读)

toRaw和markRaw

  • toRaw

    • 作用:将一个由reactive生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新
  • markRow

    • 作用:标记一个对象,使其永远不会再成为响应式对象

      应用场景

    • 有些值不应该被设置为响应式的,例如复杂的第三方类库,

    • 当渲染具有不可变的数据源的大列表时,跳过响应式转换可以提高性能

customRef

  • 作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制,它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。
    实现防抖效果:
  • 1、先实现自定义双向绑定
  • 2、实现了双向绑定之后,实现防抖

响应式数据的判断

  • isRef:检查一个值是否为ref对象
  • isReactive:检查一个对象是否由reactive创建的响应式代理
  • isReadonly:检查一个对象是否由readonly创建的只读代理
  • isProxy:检查一个对象是否由reactive或者readonly方法创建的代理

provide和inject

  • 在父子组件传递数据时,通常使用的是 props 和 emit,父传子时,使用的是 props,如果是父组件传孙组件时,就需要先传给子组件,子组件再传给孙组件,如果多个子组件或多个孙组件使用时,就需要传很多次,会很麻烦。
  • 像这种情况,可以使用 provide inject 解决这种问题,不论组件嵌套多深,父组件都可以为所有子组件或孙组件提供数据,父组件使用 provide 提供数据,子组件或孙组件 inject 注入数据。同时兄弟组件之间传值更方便。

img

  • 祖组件

    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
    <template>
    <h2>我是祖组件</h2>
    <h3>汽车信息</h3>
    <p>名称:{{name}}</p>
    <p>价格:{{price}}</p>
    <inject_component></inject_component>
    </template>

    <script>
    import {reactive,toRefs,provide} from 'vue'
    import inject_component from "./inject.vue"

    export default {
    name: "provide_component",
    setup(){
    let car=reactive({
    name:'宝马',
    price:'40w'
    });
    provide('car',car); // 提供provide
    return{
    ...toRefs(car)
    }
    },
    components:{
    inject_component
    }
    }
    </script>

    <style scoped>

    </style>

  • 后代组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<h2>我是孙组件</h2>
<h3>汽车信息</h3>
<p>名称:{{name}}</p>
<p>价格:{{price}}</p>
</template>

<script>
import {inject,toRefs,ref} from 'vue'
export default {
name: "inject_component",
setup(){
let car=inject("car"); //使用inject接收
return{
...toRefs(car)
}
}
}
</script>

<style scoped>

</style>

  • 运行结果

image-20240307151409520

在vue2中

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
//父组件

export default{

provide:{

info:"提供数据"

}

}

//子组件

export default{

inject:['info'],

mounted(){

console.log("接收数据:", this.info) // 接收数据:提供数据

}

}
  • provide / inject 类似于消息的订阅和发布。provide 提供或发送数据, inject 接收数据。

Vue2x和Vue3x的其它变化

1.全局API的转移

vue2.x有许多全局API和配置,例如:全局注册组件、注册全局指令等

  • 注册全局组件
1
2
3
4
5
6
7
Vue.component('MyButton',{
data:()=>{
count:0,
},
template:'<button @click="count++">clicked {{count}} times</button>'
});

  • 注册全局指令,
1
2
3
4
Vue.directive('focus',{
inserted:el=>el.foucus
})

  • Vue3.0中对这些API做出了调整

  • 将全局的API,即Vue.xxx调整到应用实例app上

2.x 全局API(Vue) 3.x 实例API(app)
Vue.config.xxx app.config.xxx
Vue.config.productionTip 移除
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties
  • 移除过滤器(filter):过滤器虽然看起来方便,但它需要一个自定义语法,打破大括号内表达式’只是javascript’的假设,这不仅有学习成本,而且有实现成 本, 建议用法调用或者计算属性去替换过滤器

项目搭建

  • node.js 安装16.0以上的版本
1
2
3
4
5
C:\Users\Administrator>node -v
v18.17.0

D:\project>npm -v
9.6.7
  • vite初始化Vue3项目
1
2
3
4
5
6
7
8
9
10
11
12
D:\project>npm init vite-app vue3Demo
Need to install the following packages:
create-vite-app@1.21.0
Ok to proceed? (y) y

cd vue3Demo
npm install
D:\project\vue3Demo>npm run dev
Dev server running at:
> Network: http://192.168.57.198:3000/
> Local: http://localhost:3000/

项目结构分析

  • 这里的项目目录结构分析主要是main.js文件
  • Vue2里面的main.js
1
2
3
4
5
6
new Vue({
el: '#app',
components: {},
template: ''
});

  • Vue3里面的main.js
1
2
3
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
  • 在Vue2里面,通过new Vue({})构造函数创建应用实例对象,而Vue3引入的不再是Vue的构造函数,引入的是一个名为createApp的工厂函数创建应用实例对象。

  • Vue3-devtool获取

Composition API

setp

  • setup是所有Composition API(组合式API)的入口,组件中所用到的数据、方法等等,均要配置在setup里面

  • 组件中所用到的数据、方法等等,均要配置在setup里面

  • setup的执行时机

    • 在beforeCreate之前执行一次,此时this为undefined
  • setup函数的两种返回值

    • 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用
    • 若返回一个渲染函数,则可以自定义渲染内容
  • setup的参数

    • props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性
    • context:上下文对象,接收参数 context 内部函数props接受的自定义属性
      • attrs:值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性,相当于this.$attrs
      • slots:收到的插槽内容,相当于this.$slots
      • emit:分发自定义事件的函数,相当于this.$emit

注意事项:

尽量不要与Vue2x的配置使用

  • Vue2x的配置(data、methods、computed)均可以访问到setup中的属性、方法
  • setup中不能访问Vue2x的配置(data、methods、computed)
  • 如果data里面的属性和setup里面的属性有重名,则setup优先

返回值

示例一:setup函数的两种返回值

  • 安装路由依赖
1
npm install vue-router@4 --save
  • src\router\routers.js 手动新建自定义路由
1
2
3
4
5
6
7
8
const routes = [
{
name: "home",
path: "/home",
component: () => import("../components/views/home.vue")
}
]
export default routes;

src\router\index.js 对外暴露路由

1
2
3
4
5
6
7
8
9
10
import { createRouter, createWebHistory } from "vue-router"

// createRouter方法用于创建路由实例对象
// createWebHashHistory方法用于指定路由的工作模式(hash模式
import routes from "./routers"
var router=createRouter({
history:createWebHistory(),
routes
})
export default router
  • src-main.js内容引用路由
1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from "./routers/index.js"

const app = createApp(App)

app.use(router)
app.mount("#app")

  • 编写src-components-views-home.vue
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
<template>
<h2>练习setup相关内容</h2>
<!--<h2>setup返回一个对象,并使用对象中的属性和方法</h2>-->
<!--<p>姓名:{{student.name}}</p>-->
<!--<p>年龄:{{student.age}}</p>-->
<!--<button @click="hello">点击查看控制台信息</button>-->
<hr>
<h2>setup返回一个函数</h2>
</template>

<script>
import {h} from 'vue'
export default {
name: "setupComponent",
setup(){
// 属性
let student={
name:'张三',
age:18,
}
// 方法
function hello() {
console.log(`大家好,我叫${student.name},今年${student.age}`)
}
return{ // 返回一个对象
student,
hello,
}
// return()=>h('h1','你好') // 返回一个函数
}
}
</script>

<style scoped>

</style>


这里需要注意的是setup里面定义的属性和方法均要return出去,否则无法使用

  • 启动测试
1
npm run dev

示例二:setup里面的参数和方法和配置项混合使用

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
<template>
<h2>setup和配置项混用</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>性别:{{sex}}</h2>
<button @click="sayHello">sayHello(Vue3里面的方法)</button>
<button @click="sayWelcome">sayWelcome(Vue2里面的方法)</button>
</template>

<script>
export default {
name: "setup01_component",
data(){
return{
sex:'男',
sum:0,
}
},
methods:{
sayWelcome(){
console.log(`sayWelcome`)
},
},
setup(){
let sum=100;
let name='张三';
let age=18;
function sayHello() {
console.log(`我叫${name},今年${age}`)
}
return{
name,
age,
sayHello,
sum
}
}
}
</script>

<style scoped>

</style>


  • 这段代码是先实现了setup里面的属性和方法,以及Vue2中配置项里面的属性和方法。接下来添加对应的混合方法
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
<template>
<h2>setup和配置项混用</h2>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>性别:{{sex}}</h2>
<button @click="sayHello">sayHello(Vue3里面的方法)</button>
<button @click="sayWelcome">sayWelcome(Vue2里面的方法)</button>
<br>
<br>
<button @click="test01">测试Vue2里面调用Vue3里面的属性和方法</button>
<br>
<br>
<button @click="test02">测试Vue3setup里面调用Vue2里面的属性和方法</button>
<br>
<h2>sum的值是:{{sum}}</h2>
</template>

<script>
export default {
name: "setup01_component",
data(){
return{
sex:'男',
sum:0,
}
},
methods:{
sayWelcome(){
console.log(`sayWelcome`)
},
test01(){
console.log(this.sex); // Vue2里面的属性(data里面的属性)
// setup里面的属性
console.log(this.name);
console.log(this.age);
// setup里面的方法
this.sayHello();
}
},
setup(){
let sum=100;
let name='张三';
let age=18;
function sayHello() {
console.log(`我叫${name},今年${age}`)
}
function test02() {
// setup里面的属性
console.log(name);
console.log(age);

// data里面的属性
console.log(this.sex);
console.log(this.sayWelcome);
}
return{
name,
age,
sayHello,
test02,
sum
}
}
}
</script>

<style scoped>

</style>

测试Vue3setup里面调用Vue2里面的属性和方法时,this.sex无数据

Vue2里面props和slot的使用

讲解setup这里面的两个参数之前,先回顾一下Vue2里面的相关知识

  • props和自定义事件的使用
  • attrs
  • slot(插槽)

示例一:Vue2props和自定义事件的使用

准备两个组件,分别为parent.vue组件和child.vue组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="parent">
我是父组件
<child msg="传递信息" name="张三" @sendParentMsg="getMsg"/>
</div>
</template>
<script>
import Child from "./child.vue";
export default {
name: "Parent",
components: {Child},
methods:{
getMsg(msg){
console.log(msg)
}
}
}
</script>
<style scoped>
.parent{
padding: 10px;
background-color: red;
}
</style>
  • child
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
<template>
<div class="child">
<h2>我是子组件</h2>
<p>父组件传递过来的消息是:{{msg}}</p>
<p>父组件传递过来的消息是:{{name}}</p>
<button @click="sendMsg">向父组件的传递信息</button>
</div>
</template>
<script>
export default {
name: "Child",
props:{
msg:{
type:String,
default:''
},
name:{
type:String,
default:''
}
},
mounted(){
console.log(this);
},
methods:{
sendMsg(){
this.$emit("sendParentMsg",'通知父组件更新123')
}
}
}
</script>
<style scoped>
.child{
padding: 10px;
background-color: orange;
}
</style>

vue2_props.gif

总结
  • 子组件通过props接收父组件传递的信息,通过this.$emit()自定义事件向父组件传递信息。当使用props接收数据的时候,attrs里面的数据为空,如果没有使用props接收数据的话,那么props里面就有值。

Vue2里面slot的使用

  • 这两个分类栏里的数据都不一样,但是整体结构是一样的,这就要求组件的结构一样,但是内部 DOM 结构是由使用组件的时候决定的,这就需要插槽

  • 同理准备两个组件,一个Index.vue组件,另一个为MySlot.vue组件

  • Index.vue

    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
    <template>
    <div class="index">
    <h2>我是Index组件</h2>
    <!--写法一-->
    <my-slot>
    <!--插槽里面的内容-->
    <h2>传入的slot参数</h2>
    <h2>传入的slot参数</h2>
    <h2>传入的slot参数</h2>
    <h2>传入的slot参数</h2>
    </my-slot>
    <!--写法二-->
    <my-slot>
    <div slot="header">
    <span>我在header插槽中</span>
    </div>

    <div slot="header">
    <span>我是footer附件</span>
    </div>
    </my-slot>
    </div>
    </template>

    <script>
    import MySlot from "./MySlot.vue";
    export default {
    name: "Index",
    components: {MySlot}
    }
    </script>

    <style scoped>
    .index{
    padding: 10px;
    background: red;
    }
    </style>

  • MySlot

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
<template>
<div class="slot">
<h2>我是MySlot组件1</h2>
<slot></slot>
<br>
<slot name="header">header</slot>
<br>
<slot name="footer">footer</slot>
</div>
</template>

<script>
export default {
name: "MySlot",
mounted(){
console.log(this);
}
}
</script>

<style scoped>
.slot{
padding: 10px;
background: orange;
}
</style>

image-20240229145232356

ref

  • 作用:ref( ) 接受一个内部值,返回一个ref 对象,这个对象是响应式的、可更改的,且只有一个指向其内部值的属性 .value。

  • 语法:const xxx=ref(initValue)

  • 创建一个包含响应式数据的引用对象(reference对象);

  • JS中操作数据:xxx.value=xxx,模板中读取数据:不需要.value,直接:{{xxx}}

  • 备注:

    • 接收的数据可以是:基本类型,也可以是对象类型

    • 基本类型的数据:响应式依然是靠Object.defineProperty()的get和set完成的

    • 对象类型的数据:内部求助了Vue3.0中的一个新函数-reactive函数

  • 测试代码

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
<template>
<h1>ref</h1>
<h2>ref定义基本数据类型</h2>
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<p>婚否:{{isMarry}}</p>
<h2>ref定义对象类型</h2>
<p>爱好:{{hobby}}</p>
<p>证件类型:{{user.idCard}}</p>
<p>国籍:{{user.nation}}</p>
<button @click="changeName">修改信息</button>
</template>

<script>
import {ref} from 'vue'
export default {
name: "refComponent",
setup(){
// 使用基本数据类型 number,string,boolean,
let name=ref('张三');
let age=ref(18);
let isMarry=ref(false);
// 使用ref定义数组
let hobby=ref(['吃饭','睡觉','打豆豆']);
// 使用ref定义对象
let user=ref({
idCard:'身份证',
nation:['中国','美国','英国','俄罗斯']
})
function changeName() {
// 修改基本数据数据类型
name.value='李四'; // ref定义的响应式数据修改数据时必需要.value
age.value=20;
isMarry.value=true;
// 修改对象数据类型
hobby.value[0]='玩游戏';
user.value.idCard='港澳台居民身份证';
user.value.nation[0]='挪威';
}
return{
name,
age,
isMarry,
changeName,
user,
hobby
}
}
}
</script>

<style scoped>

</style>

image-20240229150642949

点击修改信息数据变化

image-20240229150702474

  • ref定义的响应式数据修改数据时必需要.value
  • ref定义的对象数据类型,内部求助了Vue3.0中的一个新函数-reactive函数(看下面的介绍)
  • 模板中使用数据时不需要.value

reactive

  • 作用:定义一个对象类型的响应式数据(基本类型别用它,用ref函数)

  • const 代理对象=reactive(被代理的对象)接收一个对象(或数组),返回一个代理器对象(Proxy的实例对象,简称Proxy对象)

  • reactive定义的响应式数据是深层次的

  • 内部基于ES6的Proxy实现,通过代理对象的操作源对象的内部数据都是响应式的

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
<template>
<h2>reactive响应式数据</h2>
<p>姓名:{{student.name}}</p>
<p>年龄:{{student.age}}</p>
<p>爱好:{{student.hobbies}}</p>
<button @click="changeStuInfo">改变学生信息</button>
</template>

<script>
import {reactive} from 'vue'
export default {
name: "reactiveComponent",
setup(){
// 数据
let student=reactive({
name:'张三',
age:19,
hobbies:['吃饭','睡觉','打豆豆']
});
console.log(student)
// 方法
function changeStuInfo() {
student.name='李四';
student.age=20;
student.hobbies[0]='做家务'
}
return{
student,
changeStuInfo,
}
}
}
</script>

<style scoped>

</style>

image-20240229153509613

  • 点击改变学生信息后

image-20240229153542699

reactive对比ref

  • 从定义数据的角度对比

    • ref用来定义:基本类型数据
    • reactive用来定义:对象(或数组)类型数据
    • 备注:ref也可以用来定义对象(或数组)类型的数据,它内部会自动通过reactive转为代理对象
  • 从原理角度对比

    • ref通过Object.defineProperty()的get和set实现(响应式)数据劫持
    • reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据
  • 从使用角度

    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
    • reactive定义的数据:操作数据与读取数据均不需要.value

watch和watchEffect

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
   //	attr表示需要监视的属性
// 情况一:监视单个ref定义的响应式数据
watch(attr,(newValue,oldValue)=>{
console.log('attr变化了',newValue,oldValue);
})

// 情况二; 监视多个ref定义的响应式数据
watch([attr1,attr2,....,attrn],(newValue,oldValue)=>{
console.log('attr1或attrn变化了',newValue,oldValue);
})

// obj表示需要监听的对象
// 情况三:监视reactive定义的响应式数据
// 若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue
// 若watch监视的是reactive定义的响应式数据,则强制打开开启了深度监视
watch(obj,(newValue,oldValue)=>{
console.log('obj变化了',newValue,oldValue)

},{immediate:true,deep:false}); // 此处deep配置不在奏效

// 情况四,监视reactive定义的响应式数据中的某个属性
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})

// 情况五:监视reactive定义的响应式数据中的某一些属性
watch([()=>person.name,()=>person.age],(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
})
// 特殊情况
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:false});// 此处由于是监视reactive所定义的对象中的某个属性,所以deep配置有效

  • watch

    • 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)

    • 监视reactive定义的响应式数据中某个属性时deep配置有效

示例一:wath监听ref定义的响应式数据

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
<template>
<h2>watch监听ref定义的响应式数据</h2>
<h2>姓名:{{userName}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="userName+='!'">修改姓名</button>
<button @click="age++">修改年龄</button>
<hr>
<h2>姓名:{{user.name}}</h2>
<h2>年龄:{{user.age}}</h2>
<button @click="user.name+='!'">修改姓名</button>
<button @click="user.age++">修改年龄</button>
</template>

<script>
import {ref,watch} from 'vue';
export default {
name: "watch_component01",
setup(){
let userName=ref('张三');
let age=ref(18);
let user=ref({
name:'张三',
age:21,
})
// watch监听ref定义的单个响应式数据
watch(userName,(newValue,oldValue)=>{
console.log(`userName发生了变化,新值是:${newValue},旧值是:${oldValue}`)
});
watch(age,(newValue,oldValue)=>{
console.log(`age发生了变化,新值是:${newValue},旧值是:${oldValue}`);
});

// 如果需要监听多个ref定义的响应式数据的话,代码如下
/**
* newValue和oldValue都是数组的形式,其中数组的第n位表示监听位置的第n位
* 例如:此例子中,监听属性的第一位是userName,那位newValue和oldValue对应的第一位也是
* userName。
* 如果有立即执行,那么最开始的值为[],而不是[undefined,undefined]
*/
watch([userName,age],(newValue,oldValue)=>{
console.log('userName或age中的其中一个发生了变化,',newValue,oldValue)
})

// watch监视ref定义的响应式对象数据
watch(user.value,(newValue,oldValue)=>{
console.log('person发生了变化',newValue,oldValue)
})
watch(user,(newValue,oldValue)=>{
console.log('person发生了变化',newValue,oldValue);
},{deep:false})

return{
userName,
age,
user
}
}
}
</script>

<style scoped>

</style>

image-20240229162030615

  • 点击修改按钮

image-20240229164328671

示例二:watch监听reactive定义的响应式数据

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
<template>
<h1>watch监听reactive定义的响应式数据</h1>
<p>姓名:{{user.name}}</p>
<p>年龄:{{user.age}}</p>
<p>薪水:{{user.job.salary}}K</p>
<button @click="user.name+='!'">改变姓名</button>
<button @click="user.age++">改变年龄</button>
<button @click="user.job.salary++">改变薪水</button>
</template>

<script>
import {watch,reactive} from 'vue'
export default {
name: "watch_component02",
setup(){
let user=reactive({
name:'张三',
age:18,
job:{
salary:20
}
});

// 情况一:监听reactive定义的响应式数据,无法正确获取oldValue
/**
* 此时的newValue和oldValue都是最新的数据
* 默认强制开启深度监视,此时深度监视失效
*/
watch(user,(newValue,oldValue)=>{
console.log(newValue,oldValue);
},{deep:false});


// 情况二,监视reactive定义的响应式数据的单个属性
// watch(()=>user.name,(newValue,oldValue)=>{
// console.log('name发生了变化',newValue,oldValue);
// });
// watch(()=>user.age,(newValue,oldValue)=>{
// console.log('age发生了变化',newValue,oldValue);
// })


// 情况三:监视reactive定义的响应式数据的多个属性
/**
* newValue和oldValue都是数组的形式,其中数组的第n位表示监听位置的第n位
* 例如:此例子中,监听属性的第一位是userName,那位newValue和oldValue对应的第一位也是
* userName,
*/
// watch([()=>user.name,()=>user.age],(newValue,oldValue)=>{ // 写法一
// console.log('name或age中的某个属性发生了变化',newValue,oldValue);
// })
// watch(()=>[user.name,user.age],(newValue,oldValue)=>{ // 写法二
// console.log('name或者age中的某个属性发生了变化',newValue,oldValue)
// })

// 情况四:监视reactive定义的响应式数据的对象的某个属性,此时deep有效
/**
* 注意:此时需要区别是reactive定义的对象还是reactive定义的对象里面的某个属性
* 此时deep有效,关闭了监视
*/
// watch(()=>user.job,(newValue,oldValue)=>{
// console.log(newValue,oldValue);
// },{deep:false});
return{
user
}
}
}
</script>

<style scoped>

</style>

  • watchEffect

    • watch的套路是:既要指明监视的属性,也要指明监视的回调

    • watchEffect的套路是:不用指明监视那个属性,监视的回调中用到那个属性,那就监视那个属性

    • watchEffect有点像computed

      • 但computed注重的是计算出来的值(回调函数的返回值),所以必需要写返回值

      • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值

  • 代码

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
<template>
<h1>watchEffect监视ref和reactive定义的响应式数据</h1>
<h2>当前求和:{{sum}}</h2>
<button @click="sum++">点我加1</button>
<hr>
<h2>当前的信息:{{msg}}</h2>
<button @click="msg+='!'">修改信息</button>
<hr>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>薪资:{{person.job.j1.salary}}</h2>
<button @click="person.name+='!'">修改姓名</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.job.j1.salary++">涨薪</button>
</template>

<script>
import {ref,reactive,watchEffect} from 'vue';
export default {
name: "watch_effect_component01",
setup(){
let sum=ref(0);
let msg=ref('你好');
let person=reactive({
name:'张三',
age:18,
job:{
j1:{
salary:100,
}
}
});
/**
* 在watchEffect里面写需要监视的属性,默认会执行一次
* 如果是监视ref定义的响应式书则需要.value
* 如果是监视reactive定义的响应式数据则直接监视
*/
watchEffect(()=>{
let x1=sum.value;
let x2=person.job.j1.salary;
console.log('watchEffect所指定的回调函数执行了');
})

return{
sum,
msg,
person
}
}
}
</script>

<style scoped>

</style>

  • 点我加1和涨薪,就会触发watchEffect监控中的内容

image-20240229175711760

说明