当前位置: 首页 > news >正文

一文理解服务端渲染SSR的原理,附实战基于vite和webpack打造React和Vue的SSR开发环境

SSR和CSR

首先,我们先要了解什么是SSRCSRSSR是服务端渲染,CSR是客户端渲染,服务端渲染是指 HTTP 服务器直接根据用户的请求,获取数据,生成完整的 HTML 页面返回给客户端(浏览器)展现。相对地,HTTP 服务器根据用户请求返回对应的 JSON 数据,在 HTML 页面中通过 JavaScript 来生成最终用户看到的页面,叫做客户端渲染。比如,现在非常流行的 VueReact 框架就是客户端渲染的一个应用。在非常早期的 Web 技术中,大家还是使用 JSP这种古老的模板语法来编写前端的页面,然后直接将 JSP 文件放到服务端,在服务端填入数据并渲染出完整的页面内容,可以说那个时代的做法是天然的服务端渲染。但是随着Web2.0的到来。Web应用变得复杂了起来,相比于服务端渲染要发送完整页面,客户端渲染每次只需要请求页面上需要更新的数据,网络传输数据量小,能够显著减轻服务器压力。而且客户端渲染真正实现了前后端职责分离,后端只需关注数据,逻辑由前端的 JS 去完成,这种开发模式更加灵活,效率也更高。因此,客户端渲染渐渐成为了主流。

但客户端渲染也存在着一定的问题,例如首屏加载比较慢SEO 不太友好,因此 SSR即服务端渲染技术应运而生,它在保留 CSR 技术栈的同时,也能解决 CSR 的各种问题。在某些特定的情况下,采用服务端渲染要比客户端渲染有优势,所以服务端渲染又逐渐回归人们的视野,成为了特定场景下的一种选择。当然,SSR 中只能生成页面的内容和结构,并不能完成事件绑定,因此需要在浏览器中执行 CSR 的 JS 脚本,完成事件绑定,让页面拥有交互的能力,这个过程被称作hydrate(翻译为注水或者激活)。同时,像这样服务端渲染 + 客户端 hydrate 的应用也被称为同构应用

这里附上CSRSSR的简易流程图

看到这里相信大家应该对SSRCSR的区别有所了解了,接下来我们就自己动手搭建ReactVueSSR环境,我们分别使用webpackvite来搭建。

实战环节

使用vite打造React的SSR环境

搭建项目骨架

首先我们先使用vite初始化一个React的项目,我们这里包管理工具使用pnpm,如果没有下载pnpm,先在命令行执行npm install pnpm -g,然后我们执行pnpm create vite,输入我们的项目名,然后选择Reactts进行开发,进入项目根目录执行pnpm i安装依赖。

然后我们进入tsconfig.json里,把esModuleInterop的值改为true,让我们在项目里可以支持import path from 'path'这种写法’:

{"compilerOptions": {"target": "ESNext","useDefineForClassFields": true,"lib": ["DOM", "DOM.Iterable", "ESNext"],"allowJs": false,"skipLibCheck": true,// 可以支持import path from 'path'的写法"esModuleInterop": true,"allowSyntheticDefaultImports": true,"strict": true,"forceConsistentCasingInFileNames": true,"module": "ESNext","moduleResolution": "Node","resolveJsonModule": true,"isolatedModules": true,"noEmit": true,"jsx": "react-jsx"},"include": ["src"],"references": [{ "path": "./tsconfig.node.json" }]
} 

删除项目自带的src/main.ts,然后在 src 目录下新建entry-client.tsxentry-server.tsx两个入口文件:

entry-client.ts

// 客户端入口文件
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'ReactDOM.hydrateRoot(document.getElementById('root')!,<React.StrictMode><App data={data} /></React.StrictMode>
) 

entry-server.ts

// 导出 SSR 组件入口
import App from './App'
import './index.css'function ServerEntry(props: any) {return <App data={props.data} />
}export { ServerEntry } 

我们以 Express 框架为例来实现 Node 后端服务,后续的 SSR 逻辑会接入到这个服务中。当然我们需要安装以下的依赖:

pnpm i express -S 
pnpm i @types/express -D 

接着新建src/ssr-server/index.ts:

