最近研究socket, 所以就顺便看了一下vite源码, vite的热更新就是根据socket实现的, 所以正好记录一下.

前端任何脚手架的入口,肯定是在package.json文件中,当我们输入script命令时, 会经历什么样的步骤呢? 接下来我们一起来探索一下~~~

入口-package.json

看下面就是一个普通前端vite脚手架启动的服务

当我们在终端输入npm run dev时, 会如何调用vite进行项目的启动呢???

当我们输入npm run dev时, 当然我们就相当于执行vite --mode dev

命令

然后就会区node-module文件夹中的.bin文件夹中找, 我们能够找到两个关于vite命令的文件

这两个不同的命令,表示在不同的操作系统,调用不同的命令执行

.cmd为后缀名是在window操作系统下执行的命令, 而没有后缀名的,则是在linux系统里面执行的

.cmd

    @IF EXIST "%~dp0\node.exe" ("%~dp0\node.exe"  "%~dp0..\vite\bin\vite.js" %*) ELSE (@SETLOCAL@SET PATHEXT=%PATHEXT:;.JS;=;%node  "%~dp0..\vite\bin\vite.js" %*)

.cmd文件是Windows系统下的批处理文件,用于执行命令行命令的集合。.cmd文件的主要特点是:1. 只能运行在Windows系统下。
2. 以文本格式保存,可以用记事本编辑。

  1. 扩展名为.cmd。
  2. 支持Windows命令行命令,如dir、copy、del等,也支持if语法、for循环等逻辑控制语句。
  3. 需要管理员权限才能执行某些命令。
  4. 在文件开头指定编码格式,如@echo off和chcp 65001等,否则会出现中文乱码。

.sh

这个看不到后缀名的是sh文件

以#!/bin/sh开头

        #!/bin/shbasedir=$(dirname "$(echo "$0" | sed -e 's,\,/,g')")case `uname` in*CYGWIN*) basedir=`cygpath -w "$basedir"`;;esacif [ -x "$basedir/node" ]; then"$basedir/node"  "$basedir/../vite/bin/vite.js" "$@"ret=$?else node  "$basedir/../vite/bin/vite.js" "$@"ret=$?fiexit $ret

是Linux/Unix Shell脚本的标记,用于声明脚本的shell类型和执行路径。当系统遇到以#!/开头的脚本时,会提取脚本第一行的信息来确定执行该脚本的shell环境与路径。例如,以#!/bin/sh开头的脚本会使用/bin/sh路径下的shell来执行脚本。
常见的shell类型有:

  1. /bin/sh:指向系统默认的shell,通常是bash。

  2. /bin/bash:直接指定bash shell来执行脚本。

  3. /usr/bin/perl:使用perl来执行脚本。

  4. /usr/bin/python:使用python来执行脚本。

通过#!/bin/sh指定要使用系统默认shell(通常是bash)来执行该脚本。

vite源码目录

可以从上面的命令文件中,找到, 其实最后就是执行了/…/vite/bin/vite.js文件

这还是相对路径指定,就是指定在node-modules文件夹下的vite文件

展开之后就是这样

/bin/vite.js

vite.js其实最重要的一句就是start函数

function start() {require('../dist/node/cli')
}

inspector

这里可以讲一下inspector.Session

inspector.Session是Chrome DevTools中的API,用于与Chrome浏览器建立调试会话,以调试和分析页面。
使用inspector.Session API可以:

  1. 与目标页面建立调试连接,随时启动或停止调试。
  2. 收集页面的事件、网络请求、控制台输出等信息。
  3. 设置断点和黑箱断点,调试运行中的JavaScript代码。
  4. 执行运行时命令,如:清理缓存、重新加载页面等。
  5. 分析DOM、CSS、内存等,检查页面的布局、样式和性能问题。

不过可以看到这是node环境, 所以没有直接使用inspector对象, 而是用require引入

process.argv.splice(profileIndex, 1)const next = process.argv[profileIndex]if (next && !next.startsWith('-')) {process.argv.splice(profileIndex, 1)}const inspector = require('inspector')const session = (global.__vite_profile_session = new inspector.Session())session.connect()session.post('Profiler.enable', () => {session.post('Profiler.start', start)})

这部分是什么功能呢?

