nuxt 服务端渲染
nuxt 服务端渲染
1 nuxt 生命周期
1.1 服务端生命周期
middleware , plugins , validate 比较特殊,算成生命周期???
服务端生命周期中, 无法拿到window对象
nuxtServerInit()
middleware 中间件
- 全局中间件
// nuxt.config.js
// 使用
export default {serverMiddleware: [// string 形式中间件对应middleware文件夹中文件名'string',// 对象path形式,针对路由{path: '/api',handler: '../components/index.vue'}]
}
// middleware
// 定义
export default function(req, res, next) {// req http request// res http response// next 调用下一个中间件函数 next() // express 相似的99%
}
- 页面中间件
// page/index.vue
// 使用
export default {middleware: 'middleware-name'
}
plugins 插件
- 工具方法??? axios,element-ui之类的
// nuxt.config.js
// 使用
export default {plugins: ['./plugins/request.js']
}
// plugins/request.js
// 定义
// 没有固定格式~~~
validate 校验
- 动态路由校验参数
// 找不到其他使用场景
// false 将会跳转到错误页
// true 放行
return false | true
以下服务端生命周期无法拿到vue实例this对象
1.1.1 asyncData()
- 主要数据获取位置,用于初始服务端渲染的数据
- return的数据将会加载到data方法中
1.1.2 fetch()
- 执行顺序在created之后,但是在服务端执行的
- return的数据将会加载到data方法中
1.2 客户端生命周期
1.2.1 beforeCreate()
- 特殊(介于服务端和客户端之间)
- 还未实例化无法拿到data数据
1.2.2 created()
- 特殊(介于服务端和客户端之间)
1.2.3 beforeMount()
1.2.4 mounted()
- 次要数据获取位置(客户端渲染)
1.2.5 beforeUpdate()
1.2.6 updated()
1.2.7 activated 失效
- 由于是服务端渲染,所以不支持组件的keep-alive,那自然activated、deactivated这两个生命周期也没了
1.2.8 deactivated 失效
- 由于是服务端渲染,所以不支持组件的keep-alive,那自然activated、deactivated这两个生命周期也没了
2 router 路由
- 采用了约定式路由
- 以pages为根 /
- 没有vue的路由表
2.1 路由守卫
plugin 形式路由守卫
/* 全局路由守卫 */
export default ({app
}) => {/* 前置路由守卫 */app.router.beforeEach((to, form, next) => {next()})
}
/* 后置路由守卫... */
...
middleware 形式路由守卫
import Cookie from 'js-cookie'
/* 主要是判断token cookie */
export default function({route,req,res,redirect
}) {const isServer = process.serverconst isClient = process.clientif (isServer) {/* 服务端 */let cookie = getCookiesInServer(req)}if (isClient) {/* 客户端 */let cookie = getCookiesInClient('token')}
}
//获取服务端cookie
function getCookiesInServer(req) {let service_cookie = {};req && req.headers.cookie && req.headers.cookie.split(';').forEach(function(val) {let parts = val.split('=');service_cookie[parts[0].trim()] = (parts[1] || '').trim();});return service_cookie;
},
//获取客户端cookie
function getCookiesInClient(key) {return Cookie.get(key) ? Cookie.get(key) : ''
}
2.2 路由视图
<!-- 挖个坑等着填 -->
<nuxt /><!-- 这个坑... -->
<nuxt-child />
2.3 路由跳转
nuxt-link更多用法参考vue-router
<nuxt-link to="/index/:id" />
<!-- tag='li' 废弃??? -->
<nuxt-link :to="{path:'/index',params:{id:xxx}},query:{id:xxx}" active-class="active-class" />
- 编程式导航
/* 服务端 */
asyncData({redirect
}) {redirect(path)
}
/* 客户端 */
this.$router.push()
this.$router.replace()
2.4 扩展路由
2.4.1 resful 风格传参 + 扩展路由
// 引用单个扩展路由
export default {router: {// 扩展路由 path.resolve 用于路径拼接extendRoutes(routes, resolve) {routes.push({path: '/index',name: 'index',component: resolve(__dirname, 'pages/index.vue')},{// 无法采用_id_type.vue 解析id和type// 无法采用_.vue 解析id和type// 手动扩展resful风格路由path:'/test/:id/:type',name:'test',component: resolve(__dirname,'page/test.vue')})}}
}// 封装成路由表的形式
import path from 'path'// 组成完整路径 process.cws() 拿到项目运行时路径 __dirname 刷新丢失
const resolve = (path) => path.resolve(process.cws(), `./${path}`)export const $routes = [{path: '/',name: 'Home',component: resolve('pages/home/index.vue'),meta: {title: '首页',icon: 'icon-home'}},{path: '/dashboard',component: resolve('pages/dashboard/index.vue'),meta: {title: '控制台',icon: 'icon-dashboard'},children: [{path: '',name: 'dashboard-cloud',component: resolve('pages/dashboard/cloud/index.vue'),meta: {title: 'xxx',icon: 'icon-cloud'}}]}
]
const extendRoutes = (routes) => {routes.length = 0 // 清除 nuxt 自己生成的路由,这里不要用 空数组 赋值routes.push(...$routes)
}
export default {base: '/',extendRoutes
}
2.5 动态路由
定义规则
- 文件嵌套
- 命名规则
- _id.vue(传递单个参数)
- _v.vue(传递多个参数,无限匹配)
跳转规则
- 跳转位置/text/1
- 当text目录中存在_id.vue 的时候跳转_id.vue,不存在直接not found
- 跳转位置/text
- 当text目录中存在index.vue 的时候跳转index.vue,不存在直接跳转_id.vue(没有validate)
- 跳转位置/text/1
3 状态管理
- 和 vuex 相似度90%, 没有 new Store() 的书写形式(不创建index.js 文件的情况下)
- 以文件形式来划分模块(不创建index.js 文件的情况下)
/* 状态 */
export const state = {text: ''
}export const getter = {getText(state) {return state.text}
}/* 同步方法 */
export const mutations = {add(state, text) {// state 存储对象// 传入参数state.text = text}
}/* 异步方法 */
export const action = {async getMsg() {return new Promise((res, rej) => {})}
}
- 以引入 index.js 实例Vuex. Store()对象的形式参考官网
- vuex入门到入坟
4 网络请求
4.1 采用集成的 @nuxtjs/axios
- 引入插件
npm i @nuxtjs/axios @nuxtjs/proxy
nuxt 集成 - 在nuxt.config.js中进行配置
export default {modules: {'@nuxtjs/axios',},plugins: ['./plugins/request.js'],axios: {// 开启代理proxy: true,// 加个请求前缀prefix: '/api',// 是否需要跨域凭证credentials: true},// 代理配置proxy: {'/api': {target: 'http://127.0.0.1:3003',pathRewrite: {'^/api': '/',changOrigin: treu}}}
}
- 请求守卫配置
// plugins/request.jsimport {Message
} from "element-ui";import { Demo } from '../api/api-demo.js'export default ({redirect,inject,$axios
}) => {$axios.onRequest(config => {console.log('test onRequest', config)// 向请求头中塞入 tokenconfig.headers.token = 'xxx'// 向参数中塞入 tokenlet data = {}if (config.method.toUpperCase() === 'GET') {data = config.params || {}data.token = 'sss'config.params = data} else {// POST 需要塞入 datadata = config.data || {}data.token = 'sss2'config.data = data}console.log('test onRequest2', config)});$axios.onResponse(res => {// 返回数据逻辑处理console.log('test onResponse', res)if (res.data.code === 1) {// 重定向到 login 页redirect('/login')}});$axios.onError(error => {Message({// 饿了么的消息弹窗组件,类似toastshowClose: true,message: error,type: "error.data.error.message"});console.log('Making request to ' + error.response.config.url)switch (error.response.status) {case 403:// 重定向到 403 页redirect('/error/403')break;case 404:// 重定向到 404 页redirect('/error/404')break;case 500:// 重定向到 500 页redirect('/error/500')break;default:break;}})const Api = {}// 模块化 apiApi.demo = new Demo($axios)// 服务端挂载到app上app.api = Api// 调用方式/*const { getList } = app.api.demo*/// 客户端挂载到this上,通过inject进行暴露inject('api',Api)// 调用方式/*const { getList } = this.$api.demo*/};
- 模块化封装请求
// api-demo.js
export class Demo(){constructor(request){this.request = request}getList = (params) => {return this.request({method:"get",url:'/demo/list',params})}addUser = (data) => {return this.request({method:"post",url:'/demo/addUser',data})}
}
4.2 采用非集成的 axios
npm i axios -d
进行安装- 进行配置
export default = {// 代理配置proxyTable: {'/api': {target: "http://192.168.1.37:5000", //测试服务器// target: "http://192.168.x.xxx:8008",//xxxchangeOrigin: true,pathRewrite: {"^/api": ""}}},
}
- 进行守卫配置(关键点在于创建axios.create(), 防止客户端,服务端同时发送多次发送请求???)
/*** 封装Axios* 处理请求、响应错误信息*/
import {Message
} from 'element-ui' //引用饿了么UI消息组件
import axios from 'axios' //引用axios// create an axios instance
const service = axios.create({// 所有异步请求都加上/api,nginx转发到后端SpringbootbaseURL: '/api/',withCredentials: true, // send cookies when cross-domain requeststimeout: 5000 // request timeout
})// request interceptor
service.interceptors.request.use(config => {// do something before request is sent// config.headers['-Token'] = getToken()// 必须返回config,否则请求将出现错误return config},error => {// do something with request errorconsole.log(error) // for debugreturn Promise.reject(error)}
)// response interceptor
service.interceptors.response.use(/*** If you want to get http information such as headers or status* Please return response => response*//*** Determine the request status by custom code* Here is just an example* You can also judge the status by HTTP Status Code*/response => {const res = response.data //res is my own dataif (res.code === 20000) {// do somethings when response success// Message({// message: res.message || '操作成功',// type: 'success',// duration: 1 * 1000// })return res} else {// if the custom code is not 200000, it is judged as an error.Message({message: res.msg || 'Error',type: 'error',duration: 2 * 1000})return Promise.reject(new Error(res.msg || 'Error'))}},error => {console.log('err' + error) // for debugMessage({message: error.message,type: 'error',duration: 5 * 1000})return Promise.reject(error)}
)export default service //导出封装后的axios
- 进行模块化封装请求
import request from './plugins/request'
export function getMsg(params) {return request({methods: 'get',url: url,params})
}
5 错误页定制
- layouts下新建error.vue 文件进行定制
<template><div>{{error.statusCode?'请求系列错误(404页面找不到除外)':'其他错误'}}</div>
</template>
<script>export default {/* 接收 error */props: ['error']/* error:{statusCode:404,message:'Page not found'} */}
</script>
注意点
- 在发送请求失败的时候 采用promise形式不会出现问题
- 采用 async await 进行解构的时候,同步错误将会强制跳转到错误页
- 解决方案 try catch进行错误捕获,但是不要抛出错误,抛出错误还是会强制跳转错误页
- 坑点 容错率低
6 layouts 布局
nuxt.config.js
同级目录layouts
- 默认布局
layouts/default.vue
<template><nuxt />
</template>
6.1 定制错误页
layouts/error.vue
<template><div>自定义error-page<div>{{error.statusCode}}</div></div>
</template>
<script>props:['error']
</script>
<style>
</style>
- pages中的文件
<!-- pages/index.vue -->
<template></template>
<script>export default {// 定义了 layout 将采用auth layout中的auth模板layout: 'auth'}
</script>
7 app.html
根目录下新建app.html
<!DOCTYPE html>
<html {{ HTML_ATTRS }}><head {{ HEAD_ATTRS }}>{{ HEAD }}</head><body {{ BODY_ATTRS }}>{{ APP }}</body>
</html>
HTML_ATTRS
,HEAD_ATTRS
,HEAD
,BODY_ATTRS
接收nuxt.config.js
的配置
8 过度动画
- pages中的vue文件中没有加入transition 将会采用默认的过度
- 使用方式1
<!-- 将采用 page-x-x的过度形式 -->
<transition name="page" mode="out-in"><nuxt />
</transition>
- 使用方式2
export default {transition: {name: 'page',mode: 'out-in'}
}
- 使用方式3 函数形式
export default {transition(to, from) {/* 路由守卫的 to from 没有next *//* 返回字符串作为过度动画名称 *//* 定制过度动画??? */}
}
- 更多参考
.page-enter-active,
.page-leave-active {transition: opacity 0.5s;
}.page-enter,
.page-leave-to {opacity: 0;
}.layout-enter-active,
.layout-leave-active {transition: opacity 0.5s;
}.layout-enter,
.layout-leave-to {opacity: 0;
}.slide-bottom-enter-active,
.slide-bottom-leave-active {transition: opacity 0.25s ease-in-out, transform 0.25s ease-in-out;
}.slide-bottom-enter,
.slide-bottom-leave-to {opacity: 0;transform: translate3d(0, 15px, 0);
}.bounce-enter-active {transform-origin: top;animation: bounce-in 0.8s;
}.bounce-leave-active {transform-origin: top;animation: bounce-out 0.5s;
}@keyframes bounce-in {0% {transform: scale(0);}50% {transform: scale(1.25);}100% {transform: scale(1);}
}@keyframes bounce-out {0% {transform: scale(1);}50% {transform: scale(1.25);}100% {transform: scale(0);}
}
9 服务端hooks参数
asyncData({isDev,route,store,env,params,query,req,res,redirect,error
}) {}
- app, // vue 实例???
- isDev, // 开发模式???
- route, // 当前路由信息
- store, // 状态对象
- env, // 环境变量
- params, // 路由参数
- query, // 路由参数
- req, // 响应对象
- res, // 请求对象
- redirect, // 重定向
- error // 错误对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-papmYRoy-1646807654383)(https://zh.nuxtjs.org/docs/2.x/context.svg)]
10 优化
- 防抖节流
- 文件压缩
- npm i compression-webpack-plugin@5.0.0(注意版本问题)
optimization: {splitChunks: {minSize: 10000,maxSize: 250000}
},
build:{plugins: [new CompressionPlugin({test: /\.js$|\.html$|\.css/, // 匹配文件名threshold: 10240, // 对超过10kb的数据进行压缩deleteOriginalAssets: false // 是否删除原文件})],
}
- element-ui按需导入
// nuxt.config.js
plugins:[{src: "./plugins/element.js",ssr: false},
]
build:{// 配置babel}
- 异常处理错误页
- 取消css ,js预加载
// nuxt.config.js
render: {resourceHints: false,
},
- 将css打包成文件进行引入
// nuxt.config.js
build:{extractCSS: { allChunks: true },
}
- 开启懒加载
build:{analyze: true
}
使用静态文件
- 根目录建立
static
目录 - 开启配置
// nuxt.config.js target: 'static',
- 根目录建立
seo优化配置header
- title
- description
- keyword
// nuxt.config.jshead: {title: '',htmlAttrs: {lang: 'zh'},meta: [{ charset: 'utf-8' },{ name: 'viewport', content: 'width=device-width, initial-scale=1' },{ hid: 'description', name: 'description', content: '' },{ name: 'keyword', content: '' },{ name: 'format-detection', content: 'telephone=no' }],link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]},
9.1坑点
// nuxt.config.js// 此项配置为 generate 生成静态网站时使用,ssr 谨慎使用,// 打包时将会出现错误 target:'static'
11 打包部署
- 检查哪些插件是在生产环境中需要的,不要装在开发环境当中
npm run build
进行项目打包
Asset | Size | Chunks | Chunk Names |
---|---|---|---|
components/nuxt-logo.js | 5.89 KiB | 1 [emitted] | components/nuxt-logo |
components/tutorial.js | 10.9 KiB | 2 [emitted] | components/tutorial |
pages/aesthetic.js | 1.2 KiB | 3 [emitted] | pages/aesthetic |
pages/contact.js | 1.77 KiB | 4 [emitted] | pages/contact |
pages/dynamic.js | 1.18 KiB | 5 [emitted] | pages/dynamic |
pages/index.js | 21.4 KiB | 6, 2 [emitted] | pages/index |
pages/match.js | 1.9 KiB | 7 [emitted] | pages/match |
pages/member.js | 1.76 KiB | 8 [emitted] | pages/member |
pages/survey.js | 1.76 KiB | 9 [emitted] | pages/survey |
pages/test/__.js | 1.87 KiB | 10 [emitted] | pages/test/__ |
pages/test/_id.js | 2.18 KiB | 11 [emitted] | pages/test/_id |
pages/test/index.js | 2 KiB | 12 [emitted] | pages/test/index |
server.js | 98.5 KiB | 0 [emitted] | app |
server.manifest.json | 1.25 KiB | [emitted] | — |
以上为打包之后的结果
当前文件夹
name |
---|
.nuxt |
api |
components |
layouts |
node_modules |
pages |
plugins |
static |
store |
.editorconfig |
.gitignore |
nuxt.config.js |
package-lock.json |
package.json |
-README.md |
- 将
.nuxt
,static
,nuxt.config.js
,package.json
, 将这四个文件移动到对应的服务器目录中 - 运行
npm i production
进行开发环境依赖安装 - 启动项目
npm run start
pm2 管理项目
- 全局安装pm2
npm i pm2 -g
- linux 环境启动项目
pm2 start npm --name 'project' -- run start
- 全局安装pm2
配置示例
// nuxt.config.js /** @Author: your name* @Date: 2021-08-31 09:08:37* @LastEditTime: 2021-10-27 11:03:01* @LastEditors: Please set LastEditors* @Description: In User Settings Edit* @FilePath: \art-society\nuxt.config.js*/const CompressionPlugin = require('compression-webpack-plugin'); export default {// Global page headers: https://go.nuxtjs.dev/config-headhead: {title: '',htmlAttrs: {lang: 'zh'},meta: [{ charset: 'utf-8' },{ name: 'viewport', content: 'width=device-width, initial-scale=1' },{ hid: 'description', name: 'description', content: '' },{ name: 'keyword', content: '' },{ name: 'format-detection', content: 'telephone=no' }],link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]},// Global CSS: https://go.nuxtjs.dev/config-csscss: ['./assets/css/quill.bubble.css','./assets/css/quill.core.css','./assets/css/quill.snow.css','./static/css/normal.css',// 'element-ui/lib/theme-chalk/index.css',],// Plugins to run before rendering page: https://go.nuxtjs.dev/config-pluginsplugins: [{src: "./plugins/router.js",ssr: true},{// 调试线条,开发模式下使用src: "./plugins/debug.js",ssr: false},{src: "./plugins/element.js",ssr: true},// api 部分"./plugins/axios.js",],// Auto import components: https://go.nuxtjs.dev/config-componentscomponents: true,// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modulesbuildModules: [],// Modules: https://go.nuxtjs.dev/config-modules// nuxt axiosmodules: ['@nuxtjs/style-resources','@nuxtjs/proxy','@nuxtjs/axios'],// 引用lessstyleResources: {less: './assets/css/*.less'},router: {// 扩展路由extendRoutes(routes, resolve) {routes.push({path: '/index',name: 'index',component: resolve(__dirname, 'pages/index.vue')},{path: '/info/info-detail/:id/:type',name: 'InfoDetail',component: resolve(__dirname, 'pages/info/info-detail/_id_type.vue')}// {// path: '/',// redirect: '/index',// component: resolve(__dirname, 'pages/index.vue')// })}},axios: {proxy: true, // 表示开启代理prefix: '/api', // 表示给请求url加个前缀 /apicredentials: true // 表示跨域请求时是否需要使用凭证},// 代理配置proxy: {// '/api': {// target: "http://192.168.1.39:8086/",//测试服务器// changeOrigin: true,// pathRewrite: {// "^/api": ""// }// },},server: {port: 3001, // default: 3000host: '0.0.0.0', // default: localhost},render: {resourceHints: false, // 禁用预加载},// target:'static',// Build Configuration: https://go.nuxtjs.dev/config-buildbuild: {/*** You can extend webpack config here*/extend(config, ctx) {},// vendor: ['axios'], // 为防止重复打包extractCSS: { allChunks: true }, // css 作为外部文件引入optimization: {splitChunks: {minSize: 10000,maxSize: 250000}},// analyze: true, // 懒加载plugins: [new CompressionPlugin({test: /\.js$|\.html$|\.css/, // 匹配文件名threshold: 10240, // 对超过10kb的数据进行压缩deleteOriginalAssets: false // 是否删除原文件})],babel: {"plugins": [[// 配置element 按需导入"component",{"libraryName": "element-ui","styleLibraryName": "theme-chalk"}]]}}, }
nuxt 服务端渲染相关推荐
- nuxt服务端渲染技术
第5章 网站前台-活动与招聘 学习目标: 掌握NUXT框架的基本使用方法 完成十次方网站前台的搭建 完成十次方网站前台活动模块的功能 完成十次方网站前台招聘模块的功能 1 服务端渲染技术NUXT 1. ...
- Vue、Nuxt服务端渲染,NodeJS全栈项目,面试小白的博客系统~~
Holle,大家好,我是李白!! 一时兴起的开源项目,到这儿就告一段落了. 这是一个入门全栈之路的小项目,从设计.前端.后端.服务端,一路狂飙的学习,发量正在欣喜若狂~~ 接触过WordPress,H ...
- nuxt2,服务端渲染应用框架, 从创建开发到部署上线
文章目录 前言 一.创建一个nuxt项目 二.目录解读 三.新建页面,路由跳转 四.组件的使用 五.插件的使用 六.异步数据和代理,nuxt中使用axios和proxy代理 七.nuxt Seo优化 ...
- Node项目部署到阿里云服务器(ECS),以Nuxt.js服务端渲染项目为例
1.前言 最近打算业余时间搭个网站,选择的技术栈为node+mongodb+Nuxt.js(基于vue,用于创建服务端渲染 (SSR) 应用),以下不会教科书式讲解,只是提供整体思路.参考资料以及关键 ...
- Nuxt --- 也来说说vue服务端渲染
Nuxt 随着现在vue和react的流行,许多网站都做成了SPA,确实提升了用户体验,但SPA也有两个弱点,就是SEO和首屏渲染速度.为了解决单页应用的痛点,基于vue和react的服务端渲染应运而 ...
- 基于vue的nuxt框架cnode社区服务端渲染
nuxt-cnode 基于vue的nuxt框架仿的cnode社区服务端渲染,主要是为了seo优化以及首屏加载速度 线上地址 http://nuxt-cnode.foreversnsd.cn githu ...
- js 操作vuex数据_请教个有关 Vue.js 使用 Nuxt.js 服务端渲染,使用 Vuex 取数据的时候报错...
查过资料没有什么结果,首先怀疑的是 SSR 的问题,但是简单的测试感觉不是 SSR 的问题.没有找到原因,希望在这里能得到解惑! 使用 Nuxt.js 做服务端渲染,前后端分离,Token 存储在 l ...
- Vue 服务端渲染(SSR)、Nuxt.js - 从入门到实践
前言 10月初有幸接到公司官网改版需求,要求采用服务端渲染模式对原网站进行seo优化. 由于团队一直使用的vue技术栈,所以我第一时间想到的就是采用vue 服务端渲染(SSR)来实现该需求,即能减少团 ...
- SSR服务端渲染(nuxt重构项目)
SSR服务端渲染(nuxt重构项目) 目的:优化SEO,提高网站权重 ,页面静态化,强化搜索引擎,提高首屏渲染速度 参考文档:https://zh.nuxtjs.org/guide/installat ...
最新文章
- Outlook新邮件要双击打开后才显示为已读
- 单元格内多个姓名拆分成一列_excel单元格拆分拆分同一单元格中的姓名,原来这么简单啊!...
- python私人兼职_python能做什么副业#下班后有哪些兼职副业
- 怎么计算网站高峰期并发量和所需的带宽?
- 千亿级别数字的人民币读法
- ADBB的完整形式是什么?
- iPhone 13系列上线1小时 京东预约人数破20万
- 【LeetCode】汉明距离(Hamming Distance)
- 机器学习基石 作业四
- 执行Jar包报错FileNotFoundException: /home/data/t.txt (Too many open files)以及Cannot run program “/bin/bash
- 使用Mono.cecil修改Unity游戏内存
- 如何用AI快速绘制大鼠模型及相关技术路线图,超详细教程!
- 邝子平:vc兼做pe?
- 8乘8led点阵显示数字_8乘以8点阵显示依次从左往右全部点亮,有老哥有51编程语言吗?...
- 又双叕来分享实用的 好用的 方便的 网页转换器了
- SQL server 实验五 (sql 查询语句)
- 新型勒索软件Phobos利用弱安全性***目标
- 【MFC】学习笔记:文件操作类——CFile
- 【java数据类型】
- STM32的串口硬件流控(RS232/RS485)