// 后端服务
import express from 'express';async function createServer() {const app = express();app.listen(3000, () => {console.log('Node 服务器已启动~')console.log('http://localhost:3000');});
}createServer(); 

SSR运行时的实现

SSR 作为一种特殊的后端服务,我们可以将其封装成一个中间件的形式,如以下的代码所示:

src/ssr-server/index.ts

import express, { RequestHandler, Express } from 'express'
import { ViteDevServer } from 'vite'const isProd = process.env.NODE_ENV === 'production'
const cwd = process.cwd()async function createSsrMiddleware(app: Express): Promise<RequestHandler> {let vite: ViteDevServer | null = nullif (!isProd) {vite = await (await import('vite')).createServer({root: process.cwd(),server: {middlewareMode: 'ssr',},})// 注册 Vite Middlewares// 主要用来处理客户端资源app.use(vite.middlewares)}return async (req, res, next) => {// SSR 的逻辑// 1. 加载服务端入口模块// 2. 数据预取// 3. 「核心」渲染组件 -> 字符串// 4. 拼接 HTML,返回客户端}
}async function createServer() {const app = express()// 加入 Vite SSR 中间件app.use(await createSsrMiddleware(app))app.listen(3000, () => {console.log('Node 服务器已启动~')console.log('http://localhost:3000')})
}createServer() 

Ok,那接下来我们来实现中间件内SSR的逻辑实现,首先实现第一步即加载服务端入口模块:

我们将它抽离到src/server/utils.ts里:

src/server/utils.ts

import { ViteDevServer } from 'vite'
import path from 'path'export const isProd = process.env.NODE_ENV === 'production'
export const cwd = process.cwd()async function loadSsrEntryModule(vite: ViteDevServer | null) {// 生产模式下直接 require 打包后的产物if (isProd) {const entryPath = path.join(cwd, 'dist/server/entry-server.js');return require(entryPath);} // 开发环境下通过 no-bundle 方式加载else {const entryPath = path.join(cwd, 'src/entry-server.tsx');return vite!.ssrLoadModule(entryPath);}
} 

我们补充一下中间件的逻辑:

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {// 省略前面的代码return async (req, res, next) => {const url = req.originalUrl;// 1. 服务端入口加载const { ServerEntry } = await loadSsrEntryModule(vite);// ...}
} 

接下来我们来实现服务端的数据预取操作,你可以在entry-server.tsx中添加一个简单的获取数据的函数:

export async function fetchData() {return { user: 'pujie' }
} 

然后在 SSR 中间件中完成数据预取的操作:

src/ssr-server/index.ts

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {// 省略前面的代码return async (req, res, next) => {const url = req.originalUrl;// 1. 服务端入口加载const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);// 2. 预取数据const data = await fetchData();}
} 

接着我们进入到核心的组件渲染阶段: src/ssr-server/index.ts

import { renderToString } from 'react-dom/server';
import React from 'react';async function createSsrMiddleware(app: Express): Promise<RequestHandler> {// 省略前面的代码return async (req, res, next) => {const url = req.originalUrl;// 1. 服务端入口加载const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);// 2. 预取数据const data = await fetchData();// 3. 组件渲染 -> 字符串const appHtml = renderToString(React.createElement(ServerEntry, { data }));}
} 

我们在第一步之后我们拿到了入口组件,现在可以调用React的renderToStringAPI 将组件渲染为字符串,组件的具体内容便就此生成了。 OK,目前我们已经拿到了组件的 HTML 以及预取的数据,接下来我们在根目录下的 HTML 中提供相应的插槽,方便内容的替换:

index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Vite App</title></head><body><div id="root"><!-- SSR_APP --></div><script type="module" src="/src/entry-client.tsx"></script><!-- SSR_DATA --></body>
</html> 

OK然后我们拿到html文件后,我们要将插槽占位的内容换成对应的内容,然后将html返回给客户端: src/ssr-server/index.ts

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {// 省略之前的代码return async (req, res, next) => {const url = req.originalUrl;// 省略前面的步骤// 4. 拼接完整 HTML 字符串,返回客户端const templatePath = resolveTemplatePath();let template = await fs.readFileSync(templatePath, 'utf-8');// 开发模式下需要注入 HMR、环境变量相关的代码,因此需要调用 vite.transformIndexHtmlif (!isProd && vite) {template = await vite.transformIndexHtml(url, template);}const html = template.replace('<!-- SSR_APP -->', appHtml)// 注入数据标签,用于客户端 客户端进行注水接管浏览器事件.replace('<!-- SSR_DATA -->',`<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`);res.status(200).setHeader('Content-Type', 'text/html').end(html);}
} 

补充工具函数: src/ssr-server/utils.ts

 export function resolveTemplatePath() {return isProd? path.join(cwd, 'dist/client/index.html'): path.join(cwd, 'index.html')
} 

在拼接 HTML 的逻辑中,除了添加页面的具体内容,同时我们也注入了一个挂载全局数据的script标签,这是用来干什么的呢?

SSR 的基本概念中我们就提到过,为了激活页面的交互功能,我们需要执行 CSRJavaScript 代码来进行 hydrate 操作,而客户端 hydrate 的时候需要和服务端同步预取后的数据,保证页面渲染的结果和服务端渲染一致,因此,我们刚刚注入的数据 script 标签便派上用场了。由于全局的 window 上挂载服务端预取的数据,我们可以在entry-client.tsx也就是客户端渲染入口中拿到这份数据,并进行 hydrate,如果我们要做全局状态管理也是这个思路。

entry-client.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { fetchData } from './entry-server'
import './index.css'// 跳过ts类型检测
// @ts-ignore 
const data = window.__SSR_DATA__ ?? fetchData() // 如果服务端获取数据失败我们要做客户端降级,在客户端获取数据ReactDOM.hydrateRoot(document.getElementById('root')!,<React.StrictMode><App data={data} /></React.StrictMode>
) 