在启动项目时添加–profile,可以打开Chrome的"性能"面板,用于分析项目在运行时的CPU/内存使用情况,找出潜在的性能瓶颈。例如,在一个Vue项目中,可以这样启动:

npm run serve --profile

这会在Chrome中打开"性能"面板,并开始记录项目的性能数据。
在面板中,你可以看到项目在运行时:

  1. CPU的使用占比。可以找出消耗CPU最多的文件/函数。
  2. 内存的增长曲线。查看内存泄露或非常耗内存的组件。
  3. 事件的触发情况。如鼠标点击、页面滚动等事件的频率。
  4. 帧率的变化。帧率过低会造成卡顿,需要优化。
  5. 页面加载与渲染的时间轴。分析页面加载流程与瓶颈。
  6. 网络请求的数量和耗时。
  7. 网络请求的数量和耗时。这些可以缩短加载时间以获得更快的体验。

/node/cli- vite命令

这个文件记录了vite有哪些命令

    const cli = cac('vite') // Command And Conquer 是一个用于构建 CLI 应用程序的 JavaScript 库。

比如执行了vite dev 或者vite serve, 就会去执行action里面的逻辑

执行了vite build, 就会执行下面的action逻辑

vite中就只有dev和build这两个命令最重要

接下来介绍一下各个命令

build: 打包

dev: 运行

preview: 预览生产环境构建结果

optimize: 用于对Vite项目进行生产环境构建与优化

version: 查看当前项目中使用的Vite版本

help: 查看Vite CLI提供的所有命令与选项的帮助信息

parse: 解析Vite项目中的import语句与别名,获得其最终解析结果

dev

action里面就是创建一个serve,然后开始监听

server中处理vite.config.ts配置, 处理httpsConfig, 处理ChokidarOptions

这里介绍一下Chokidar

chokidar是一个用于Node.js的文件系统监视器,可以监听文件和文件夹的变化,并执行相应的回调函数。它支持跨平台运行,并可以监视新增、修改、删除、移动等文件系统操作。chokidar还支持通过正则表达式或glob模式对指定的文件进行过滤,并且支持批量处理多个文件。由于其功能强大和易于使用,chokidar已经成为Node.js生态系统中最受欢迎的文件系统监视器之一。

后面就是这个监听文件是否变化, 然后执行HMR(hot modules replacement)更新的

当然,这个只能监听本地文件, 所以它监听的参数只能是相对或绝对路径, 不能监控远程文件的变化

启动一个httpServer, 这个服务是node端处理文件是否发生变化, 以及发生变化之后去处理

resolveHttpServer

创建一个http服务直接使用node自带的http模块建立

函数具体源代码如下:

    export async function resolveHttpServer({ proxy }: CommonServerOptions,app: Connect.Server,httpsOptions?: HttpsServerOptions,): Promise<HttpServer> {if (!httpsOptions) {const { createServer } = await import('node:http')return createServer(app)}// #484 fallback to http1 when proxy is needed.if (proxy) {const { createServer } = await import('node:https')return createServer(httpsOptions, app)} else {const { createSecureServer } = await import('node:http2')return createSecureServer({// Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM// errors on large numbers of requestsmaxSessionMemory: 1000,...httpsOptions,allowHTTP1: true,},// @ts-expect-error TODO: is this correct?app,) as unknown as HttpServer}}

启动一个http服务之后就是在node端建立websocket

server-createSocket

可以看到socekt经过封装之后, 返回了listen, on, off, send , close方法和clients属性

后面node端发送socket信息, 就是使用send方法

函数具体源代码如下:

export function createWebSocketServer(server: Server | null,config: ResolvedConfig,httpsOptions?: HttpsServerOptions,): WebSocketServer {let wss: WebSocketServerRawlet wsHttpServer: Server | undefined = undefinedconst hmr = isObject(config.server.hmr) && config.server.hmrconst hmrServer = hmr && hmr.serverconst hmrPort = hmr && hmr.port// TODO: the main server port may not have been chosen yet as it may use the next availableconst portsAreCompatible = !hmrPort || hmrPort === config.server.portconst wsServer = hmrServer || (portsAreCompatible && server)const customListeners = new Map<string, Set<WebSocketCustomListener<any>>>()const clientsMap = new WeakMap<WebSocketRaw, WebSocketClient>()const port = hmrPort || 24678const host = (hmr && hmr.host) || undefinedif (wsServer) {wss = new WebSocketServerRaw({ noServer: true })wsServer.on('upgrade', (req, socket, head) => {if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {wss.handleUpgrade(req, socket as Socket, head, (ws) => {wss.emit('connection', ws, req)})}})} else {// http server request handler keeps the same with// https://github.com/websockets/ws/blob/45e17acea791d865df6b255a55182e9c42e5877a/lib/websocket-server.js#L88-L96const route = ((_, res) => {const statusCode = 426const body = STATUS_CODES[statusCode]if (!body)throw new Error(`No body text found for the ${statusCode} status code`)res.writeHead(statusCode, {'Content-Length': body.length,'Content-Type': 'text/plain',})res.end(body)}) as Parameters<typeof createHttpServer>[1]if (httpsOptions) {wsHttpServer = createHttpsServer(httpsOptions, route)} else {wsHttpServer = createHttpServer(route)}// vite dev server in middleware mode// need to call ws listen manuallywss = new WebSocketServerRaw({ server: wsHttpServer })}wss.on('connection', (socket) => {socket.on('message', (raw) => {if (!customListeners.size) returnlet parsed: anytry {parsed = JSON.parse(String(raw))} catch {}if (!parsed || parsed.type !== 'custom' || !parsed.event) returnconst listeners = customListeners.get(parsed.event)if (!listeners?.size) returnconst client = getSocketClient(socket)listeners.forEach((listener) => listener(parsed.data, client))})socket.on('error', (err) => {// config.logger.error(`${colors.red(`ws error:`)}\n${err.stack}`, {//   timestamp: true,//   error: err,// })console.error(`ws error:`)})socket.send(JSON.stringify({ type: 'connected' }))if (bufferedError) {socket.send(JSON.stringify(bufferedError))bufferedError = null}})wss.on('error', (e: Error & { code: string }) => {if (e.code === 'EADDRINUSE') {config.logger.error(colors.red(`WebSocket server error: Port is already in use`),{ error: e },)} else {config.logger.error(colors.red(`WebSocket server error:\n${e.stack || e.message}`),{ error: e },)}})// Provide a wrapper to the ws client so we can send messages in JSON format// To be consistent with server.ws.sendfunction getSocketClient(socket: WebSocketRaw) {if (!clientsMap.has(socket)) {clientsMap.set(socket, {send: (...args) => {let payload: HMRPayloadif (typeof args[0] === 'string') {payload = {type: 'custom',event: args[0],data: args[1],}} else {payload = args[0]}socket.send(JSON.stringify(payload))},socket,})}return clientsMap.get(socket)!}// On page reloads, if a file fails to compile and returns 500, the server// sends the error payload before the client connection is established.// If we have no open clients, buffer the error and send it to the next// connected client.let bufferedError: ErrorPayload | null = nullreturn {listen: () => {wsHttpServer?.listen(port, host)},on: ((event: string, fn: () => void) => {if (wsServerEvents.includes(event)) wss.on(event, fn)else {if (!customListeners.has(event)) {customListeners.set(event, new Set())}customListeners.get(event)!.add(fn)}}) as WebSocketServer['on'],off: ((event: string, fn: () => void) => {if (wsServerEvents.includes(event)) {wss.off(event, fn)} else {customListeners.get(event)?.delete(fn)}}) as WebSocketServer['off'],get clients() {return new Set(Array.from(wss.clients).map(getSocketClient))},send(...args: any[]) {let payload: HMRPayloadif (typeof args[0] === 'string') {payload = {type: 'custom',event: args[0],data: args[1],}} else {payload = args[0]}if (payload.type === 'error' && !wss.clients.size) {bufferedError = payloadreturn}const stringified = JSON.stringify(payload)wss.clients.forEach((client) => {// readyState 1 means the connection is openif (client.readyState === 1) {client.send(stringified)}})},close() {return new Promise((resolve, reject) => {wss.clients.forEach((client) => {client.terminate()})wss.close((err) => {if (err) {reject(err)} else {if (wsHttpServer) {wsHttpServer.close((err) => {if (err) {reject(err)} else {resolve()}})} else {resolve()}}})})},}}

moduleGraph: 将所有文件的保存在这里

然后就是使用chokidar监听文件的change, add, unlink

当监听到文件change, 就触发onHMRUpdate

接下来就到了处理热更新handleHMRUpdate

handleHMRUpdate

handleHMRUpdate方法中, 首先处理了如果是配置文件发生改变

如果是.env环境变量改变, 如果是依赖发生改变

就需要重启服务server.restart()

如果仅仅只有客户端, 没有node端, 也就是说不在开发环境, 是不需要热更新的

(仅限开发)客户端本身不能热更新。

这个时候socket只会发送消息,让客户端重新加载

接着处理html文件, html文件是不支持热更新的

所以如果是html文件发生改变, 也需要重新加载页面

如果以上情况都不是就进行更新模块updateModules

函数具体源代码如下:

    export async function handleHMRUpdate(file: string,server: ViteDevServer,configOnly: boolean,): Promise<void> {const { ws, config, moduleGraph } = serverconst shortFile = getShortName(file, config.root)const fileName = path.basename(file)const isConfig = file === config.configFileconst isConfigDependency = config.configFileDependencies.some((name) => file === name,)const isEnv =config.inlineConfig.envFile !== false &&(fileName === '.env' || fileName.startsWith('.env.'))if (isConfig || isConfigDependency || isEnv) {// auto restart serverdebugHmr?.(`[config change] ${colors.dim(shortFile)}`)config.logger.info(colors.green(`${path.relative(process.cwd(), file)} changed, restarting server...`,),{ clear: true, timestamp: true },)try {await server.restart()} catch (e) {config.logger.error(colors.red(e))}return}if (configOnly) {return}debugHmr?.(`[file change] ${colors.dim(shortFile)}`)// (dev only) the client itself cannot be hot updated.if (file.startsWith(normalizedClientDir)) {ws.send({type: 'full-reload',path: '*',})return}const mods = moduleGraph.getModulesByFile(file)// check if any plugin wants to perform custom HMR handlingconst timestamp = Date.now()const hmrContext: HmrContext = {file,timestamp,modules: mods ? [...mods] : [],read: () => readModifiedFile(file),server,}for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {const filteredModules = await hook(hmrContext)if (filteredModules) {hmrContext.modules = filteredModules}}if (!hmrContext.modules.length) {// html file cannot be hot updatedif (file.endsWith('.html')) {config.logger.info(colors.green(`page reload `) + colors.dim(shortFile), {clear: true,timestamp: true,})ws.send({type: 'full-reload',path: config.server.middlewareMode? '*': '/' + normalizePath(path.relative(config.root, file)),})} else {// loaded but not in the module graph, probably not jsdebugHmr?.(`[no modules matched] ${colors.dim(shortFile)}`)}return}updateModules(shortFile, hmrContext.modules, timestamp, server)}

我简单画了一下流程图

updateModules

模块更新就需要上面讲到的moduleGraph

这里存储了所有的文件模块图

当有监听到有模块更新, moduleGraph就有发生改变

去除无效的模块, 找到需要更新的模块

最后当发出send类型为update类型, 就是一个文件发生变化啦

    export function updateModules(file: string,modules: ModuleNode[],timestamp: number,{ config, ws, moduleGraph }: ViteDevServer,afterInvalidation?: boolean,): void {const updates: Update[] = []const invalidatedModules = new Set<ModuleNode>()const traversedModules = new Set<ModuleNode>()let needFullReload = falsefor (const mod of modules) {moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true)if (needFullReload) {continue}const boundaries: { boundary: ModuleNode; acceptedVia: ModuleNode }[] = []const hasDeadEnd = propagateUpdate(mod, traversedModules, boundaries)if (hasDeadEnd) {needFullReload = truecontinue}updates.push(...boundaries.map(({ boundary, acceptedVia }) => ({type: `${boundary.type}-update` as const,timestamp,path: normalizeHmrUrl(boundary.url),explicitImportRequired:boundary.type === 'js'? isExplicitImportRequired(acceptedVia.url): undefined,acceptedPath: normalizeHmrUrl(acceptedVia.url),})),)}if (needFullReload) {config.logger.info(colors.green(`page reload `) + colors.dim(file), {clear: !afterInvalidation,timestamp: true,})ws.send({type: 'full-reload',})return}if (updates.length === 0) {debugHmr?.(colors.yellow(`no update happened `) + colors.dim(file))return}config.logger.info(colors.green(`hmr update `) +colors.dim([...new Set(updates.map((u) => u.path))].join(', ')),{ clear: !afterInvalidation, timestamp: true },)ws.send({type: 'update',updates,})}

就比如下图, 文件路径和名称就来自于moduleGraph

流程图附上

client-createSocket

讲完了node端如何发送socket

接下来肯定要有客户端监听到socket

查看目录时, 我们能够看到有一个client目录

这里其实就是客户端处理socket的文件

不知道有没有人好奇, 怎么把文件引入到客户端去, 没有看源码之前, 反正我是很好奇的

不知道大家有没有记得,在action里面有一个createServer

在createServer里面有一个_createServer

在_createServer里面有一个resolveConfig

在resolveConfig里面有resolvePlugins

在resolvePlugins里面有importAnalysisPlugin

就是在importAnalysisPlugin里面

通过字符串导入方式把createHotContext导入到了客户端

这里str是什么

这是来自一个外部的npm包

magic-string

假设您有一些源代码。您想要对其进行一些轻微的修改 - 在这里和那里替换一些字符,用页眉和页脚包装它等等 - 理想情况下您希望在它的末尾生成一个源映射。您考虑过使用 recast 之类的东西(它允许您从一些 JavaScript 生成 AST,对其进行操作,并使用 sourcemap 重新打印它而不会丢失您的注释和格式),但它似乎对您的需求(或者可能是源代码不是 JavaScript)。

能够在客户端代码里面注入,这样就将createHotContext注入到客户端中,并使用

在createHotContent中, 就存在setupWebSocket啦

在浏览器端建立socket核心代码如下:

function setupWebSocket(protocol: string,hostAndPath: string,onCloseWithoutOpen?: () => void,) {const socket = new WebSocket(`${protocol}://${hostAndPath}`, 'socket-hmr')let isOpened = falsesocket.addEventListener('open',() => {isOpened = true},{ once: true },)// Listen for messagessocket.addEventListener('message', async ({ data }) => {// handleMessage(JSON.parse(data))console.log(data)})// ping serversocket.addEventListener('close', async ({ wasClean }) => {if (wasClean) returnif (!isOpened && onCloseWithoutOpen) {onCloseWithoutOpen()return}console.log(`[socket] server connection lost. polling for restart...`)await waitForSuccessfulPing(protocol, hostAndPath)location.reload()})return socket}function warnFailedFetch(err: Error, path: string | string[]) {if (!err.message.match('fetch')) {console.error(err)}console.error(`[hmr] Failed to reload ${path}. ` +`This could be due to syntax errors or importing non-existent ` +`modules. (see errors above)`,)}async function waitForSuccessfulPing(socketProtocol: string,hostAndPath: string,ms = 1000,) {const pingHostProtocol = socketProtocol === 'wss' ? 'https' : 'http'const ping = async () => {// A fetch on a websocket URL will return a successful promise with status 400,// but will reject a networking error.// When running on middleware mode, it returns status 426, and an cors error happens if mode is not no-corstry {await fetch(`${pingHostProtocol}://${hostAndPath}`, {mode: 'no-cors',headers: {// Custom headers won't be included in a request with no-cors so (ab)use one of the// safelisted headers to identify the ping requestAccept: 'text/x-socket-ping',},})return true} catch {}return false}if (await ping()) {return}await wait(ms)// eslint-disable-next-line no-constant-conditionwhile (true) {if (document.visibilityState === 'visible') {if (await ping()) {break}await wait(ms)} else {await waitForWindowShow()}}}function waitForWindowShow() {return new Promise<void>((resolve) => {const onChange = async () => {if (document.visibilityState === 'visible') {resolve()document.removeEventListener('visibilitychange', onChange)}}document.addEventListener('visibilitychange', onChange)})}function wait(ms: number) {return new Promise((resolve) => setTimeout(resolve, ms))}

这样就建立了浏览器端与node端的通信啦

好了, 今天分享到这里就结束了, 源码分析解释还是篇幅太长, 所以我这里就只提到了dev命令.

vite的其他命令其实也是这样分析, 如果有什么疑问或者不好之处, 欢迎大家在评论区指出, 祝大家有个愉快的周末!

vite源码分析之dev相关推荐

  1. partprobe源码分析

    partprobe工具 操作系统目录/usr/sbin/partprobe 程序安装包parted-3.1-17.el7.x86_64.rpm 命令用法: partprobe是用来告知操作系统内核 分 ...

  2. s-sgdisk源码分析 “--set-alignment=value分区对齐参数”

    文章目录 边界对齐子命令使用 源码分析 sgdisk.cc main函数入口 gptcl.cc DoOptions解析并执行具体命令函数 gpt.cc CreatePartition创建分区函数,设置 ...

  3. Journey源码分析三:模板编译

    2019独角兽企业重金招聘Python工程师标准>>> 在Journey源码分析二:整体启动流程中提到了模板编译,这里详细说下启动流程 看下templates.Generate()源 ...

  4. celery源码分析:multi命令分析

    celery源码分析 本文环境python3.5.2,celery4.0.2,django1.10.x系列 celery简介 celery是一款异步任务框架,基于AMQP协议的任务调度框架.使用的场景 ...

  5. Django源码分析1:创建项目和应用分析

    django命令行源码分析 本文环境python3.5.2,django1.10.x系列 当命令行输入时django-admin时 (venv) ACA80166:dbManger wuzi$ dja ...

  6. 阿里面试这样问:Nacos配置中心交互模型是 push 还是 pull ?(原理+源码分析)...

    本文来源:公众号「 程序员内点事」 对于Nacos大家应该都不太陌生,出身阿里名声在外,能做动态服务发现.配置管理,非常好用的一个工具.然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用 ...

  7. 《深入理解Spark:核心思想与源码分析》——1.2节Spark初体验

    本节书摘来自华章社区<深入理解Spark:核心思想与源码分析>一书中的第1章,第1.2节Spark初体验,作者耿嘉安,更多章节内容可以访问云栖社区"华章社区"公众号查看 ...

  8. Linux驱动修炼之道-SPI驱动框架源码分析(上)

    Linux驱动修炼之道-SPI驱动框架源码分析(上)   SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设备可工作于m/s模式.主设备发起数据帧,允许多个从设 ...

  9. Android10.0 日志系统分析(三)-logd、logcat读写日志源码分析-[Android取经之路]

    摘要:本节主要来讲解Android10.0 logd.logcat读写日志源码内容 阅读本文大约需要花费20分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Andro ...

最新文章

  1. HUAWEI视讯技术学习笔记(转载)
  2. ArcGIS Runtime for .Net Quartz开发探秘(三):承接来自GIS服务器的服务
  3. LinkedList 源码分析(JDK 1.8)
  4. 阳台花园不只美丽-东方美琪·安琪:身心健康谋定心灵升华
  5. Python实现二叉树的非递归先序遍历
  6. 服务器监控页面html_Nmon实时监控并生成HTML监控报告
  7. tcp报文 如何判断是否为握手_“三次握手,四次挥手”你真的懂吗?
  8. 谁手握账本?趣讲 ZK 的内存模型
  9. 袖珍计算机英语手册,英语袖珍迷你系列__中考英语速记手册__刘国婷.pdf
  10. 在html中实现word中打批注的功能
  11. linux查看服务进程发包,11月18日linux服务器后,服务器向外发包,CPU达99%以上
  12. CMakeLists.txt范例
  13. 航空发动机涂层行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
  14. 计算机知识小口诀,字根表口诀怎么快速背-小学数学:一年级20以内加减法口诀表,附背诵技巧!...
  15. 哈勃(Hubble)太空望远镜:人类的大眼睛
  16. 02#EXCEL函数【基础】
  17. 大数据学习笔记-------------------(17_3)
  18. 使用反应路由器V4以编程方式导航
  19. 论文发表查重率要小于多少?
  20. 如何保证邮件系统的安全?

热门文章

  1. 毕业设计-基于卷积神经网络的花卉图片识别
  2. Hadoop集群重启过程记录
  3. MinIO windos安装包百度云盘免费下载
  4. 婚恋交友约会app源码一对一一对多直播APP源码+android客户端+java服务端源码
  5. python判断成年,使用python判断你是青少年还是老年人
  6. codeforces 891C
  7. java测试模拟网页点击,WebTest比拼Selenium:模拟和真实浏览器上的测试
  8. 四川2021高考体考成绩查询,四川2021年中考体考成绩查询
  9. 微软的鼠标将会使用蓝光LED照明
  10. 拒绝细胞衰老、远离老年疾病,爱丁堡大学给细胞开出 3 张「AI 抗衰处方」