我们在这里还需要处理生产环境下静态资源的处理,这里我们可以通过serve-static中间件来完成这个服务:

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {return async (req, res, next) => {try {const url = req.originalUrl;if (!matchPageUrl(url)) {// 走静态资源的处理return await next();}// SSR 的逻辑省略} catch(e: any) {vite?.ssrFixStacktrace(e);console.error(e);res.status(500).end(e.message);}}
}async function createServer() {const app = express();// 加入 Vite SSR 中间件app.use(await createSsrMiddleware(app));// 注册中间件,生产环境端处理客户端资源if (isProd) {app.use(serve(path.join(cwd, 'dist/client')))}// 省略其它代码
} 

我们还需要补充matchPageUrl这个工具函数

src/ssr-server/utils.ts

export function matchPageUrl(url: string) {if (url === '/') {return true}
} 

这样一来,我们就解决了生产环境下静态资源失效的问题。不过,一般情况下,我们会将静态资源部上传到 CDN 上,并且将 Vite 的 base 配置为域名前缀,这样我们可以通过 CDN 直接访问到静态资源,而不需要加上服务端的处理。

自定义head

SSR 的过程中,我们虽然可以在决定组件的内容,即<div id="root"></div>这个容器 div 中的内容,但对于 HTML 中head的内容我们无法根据组件的内部状态来决定,比如对于一个直播间的页面,我们需要在服务端渲染出 title 标签,title 的内容是不同主播的直播间名称,不能在代码中写死,这种情况怎么办?React其实有一个库叫react-helmet可以帮我们完成这个需求,我们下载这个库:

Ok,我们编写一下App.tsx的内容:

import logo from './assets/react.svg'
import './App.css'
import { Helmet } from 'react-helmet'export interface AppProps {data: any
}function App(props: AppProps) {const { data } = props// @ts-ignorereturn (<div className="App"><Helmet><title>{data.user}的页面</title><link rel="canonical" href="http://mysite.com/example" /></Helmet><header className="App-header"><img src={logo} className="App-logo" alt="logo" /><p>Hello Vite + React<img src="https://reactjs.org"target="_blank"rel="noopener noreferrer">Learn React</a>{' | '}<aclassName="App-link"href="https://vitejs.dev/guide/features.html"target="_blank"rel="noopener noreferrer">Vite Docs</a></p></header></div>" style="margin: auto" />
}export default App 

OK,最后我们在package.json里添加一些脚本:

 "scripts": {// 开发阶段启动 SSR 的后端服务"dev": "nodemon --watch src/ssr-server/index.ts --exec esno src/ssr-server/index.ts",// 打包客户端产物和 SSR 产物"build": "npm run build:client && npm run build:server","build:client": "vite build --outDir dist/client --ssrManifest --manifest","build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",// 生产环境预览 SSR 效果"preview": "set NODE_ENV=production && esno src/ssr-server/index.ts"}, 

nodemon大家应该比较熟悉,esno是用来执行ts文件的,类似ts-node,OK,我们安装一下依赖:

pnpm i esno nodemon -D 

然后我们试一下开发环境怎么样,执行pnpm dev,可以看到如下页面:

OK,我们再试一下生产环境,执行pnpm build然后执行pnpm preview,看到的页面应该和开发环境一样的,那我们React简易的SSR环境就搭建完成了

使用webpack打造Vue的SSR环境

其实Vue的SSR环境搭建的过程是和React差不多的,只是将资源换成webpack处理而已,首先我们先搭建项目环境:

搭建项目骨架

pnpm init -y
pnpm install webpack webpack-cli -D 

建立文件目录如下:

 ├─ package.json
├─ pnpm-lock.yaml
├─ server.js
├─ src
│├─ App.vue
│├─ entry-client.js
│├─ entry-server.js
├─ webpack.base.js
├─ webpack.client.js
└─ webpack.server.js 

由于Vue的SSR搭建和React的思路差不多,所以不把步骤拆的那么细了,不过我会给代码添加注释

我们需要分别为服务端和客户端准备入口文件: entry-server.js

import { createSSRApp } from 'vue'import App from './App.vue'export default () => {return createSSRApp(App)
} 

entry-client.js

import { createSSRApp } from 'vue'import App from './App.vue'createSSRApp(App).mount('#app') 

两者区别在于:客户端版本会立即调用 mount 将组件挂载到页面上;而服务端版本只是 export 一个创建应用的工厂函数。

编写不同环境的webpack配置文件

我们需要分别为客户端、服务端版本编写 Webpack 配置文件和一个基础的配置文件,也就是服务端和客户端都会用到的配置,即上述目录中的三个**webpack.*.js 文件**。

webpack.base.js用于客户端和服务端的公共配置:

const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')module.exports = {mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',output: {filename: '[name].[contenthash].js',path: path.join(__dirname, 'dist'),},resolve: {extensions: ['.vue', '.js'],},module: {rules: [{ test: /.vue$/, use: 'vue-loader' }],},plugins: [new VueLoaderPlugin()],
} 

webpack.client.js 用于定义构建客户端资源的配置:

// 合并webpack配置的插件
const Merge = require('webpack-merge')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { WebpackManifestPlugin } = require('webpack-manifest-plugin')
const base = require('./webpack.base')module.exports = Merge.merge(base, {entry: {// 入口指向 `entry-client.js` 文件client: path.join(__dirname, './src/entry-client'),},output: {publicPath: '/',},module: {rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader'] }],},plugins: [// 这里使用 webpack-manifest-plugin 记录产物分布情况// 方面后续在 `server.js` 中使用new WebpackManifestPlugin({ fileName: 'manifest-client.json' }),// 自动生成 HTML 文件内容new HtmlWebpackPlugin({templateContent: `
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Webpack App</title>
</head>
<body><div id="app" />
</body>
</html>`,}),],
}) 

webpack.server.js 用于定义构建服务端资源的配置:

const Merge = require('webpack-merge')
const path = require('path')
const { WebpackManifestPlugin } = require('webpack-manifest-plugin')
const base = require('./webpack.base')module.exports = Merge.merge(base, {entry: {server: path.join(__dirname, 'src/entry-server'),},target: 'node',output: {// 打包后的结果会在 node 环境使用// 因此此处将模块化语句转译为 commonjs 形式libraryTarget: 'commonjs2',},module: {rules: [{test: /\.css$/,use: [// 注意,这里用 `vue-style-loader` 而不是 `style-loader`// 因为 `vue-style-loader` 对 SSR 模式更友好'vue-style-loader',{loader: 'css-loader',options: {esModule: false,},},],},],},plugins: [// 这里使用 webpack-manifest-plugin 记录产物分布情况// 方面后续在 `server.js` 中使用new WebpackManifestPlugin({ fileName: 'manifest-server.json' }),],
}) 

然后,我们只需要调用适当命令即可分别生成客户端、服务端版本代码:

# 客户端版本:
npx webpack --config ./webpack.client.js
# 服务端版本:
npx webpack --config ./webpack.server.js 

编写SSR的服务端的服务

server.js

const express = require('express')
const path = require('path')
const { renderToString } = require('@vue/server-renderer')// 通过 manifest 文件,找到正确的产物路径
const clientManifest = require('./dist/manifest-client.json')
const serverManifest = require('./dist/manifest-server.json')const serverBundle = path.join(__dirname, './dist', serverManifest['server.js'])
// 这里就对标到 `entry-server.js` 导出的工厂函数
const createApp = require(serverBundle).defaultconst server = express()server.get('/', async (req, res) => {const app = createApp()const html = await renderToString(app)const clientBundle = clientManifest['client.js']res.send(`<!DOCTYPE html><html><head><title>Vue SSR </title></head><body><!-- 注入组件运行结果 --><div id="app">${html}</div><!-- 注入客户端代码产物路径 --><!-- 实现 Hydrate 效果 --><script src="${clientBundle}"></script></body></html>`)
})server.use(express.static('./dist'))server.listen(3000, () => {console.log('服务在3000端口运行')
}) 

OK,我们编写App.vue文件:

<template><div :class="['main', cls]"><h3>{{ message }}</h3><button @click="handleClick">Toggle</button></div>
</template><script>
import { ref, computed } from '@vue/reactivity'
export default {setup() {const isActivity = ref(false)const cls = computed(() => (isActivity.value ? 'activate' : 'deactivate'))const handleClick = () => {isActivity.value = !isActivity.value}return {isActivity,message: 'Hello World',cls,handleClick,}},
}
</script><style>
h3 {color: #42b983;
}
.main {position: fixed;top: 0;left: 0;bottom: 0;right: 0;padding: 20px 12px;transition: background 0.3s linear;
}
.activate {background: #000;
}
.deactivate {background: #fff;
}
</style> 

然后我们在package.json里添加脚本如下:

 "scripts": {"build:client": "webpack --config ./webpack.client.js","build:server": "webpack --config ./webpack.server.js","start": "nodemon server.js"}, 

Ok,然后我们执行pnpm build:clientpnpm build:server,然后执行pnpm start,项目就跑起来了,我们可以看到如下页面,点击Toggle可以切换背景:

OK,相信读了文章的同学们应该对SSR应该有了新的认识,有问题可以一起讨论哇,咱们下回再见咯。

React的SSR环境搭建代码地址

Vue的SSR环境搭建代码地址

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

相关文章:

一文理解服务端渲染SSR的原理,附实战基于vite和webpack打造React和Vue的SSR开发环境

SSR和CSR 首先&#xff0c;我们先要了解什么是SSR和CSR&#xff0c;SSR是服务端渲染&#xff0c;CSR是客户端渲染&#xff0c;服务端渲染是指 HTTP 服务器直接根据用户的请求&#xff0c;获取数据&#xff0c;生成完整的 HTML 页面返回给客户端&#xff08;浏览器&#xff09;展…...

Matlab 实用小函数汇总

文章目录Part.I 元胞相关Chap.I 创建空 char 型元胞Part.II 矩阵相关Chap.I 矩阵插入元素Part.III 字符串相关Chap.I 获取一个文件夹下所有文件的文件名的部分内容Part.IV 结构体相关Chap.I 读取结构体Chap.II 取结构体中某一字段的所有值本篇博文记录一些笔者使用 Matlab 时&a…...

Echarts 仪表盘倾斜一定角度显示,非中间对称

第024个点击查看专栏目录大多数的情况下&#xff0c;制作的仪表盘都是中规中矩&#xff0c;横向中间对称&#xff0c;但是生活中的汽车&#xff0c;摩托车等仪表盘确是要倾斜一定角度的&#xff0c;Echarts中我们就模拟一个带有倾斜角度的仪表盘。核心代码见示例源代码 文章目录…...

Vue中如何利用websocket实现实时通讯

首先我们可以先做一个简单的例子来学习一下简单的websocket模拟聊天对话的功能 原理很简单&#xff0c;有点像VUE中的EventBus&#xff0c;用emit和on传来传去 首先我们可以先去自己去用node搭建一个本地服务器 步骤如下 1.新建一个app.js&#xff0c;然后创建pagejson.js文…...

​力扣解法汇总1144. 递减元素使数组呈锯齿状

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣 描述&#xff1a; 给你一个整数数组 nums&#xff0c;每次 操作 会从中选择一个元素并 将该元素的…...

Spring彻头彻尾的讲解,按照Spring框架启动流程,逐步剖析问题,不再是大杂烩!

文章目录1. 定义Spring Bean篇1.1 定义Spring Bean的几种方式1.1.1 XML文件定义Spring Bean1.1.2 JavaConfig定义Spring Bean1.1.3 Component注解定义SpringBean1.2 装配Spring Bean的四种常用方式1.2.1 手动装配 XML文件1.2.2 自动装配 XML文件1.2.3 手动装配 JavaConfig文…...

[2]MyBatis+Spring+SpringMVC+SSM整合一套通关

二、Spring 1、Spring简介 1.1、Spring概述 官网地址&#xff1a;https://spring.io/ Spring 是最受欢迎的企业级 Java 应用程序开发框架&#xff0c;数以百万的来自世界各地的开发人员使用 Spring 框架来创建性能好、易于测试、可重用的代码。 Spring 框架是一个开源的 Jav…...

Javascript的API基本内容(三)

一、事件流 假设页面里有个div&#xff0c;当触发事件时&#xff0c;会经历两个阶段&#xff0c;分别是捕获阶段、冒泡阶段简单来说&#xff1a;捕获阶段是 从父到子 冒泡阶段是从子到父实际工作都是使用事件冒泡为主 二、页面加载事件 加载外部资源&#xff08;如图片、外联CS…...

【Python入门第十九天】Python 函数

函数是一种仅在调用时运行的代码块。 可以将数据&#xff08;称为参数&#xff09;传递到函数中。 函数可以把数据作为结果返回。 创建函数 在 Python 中&#xff0c;使用 def 关键字定义函数&#xff1a; 实例 def my_function():print("Hello from a function&quo…...

web前端性能优化

3.性能检测 当面对具体的项目实践时&#xff0c;该如何快速提升性能体验呢&#xff1f;或者说如何能够准确地定位到性能瓶颈呢&#xff1f;难道要比对着优化知识点清单&#xff0c;一项一项手动排查或完全凭借经验去处理吗&#xff1f;不&#xff0c;我们需要有一整套清晰科学…...

Telnet 基础实验2: SSH 实验

Telnet 基础实验2&#xff1a; SSH 实验 本实验只能使用 eNSP中 AR 系统的路由器做 拓扑图 SSH &#xff1a; Secure Shell 是一个网络安全协议&#xff0c;基本于 TCP 协议 22 端口传输数据&#xff0c;通过对网络数据的加密&#xff0c;使其能够在一个不安全的网络环境中&a…...

Panda Farm:首个部署在 Arbitrum 上的轻量化 GameFi 游戏

在2月16日&#xff0c;Bitget平台宣布 Launchpad 重新启动&#xff0c;并推出了重启后的首个项目 Panda Farm&#xff08;BBO&#xff09;&#xff0c;该 Launchpad 启动后得到了较高的关注。 Panda Farm 是部署在 Arbitrum 上的 GameFi应用&#xff0c;这可能首先意味着 Bitge…...

Redis实现分布式锁

1、使用背景几乎每个互联网公司中都使用了分布式部署&#xff0c;分布式服务下&#xff0c;就会遇到对同一个资源的并发访问的技术难题&#xff0c;如秒杀、下单减库存等场景。这些场景有一个共同特点就是访问量激增&#xff0c;虽然在系统设计时会通过限流、异步、排队等方式优…...

刷题小抄1-2数之和

时间复杂度和空间复杂度 对于一个算法高效性的评估,分为时间复杂度与空间复杂度两种,在算法优化到极致的情况下,只能舍弃时间来换取空间,或者舍弃空间来换取时间,故而两者可以说是互斥关系 时间复杂度衡量的是算法运行的速度,而空间复杂度衡量的是算法运行所需要的额外内存空…...

axicom的测试文档

目录&#xff09;SQLpython开放性业务题&#xff08;二选一&#xff09;完整代码SQL 问题描述 SQL&#xff0c; 请根据前一周各产品的总GMV将其分成五类&#xff1a;GMV Top 20%、20%-40%&#xff0c;40%-60%&#xff0c;60%-80%以及Bottom 20%的产品组&#xff0c;请计算这五…...

基于vue3异步组件、动态组件、vite批量导入实现路由权限动态管理(非addRoute方案)

开发后台管理系统必备的需求&#xff1a;动态菜单权限管理、或者说路由权限动态管理 原理是通过addRoute实现路由权限控制&#xff0c;一般分为两种&#xff1a; 后端生成当前用户相应的路由后由前端&#xff08;用 Vue Router 提供的API&#xff09;addRoutes 动态加载路由前…...

带中转hub的卡车无人机车辆路径问题

本文介绍了两类无人机卡车协同配送问题: 第一类是旅行商问题,也即一辆卡车拉着一架无人机服务给定的节点集合第二类是车辆路径问题,这里强制要求了一架卡车只能搭配一架无人机无人机卡车旅行商问题 符号列表: N N N:表示所有节点集合,含起始和终止节点M M M...

前端食堂技术周刊第 72 期:Signals 是前端框架的未来、Chrome Headless、ts-reset、magic-regexp、Bun 新文档

美味值&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f; 口味&#xff1a;草莓番茄 食堂技术周刊仓库地址&#xff1a;https://github.com/Geekhyt/weekly 本期摘要 Signals 是前端框架的未来Chrome Headless 进化成完全体Next.js 13.2Deno…...

mysql中用逗号隔开的字段作查询用(find_in_set的使用)

mysql中用逗号隔开的字段作查询用(find_in_set的使用) 场景说明 在工作中&#xff0c;经常会遇到一对多的关系。想要在mysql中保存这种关系&#xff0c;一般有两种方式&#xff0c;一种是建立一张中间表&#xff0c;这样一条id就会存在多条记录。或者采用第二种方式&#xff…...

Day902.Memory存储引擎 -MySQL实战

Memory存储引擎 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于Memory存储引擎的内容。 两个 group by 语句都用了 order by null&#xff0c;为什么使用内存临时表得到的语句结果里&#xff0c;0 这个值在最后一行&#xff1b; 而使用磁盘临时表得到的结果里&…...

Linux(Centos)安装RabbitMQ+延时插件+开机自启动

安装目录1&#xff1a;前言1.1 系统环境1.2&#xff1a;安装版本1.3 简介2&#xff1a;安装2.1&#xff1a;安装前准备&#xff1a;2.2&#xff1a;安装Erlang2.3&#xff1a;安装RabbitMQ2.3&#xff1a;延迟依赖插件安装1&#xff1a;前言 1.1 系统环境 操作系统版本&#…...

最近是遇到了CKPT(BLOCKED)

半夜被电话吵醒&#xff0c;数据库不可用了&#xff0c;无法交易。 远程登录查看&#xff0c;这个时候就没有所谓的安全不安全了&#xff0c;都可以远程了。 onstat - 数据库处于CKPT(REQ) CKPT&#xff1a;BLOCKED状态 onstat -l 发现所有的逻辑日志都是U------状态&#xff…...

RabbitMQ死信队列

目录 一、概念 二、出现死信的原因 三、实战 &#xff08;一&#xff09;代码架构图 &#xff08;二&#xff09;消息被拒 &#xff08;三&#xff09;消息TTL过期 &#xff08;四&#xff09;队列达到最大长度 一、概念 先从概念解释上搞清楚这个定义&#xff0c;死信&…...

Word控件Spire.Doc 【书签】教程(1):在C#/VB.NET:在 Word 中插入书签

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…...

微服务框架-学习笔记

1 微服务架构介绍 1.1 系统架构演变历史 单体架构垂直应用架构&#xff1a;按照业务线垂直划分分布式架构&#xff1a;抽出业务无关的公共模块SOA架构&#xff1a;面向服务微服务架构&#xff1a;彻底的服务化1.2 微服务架构概览 1.3 微服务架构核心要素 服务治理&#xff1…...

实验心理学笔记01:引论

原视频链接&#xff1a; https://www.bilibili.com/video/BV1Qt41137Kv 目录 一、实验心理学&#xff1a;定义、内容及简要历史回顾 二、实验心理学和普通心理学、认知心理学的区别 三、实验方法与非实验方法 四、实验范式 五、实验中的各种变量 六、The science of psy…...

预备3-如何学习编程

如何学习编程 我说说曾经学习编程踩得坑 纠结字面上的意思 如纠结一个关键词的名称如何来 为什么叫这个名称... 只是一个简单的名称,该名称代表某一想象/行为,就好比你为啥叫张三, 千万别去深究这些...做笔记的时间比敲代码的时间还多 做笔记的原因是,自己总结归纳所学的知识, …...

操作系统权限提升(十七)之绕过UAC提权-Windows令牌概述和令牌窃取攻击

系列文章 操作系统权限提升(十二)之绕过UAC提权-Windows UAC概述 操作系统权限提升(十三)之绕过UAC提权-MSF和CS绕过UAC提权 操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权 操作系统权限提升(十五)之绕过UAC提权-基于白名单DLL劫持绕过UAC提权 操作系…...

【时间之外】系统管人,能行?(冷眼旁观连载之二)

上次写了在用的工具系统和痛点&#xff0c;基本情况都交待清楚了&#xff0c;春节假期很快就过去了。这次继续按照之前观察计划&#xff0c;谈谈对这些工具使用情况的感受&#xff0c;学而时习之&#xff0c;算是抛砖引玉&#xff0c;也算是个人对这项工作的总结和体会。 目录…...

【数据结构必会基础】关于树,你所必须知道的亿些概念

目录 1.什么是树 1.1浅显的理解树 1.2 数据结构中树的概念 2.树的各种结构概念 2.1 节点的度 2.2 根节点/叶节点/分支节点 2.3 父节点/子节点 2.4祖先节点/子孙节点 2.5兄弟节点 2.6树的度 2.7节点的层次 2.8森林 3. 如何用代码表示一棵树 3.1链式结构 3.1.1 树节…...

wordpress多个主题/广州灰色优化网络公司

老刘的一半是唯物主义&#xff0c;另一半是唯心主义。 平衡与否要用唯心主义的一半来解释&#xff0c;那是一种心态。 这种心态取决于你所处的环境&#xff0c;更取决于你的欲望&#xff0c;欲望&#xff0c;很多都是来自于性格。 在“劳心者”之中&#xff0c;纯粹为生活&…...

响应式网站建设过时吗/营销策划书格式及范文

上周五新的iPhone在全世界的苹果专卖店上架了&#xff0c;不甘寂寞的华为也没闲着。就在周五当天&#xff0c;华为的“橙汁”卡车开上了伦敦的街头&#xff0c;停在了苹果专卖店的店外&#xff0c;给排队购买新iPhone的果粉们免费发放橙汁。车上标注了“Apple Free” &#xff…...

英文网站seo推广/优化大师使用心得

一. IS-IS简介 IS-IS&#xff08;Intermediate System-to-Intermediate System intra-domain routing information exchange protocol&#xff0c;中间系统到中间系统的域内路由信息交换协议&#xff09;最初是国际标准化组织&#xff08;International Organization for Stand…...

邯郸网站改版费用/长沙seo男团

在局域网中VLAN是最常见的&#xff0c;通常使用VLAN来划分不同网段&#xff0c;不同的功能区3台电脑代表3个不同网段的VLAN,接在同一台交换机上VLAN10:192.168.10.1/24 GW:192.168.10.254VLAN20:192.168.20.1/24 GW:192.168.20.254VLAN30:192.168.30.1/24 GW:192.168.30.254<…...

扁平化企业网源码win8风格精简化源码asp带后台企业网站/在线网站流量查询

1. 【Resource File 】——【添加】——【资源】 2. 选择【Version】 &#xff0c;点击新建&#xff0c;新建.rc 3. 双击*.rc, 展开如图&#xff1a; FILEVERSION为版本信息 4. FILEVERSION修改为&#xff1a;为666,0,0,1&#xff0c;重新生成DLL 查看其属性-详细信息&#…...

网站中的二维码设计/如何建一个自己的网站

Javascript是一种弱类型语言&#xff0c;JavaScript的变量类型由它的值来决定。与代数一样&#xff0c;JavaScript 变量可用于存放值(比如 x5)和表达式(比如 zxy)。JavaScript定义变量的方法&#xff1a;定义变量需要用关键字 var &#xff0c;可以在声明变量时对其赋值&#…...