Vite: 配置解析服务

概述

  • 我们为什么要去读源码?原因主要有两个:一是加深对框架本身的理解,在面对一些项目的疑难杂症时,排查问题效率会更高;二是在遇到类似的开发场景时,可以举一反三,借鉴某个框架源码的实现思路,将技巧应用到其它的项目中

  • 我们看下Vite 配置解析服务的源码部分。Vite 构建环境分为 开发环境 和 生产环境 ,不同环境会有不同的构建策略,但不管是哪种环境,Vite 都会首先解析用户配置。那接下来,我就与你分析配置解析过程中 Vite 到底做了什么。首先,要梳理整体的实现流程,然后拆解其中的重点细节,即 如何加载配置文件 ,不仅对 Vite 的配置解析服务有系统且完整的认识,最好还能写一个自己的 配置文件加载器

流程梳理

  • 我们先来梳理整体的流程,Vite 中的配置解析由 resolveConfig 函数来实现,你可以对照源码一起学习。

1 )加载配置文件

  • 进行一些必要的变量声明后,我们进入到解析配置逻辑中:
    // 这里的 config 是命令行指定的配置,如 vite --configFile=xxx
    let {
      configFile
    } = config
    if (configFile !== false) {
      // 默认都会走到下面加载配置文件的逻辑,除非你手动指定 configFile 为 false
      const loadResult = await loadConfigFromFile(
        configEnv,
        configFile,
        config.root,
        config.logLevel
      )
      if (loadResult) {
        // 解析配置文件的内容后,和命令行配置合并
        config = mergeConfig(loadResult.config, config)
        configFile = loadResult.path
        configFileDependencies = loadResult.dependencies
      }
    }
    
  • 第一步是解析配置文件的内容(这部分比较复杂,本文后续单独分析),然后与命令行配置合并。值得注意的是,后面有一个记录 configFileDependencies 的操作。因为配置文件代码可能会有第三方库的依赖,所以当第三方库依赖的代码更改时,Vite 可以通过HMR 处理逻辑中记录的 configFileDependencies 检测到更改,再重启 DevServer ,来保证当前生效的配置永远是最新的。

2 ) 解析用户插件

  • 第二个重点环节是 解析用户插件。首先,我们通过 apply 参数 过滤出需要生效的用户插件。为什么这么做呢?因为有些插件只在开发阶段生效,或者说只在生产环境生效,我们可以通过 apply: ‘serve’ 或 ‘build’ 来指定它们,同时也可以将 apply 配置为一个函数,来自定义插件生效的条件。解析代码如下:
// resolve plugins
const rawUserPlugins = (config.plugins || [])
  .flat()
  .filter((p) => {
    if (!p) {
      return false
    } else if (!p.apply) {
      return true
    } else if (typeof p.apply === 'function') {
      // apply 为一个函数的情况
      return p.apply({
        ...config,
        mode
      }, configEnv)
    } else {
      return p.apply === command
    }
  }) as Plugin[]
// 对用户插件进行排序
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)
  • 接着,Vite 会拿到这些过滤且排序完成的插件,依次调用插件 config 钩子,进行配置合并
    // run config hooks
    const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
    for (const p of userPlugins) {
      if (p.config) {
        const res = await p.config(config, configEnv)
        if (res) {
          // mergeConfig 为具体的配置合并函数,大家有兴趣可以阅读一下实现
          config = mergeConfig(config, res)
        }
      }
    }
    
  • 然后解析项目的根目录即 root 参数,默认取 process.cwd() 的结果:
    // resolve root
    const resolvedRoot = normalizePath(
      config.root ? path.resolve(config.root) : process.cwd()
    )
    
  • 紧接着处理 alias ,这里需要加上一些内置的 alias 规则,如 @vite/env 、@vite/client 这种直接重定向到 Vite 内部的模块:
    // resolve alias with internal client alias
    const resolvedAlias = mergeAlias(
      clientAlias,
      config.resolve ? .alias || config.alias || []
    )
    const resolveOptions: ResolvedConfig['resolve'] = {
      dedupe: config.dedupe,
      ...config.resolve,
      alias: resolvedAlias
    }
    

3 ) 加载环境变量

  • 现在,我们进入第三个核心环节: 加载环境变量,它的实现代码如下
    // load .env files
    const envDir = config.envDir ?
      normalizePath(path.resolve(resolvedRoot, config.envDir)) :
      resolvedRoot
    const userEnv =
      inlineConfig.envFile !== false &&
      loadEnv(mode, envDir, resolveEnvPrefix(config))
    
  • loadEnv 其实就是扫描 process.env 与 .env 文件,解析出 env 对象,值得注意的是,这个对象的属性最终会被挂载到 import.meta.env 这个全局对象上。解析 env 对象的实现思路如下:
  • 遍历 process.env 的属性,拿到指定前缀开头的属性(默认指定为 VITE_ ),并挂载 env 对象上遍历 .env 文件,解析文件,然后往 env 对象挂载那些以指定前缀开头的属性。遍历的文件先后顺序如下(下面的 mode 开发阶段为 development ,生产环境为production ):
    • .env.${mode}.local
    • .env.${mode}
    • .env.local
    • .env
  • 特殊情况: 如果中途遇到 NODE_ENV 属性,则挂到 process.env.VITE_USER_NODE_ENV ,Vite 会优先通过这个属性来决定是否走生产环境 的构建。
  • 接下来是对资源公共路径即 base URL 的处理,逻辑集中在 resolveBaseUrl 函数当中:
    // 解析 base url
    const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger)
    // 解析生产环境构建配置
    const resolvedBuildOptions = resolveBuildOptions(config.build)
    
  • resolveBaseUrl 里面有这些处理规则需要注意:
    • 空字符或者 ./ 在开发阶段特殊处理,全部重写为 /
    • . 开头的路径,自动重写为 /
    • http(s):// 开头的路径,在开发环境下重写为对应的 pathname
    • 确保路径开头和结尾都是 /
  • 当然,还有对 cacheDir 的解析,这个路径相对于在 Vite 预编译时写入依赖产物的路径:
    // resolve cache directory
    const pkgPath = lookupFile(resolvedRoot, [`package.json`], true /* pathOnly */)
    // 默认为 node_module/.vite
    const cacheDir = config.cacheDir
     ? path.resolve(resolvedRoot, config.cacheDir)
     : pkgPath && path.join(path.dirname(pkgPath), `node_modules/.vite`)
    
  • 紧接着处理用户配置的 assetsInclude ,将其转换为一个过滤器函数:
    const assetsFilter = config.assetsInclude
     ? createFilter(config.assetsInclude)
     : () => false
    
  • Vite 后面会将用户传入的 assetsInclude 和内置的规则合并:
    assetsInclude(file: string) {
     return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
    }
    
  • 这个配置决定是否让 Vite 将对应的后缀名视为 静态资源文件 (asset)来处理。

4 ) 路径解析器工厂

  • 接下来,进入到第四个核心环节: 定义路径解析器工厂。这里所说的 路径解析器 ,是指调用插件容器进行 路径解析 的函数。代码结构是这个样子的:
const createResolver: ResolvedConfig['createResolver'] = (options) => {
 let aliasContainer: PluginContainer | undefined
 let resolverContainer: PluginContainer | undefined
 // 返回的函数可以理解为一个解析器
 return async (id, importer, aliasOnly, ssr) => {
   let container: PluginContainer
   if (aliasOnly) {
     container = aliasContainer || undefined
     // 新建 aliasContainer
   } else {
     container = resolverContainer || undefined
     // 新建 resolveContainer
   }
   return (await container.resolveId(id, importer, undefined, ssr)) ? .id
 }
}
  • 这个解析器未来会在依赖预构建的时候用上,具体用法如下:
    const resolve = config.createResolver()
    // 调用以拿到 react 路径
    rseolve('react', undefined, undefined, false)
    
  • 这里有 aliasContainer 和 resolverContainer 两个工具对象,它们都含有 resolveId 这个专门解析路径的方法,可以被 Vite 调用来获取解析结果。
  • 两个工具对象的本质是 PluginContainer ,我们将在「编译流水线」小节详细介
    绍 PluginContainer 的特点和实现
  • 接着会顺便处理一个 public 目录,也就是 Vite 作为静态资源服务的目录:
    const {
      publicDir
    } = config
    const resolvedPublicDir =
      publicDir !== false && publicDir !== '' ?
      path.resolve(
        resolvedRoot,
        typeof publicDir === 'string' ? publicDir : 'public'
      ) : ''
    
  • 至此,配置已经基本上解析完成,最后通过 resolved 对象来整理一下:
    const resolved: ResolvedConfig = {
      ...config,
      configFile: configFile ? normalizePath(configFile) : undefined,
      configFileDependencies,
      inlineConfig,
      root: resolvedRoot,
      base: BASE_URL
      // 其余配置不再一一列举
    }
    

5 ) 生成插件流水线

  • 最后,我们进入第五个环节: 生成插件流水线。代码如下:
    ;(resolved.plugins as Plugin[]) = await resolvePlugins(
      resolved,
      prePlugins,
      normalPlugins,
      postPlugins
    )
    // call configResolved hooks
    await Promise.all(userPlugins.map((p) => p.configResolved ? .(resolved)))
    
  • 先生成完整插件列表传给 resolve.plugins ,而后调用每个插件的 configResolved 钩子函数。其中 resolvePlugins 内部细节比较多,插件数量比较庞大,我们暂时不去深究具体实现,这块设计编译流水线
  • 至此,所有核心配置都生成完毕。不过,后面 Vite 还会处理一些边界情况,在用户配置不合理的时候,给用户对应的提示。比如:用户直接使用 alias 时,Vite 会提示使用 resolve.alias, 最后,resolveConfig 函数会返回 resolved 对象,也就是最后的配置集合,那么配置解析服务到底也就结束了

加载配置文件详解

  • 配置解析服务的流程梳理完,但刚开始 加载配置文件(loadConfigFromFile) 的实现我们还没有具体分析,先来回顾下代码
    *const loadResult = await loadConfigFromFile(/*省略传参*/)
  • 这里的逻辑稍微有点复杂,很难梳理清楚,所以我们不妨借助刚才梳理的配置解析流程,深入 loadConfigFromFile 的细节中,研究下 Vite 对于配置文件加载的实现思路。
  • 首先,我们来分析下需要处理的配置文件类型,根据 文件后缀 和 模块格式 可以分为下面这几类
    • TS + ESM 格式
    • TS + CommonJS 格式
    • JS + ESM 格式
    • JS + CommonJS 格式
  • 那么,Vite 是如何加载配置文件的?一共分两个步骤:
    • 识别出配置文件的类别
    • 根据不同的类别分别解析出配置内容

1 ) 识别配置文件的类别

  • 首先 Vite 会检查项目的 package.json ,如果有 type: “module” 则打上 isESM 的标识:

    try {
     const pkg = lookupFile(configRoot, ['package.json'])
     if (pkg && JSON.parse(pkg).type === 'module') {
     	isMjs = true
     }
    } catch (e) {}
    
  • 然后,Vite 会寻找配置文件路径,代码简化后如下:

    let isTS = false
    let isESM = false
    let dependencies: string[] = []
    // 如果命令行有指定配置文件路径
    if (configFile) {
      resolvedPath = path.resolve(configFile)
      // 根据后缀判断是否为 ts 或者 esm,打上 flag
      isTS = configFile.endsWith('.ts')
      if (configFile.endsWith('.mjs')) {
        isESM = true
      }
    } else {
      // 从项目根目录寻找配置文件路径,寻找顺序:
      // - vite.config.js
      // - vite.config.mjs
      // - vite.config.ts
      // - vite.config.cjs
      const jsconfigFile = path.resolve(configRoot, 'vite.config.js')
      if (fs.existsSync(jsconfigFile)) {
        resolvedPath = jsconfigFile
      }
      if (!resolvedPath) {
        const mjsconfigFile = path.resolve(configRoot, 'vite.config.mjs')
        if (fs.existsSync(mjsconfigFile)) {
          resolvedPath = mjsconfigFile
          isESM = true
        }
      }
      if (!resolvedPath) {
        const tsconfigFile = path.resolve(configRoot, 'vite.config.ts')
        if (fs.existsSync(tsconfigFile)) {
          resolvedPath = tsconfigFile
          isTS = true
        }
      }
    
      if (!resolvedPath) {
        const cjsConfigFile = path.resolve(configRoot, 'vite.config.cjs')
        if (fs.existsSync(cjsConfigFile)) {
          resolvedPath = cjsConfigFile
          isESM = false
        }
      }
    }
    
  • 在寻找路径的同时, Vite 也会给当前配置文件打上 isESM 和 isTS 的标识,方便后续的解析。

2 )根据类别解析配置

ESM 格式

对于 ESM 格式配置的处理代码如下:

let userConfig: UserConfigExport | undefined
if (isESM) {
  const fileUrl = require('url')
    .pathToFileURL(resolvedPath)
  // 首先对代码进行打包
  const bundled = await bundleConfigFile(resolvedPath, true)
  dependencies = bundled.dependencies
  // TS + ESM
  if (isTS) {
    fs.writeFileSync(resolvedPath + '.js', bundled.code)
    userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`))
      .default
    fs.unlinkSync(resolvedPath + '.js')
    debug(`TS + native esm config loaded in ${getTime()}`, fileUrl)
  }
  // JS + ESM
  else {
    userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`))
      .default
    debug(`native esm config loaded in ${getTime()}`, fileUrl)
  }
}
  • 首先通过 Esbuild 将配置文件编译打包成 js 代码:
    const bundled = await bundleConfigFile(resolvedPath, true)
    // 记录依赖
    dependencies = bundled.dependencies
    
  • 对于 TS 配置文件来说,Vite 会将编译后的 js 代码写入 临时文件 ,通过 Node 原生 ESM Import 来读取这个临时的内容,以获取到配置内容,再直接删掉临时文件:
    fs.writeFileSync(resolvedPath + '.js', bundled.code)
    userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`)).default
    fs.unlinkSync(resolvedPath + '.js')
    
  • 以上这种先编译配置文件,再将产物写入临时目录,最后加载临时目录产物的做法,也是 AOT (Ahead Of Time)编译技术的一种具体实现。
  • 以上这种先编译配置文件,再将产物写入临时目录,最后加载临时目录产物的做
    法,也是 AOT (Ahead Of Time)编译技术的一种具体实现。
    export const dynamicImport = new Function('file', 'return import(file)')
    
  • 你可能会问,为什么要用 new Function 包裹?这是为了避免打包工具处理这段代码,比如 Rollup 和 TSC ,类似的手段还有 eval 。你可能还会问,为什么 import 路径结果要加上时间戳 query?这其实是为了让 dev server 重启后仍然读取最新的配置,避免缓存。

CommonJS 格式

对于 CommonJS 格式的配置文件,Vite 集中进行了解析:

// 对于 js/ts 均生效
// 使用 esbuild 将配置文件编译成 commonjs 格式的 bundle 文件
const bundled = await bundleConfigFile(resolvedPath)

dependencies = bundled.dependencies
// 加载编译后的 bundle 代码
userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)

bundleConfigFile 的逻辑上文中已经说了,主要是通过 Esbuild 将配置文件打包,拿到打包后的 bundle 代码以及配置文件的依赖(dependencies)。而接下来的事情就是考虑如何加载 bundle 代码了,这也是 loadConfigFromBundledFile 要做的事情。我们来看一下这个函数具体的实现

async function loadConfigFromBundledFile(
  fileName: string,
  bundledCode: string
): Promise < UserConfig > {
  const extension = path.extname(fileName)
  const defaultLoader = require.extensions[extension] !
    require.extensions[extension] = (module: NodeModule, filename: string) => {
      if (filename === fileName) {
        ;
        (module as NodeModuleWithCompile)
        ._compile(bundledCode, filename)
      } else {
        defaultLoader(module, filename)
      }
    }
  // 清除 require 缓存
  delete require.cache[require.resolve(fileName)]
  const raw = require(fileName)
  const config = raw.__esModule ? raw.default : raw
  require.extensions[extension] = defaultLoader
  return config
}
  • 大体的思路是通过拦截原生 require.extensions 的加载函数来实现对 bundle 后配置代码的加载。代码如下:

    // 默认加载器
    const defaultLoader = require.extensions[extension] !
      // 拦截原生 require 对于`.js`或者`.ts`的加载
      require.extensions[extension] = (module: NodeModule, filename: string) => {
        // 针对 vite 配置文件的加载特殊处理
        if (filename === fileName) {
          ;
          (module as NodeModuleWithCompile)
          ._compile(bundledCode, filename)
        } else {
          defaultLoader(module, filename)
        }
      }
    
  • 而原生 require 对于 js 文件的加载代码是这样的:

    Module._extensions['.js'] = function(module, filename) {
      var content = fs.readFileSync(filename, 'utf8')
      module._compile(stripBOM(content), filename)
    }
    
  • Node.js 内部也是先读取文件内容,然后编译该模块。当代码中调用 module._compile 相当于手动编译一个模块,该方法在 Node 内部的实现如下:

    Module.prototype._compile = function(content, filename) {
      var self = this
      var args = [self.exports, require, self, filename, dirname]
      return compiledWrapper.apply(self.exports, args)
    }
    
  • 等同于下面的形式:

    ;(function (exports, require, module, __filename, __dirname) {
     // 执行 module._compile 方法中传入的代码
     // 返回 exports 对象
    })
    
  • 在调用完 module._compile 编译完配置代码后,进行一次手动的 require,即可拿到配置对象:

    const raw = require(fileName)
    const config = raw.__esModule ? raw.default : raw
    // 恢复原生的加载方法
    require.extensions[extension] = defaultLoader
    // 返回配置
    return config
    
  • 这种运行时加载 TS 配置的方式,也叫做 JIT (即时编译),这种方式和 AOT 最大的区别在于不会将内存中计算出来的 js 代码写入磁盘再加载,而是通过拦截Node.js 原生 require.extension 方法实现即时加载

  • 至此,配置文件的内容已经读取完成,等后处理完成再返回即可

    // 处理是函数的情况
    const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig)
    if (!isObject(config)) {
      throw new Error(`config must export or return an object.`)
    }
    // 接下来返回最终的配置信息
    return {
      path: normalizePath(resolvedPath),
      config,
      // esbuild 打包过程中搜集的依赖
      dependencies
    }
    

总结

  • 回顾 Vite 配置解析的整体流程 和 加载配置文件的方法
  • 首先,Vite 配置文件解析的逻辑由 resolveConfig 函数统一实现,其中经历了加载配置文件、解析用户插件、加载环境变量、创建路径解析器工厂和生成插件流水线这几个主要的流程
  • 其次,在 加载配置文件 的过程中,Vite 需要处理四种类型的配置文件,其中对于 ESM 和 CommonJS 两种格式的 TS 文件,分别采用了 AOT 和 JIT 两种编译技术实现了配置加载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/763462.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Hadoop权威指南-读书笔记-01-初识Hadoop

Hadoop权威指南-读书笔记 记录一下读这本书的时候觉得有意思或者重要的点~ 第一章—初识Hadoop Tips&#xff1a; 这个引例很有哲理嘻嘻&#x1f604;&#xff0c;道出了分布式的灵魂。 1.1 数据&#xff01;数据&#xff01; 这一小节主要介绍了进入大数据时代&#xff0c;面…

【windows|012】光猫、路由器、交换机详解

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 ​ &#x1f3c5;阿里云ACE认证高级工程师 ​ &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社…

QML学习——Qt Quick Extras Examples 1.4(八)

Qt Quick Extras Examples 阅读官方的源码然后尝试做了下 01 A car dashboard 样例演示&#xff1a; 说明&#xff1a; ValueSource组件控制数值相关的动画&#xff0c;例如图中数值的变化&#xff1b;TurnIndicator组件是控制左右方向灯的闪烁和背景&#xff0c;里面使用…

excel修改批量一列单价的金额并保留1位小数

1.打开表格&#xff0c;要把单价金额变成现在的两倍&#xff0c;数据如下&#xff1a; 2.把单价这一列粘贴到一个新的sheet页面&#xff0c;在B2单元格输入公式&#xff1a;A2*2 然后按enter回车键,这时候吧鼠标放到B2单元格右下角&#xff0c;会出现一个黑色的小加号&#xf…

SQL 注入联合查询之为什么要 and 1=2

在 SQL 注入联合查询中&#xff0c;将 id 先置为假&#xff08;如 id-1 或其他使查询结果为空的条件&#xff09;&#xff0c;通常是为了让前面的查询语句查询不到结果&#xff0c;从而使联合查询中后面的语句结果能够显示在回显位上

【深度学习】pytorch训练中的一个大坑

使用的命令&#xff1a;iostat -x 5 可以看到 ssd的利用率已经满了。 之前在的数据集放在了 hdd上&#xff0c;训练结果特别慢。 所以我把它移动到了ssd上&#xff0c;然后训练参数用的 resume&#xff0c; 但是&#xff01;&#xff01;&#xff01;&#xff01;它把历史记住…

虚拟环境管理

虚拟环境 在使用 Python 时我们一般使用“pip install 第三方包名”来安装第三方包&#xff0c;但是由于pip的特性&#xff0c;系统只能安装每个包的一个版本。而在实际开发中&#xff0c;可能同时开发多个项目&#xff0c;如&#xff1a;上图有三个项目&#xff1b;每个项目需…

摄影后期色彩管理流程(Lightroom篇)

在摄影后期处理中&#xff0c;色彩管理是确保图像从捕捉到输出的一致性和准确性的关键。Lightroom 和 Photoshop 其实已经将这套色彩管理流程作为默认选项&#xff0c;如果实质操作时仍存在色彩偏差的问题&#xff0c;可参考以下内容。 ProPhoto RGB > Adobe RGB > sRGB …

幻兽帕鲁服务器如何安装模组安装

由于模组多数为Window版本的&#xff0c;所以本教程以服务端为Window的作为演示&#xff08;Linux服务端的也是一样的操作&#xff09;百度莱卡云开服 如果你你是Linux版本的&#xff0c;请点击跳转切换服务端教程 接下来是本地安装模组包的方法&#xff08;服务器自带&#xf…

Web3 游戏周报(6.23 - 6.29)

区块链游戏热度不减&#xff0c;你是否掌握了上周的重要动态&#xff1f; 回顾上周区块链游戏动态&#xff0c;查看 Footprint Analytics 与 ABGA 的最新数据报告。 【6.23 - 6.29】Web3 游戏行业动态&#xff1a; 继 Notcoin 之后&#xff0c;另一款 Telegram 游戏 Hamster …

React实战学习(一)_棋盘设计

需求&#xff1a; 左上侧&#xff1a;状态左下侧&#xff1a;棋盘&#xff0c;保证胜利就结束 和 下过来的不能在下右侧&#xff1a;“时光机”,保证可以回顾&#xff0c;索引 语法&#xff1a; 父子之间属性传递&#xff08;props&#xff09;子父组件传递&#xff08;写法上&…

【MySQL篇】Percona XtraBackup物理备份工具的基础理论概述(第一篇,总共五篇)

&#x1f4ab;《博主介绍》&#xff1a;✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ &#x1f4ab;《擅长领域》&#xff1a;✌️擅长Oracle、MySQL、SQLserver、阿里云AnalyticDB for MySQL(分布式数据仓库)、Linux&#xff0c;也在扩展大数据方向的知识面✌️…

​产品经理-困惑4:产品面对开发是否低人一等(4)

在互联网当中,做产品的,在面对开发是否觉得低人一等&#xff1f; 完全不会 从团队层面来看&#xff0c;任何互联网团队都是由开发、产品、视觉、运营、市场等专业人才所组成的专业团队 每人各有专攻&#xff0c;为同一个目标&#xff08;即项目成功&#xff09;而不懈努力。各工…

带安全启动—Ubuntu系统—手动安装Nvidia驱动

教程1&#xff1a;在启用安全启动的 Fedora 中安装英伟达驱动 教程2&#xff1a;UEFI安全启动模式下安装Ubuntu的NVIDIA显卡驱动 1. 搜索合适的驱动 Nvidia驱动官网 选择这个 驱动(.run)链接 2. 安装必要的软件依赖 CUDA底层用C写的&#xff0c;因此导入编译器 sudo apt i…

1-4.时间序列数据建模流程范例

文章最前&#xff1a; 我是Octopus&#xff0c;这个名字来源于我的中文名–章鱼&#xff1b;我热爱编程、热爱算法、热爱开源。所有源码在我的个人github &#xff1b;这博客是记录我学习的点点滴滴&#xff0c;如果您对 Python、Java、AI、算法有兴趣&#xff0c;可以关注我的…

已解决java.io.NotSerializableException:对象不支持序列化的正确解决方法,亲测有效!!!

已解决java.io.NotSerializableException&#xff1a;对象不支持序列化的正确解决方法&#xff0c;亲测有效&#xff01;&#xff01;&#xff01; 目录 问题分析 出现问题的场景 示例代码 报错原因 解决思路 解决方法 1. 实现Serializable接口 修改后的Employee类 2…

递归----计算P函数

注意运算中的符号不能少&#xff01;&#xff01;&#xff01;&#xff01; * 必须体现出&#xff01;&#xff01;&#xff01;&#xff01; #include <stdio.h>double P( int n, double x );int main() {int n;double x;scanf("%d %lf", &n, &x);pri…

计算机毕业设计Python+Spark股票基金推荐与预测系统 股票基金可视化 股票基金推荐系统 股票基金可视化系统 股票基金数据分析 股票基金爬虫大数据

目 录 摘 要 Abstract 第1章 前 言 1.1 项目的背景和意义 1.2 研究现状 1.3 项目的目标和范围 1.4 论文结构简介 第2章 技术与原理 2.1 开发原理 2.2 开发工具 2.3 关键技术 第3章 需求建模 3.1 系统可行性分析 3.2 功能需求分析 3.3 非功能性…

opengl箱子的显示

VS环境配置&#xff1a; /JMC /ifcOutput "Debug\" /GS /analyze- /W3 /Zc:wchar_t /I"D:\Template\glfwtemplate\glfwtemplate\assimp" /I"D:\Template\glfwtemplate\glfwtemplate\glm" /I"D:\Template\glfwtemplate\glfwtemplate\LearnOp…

Wireshark - tshark支持iptables提供数据包

tshark现在的数据包获取方式有两种&#xff0c;分别是读文件、网口监听&#xff08;af-packet原始套接字&#xff09;。两种方式在包获取上&#xff0c;都是通过读文件的形式&#xff1b;存在文件io操作&#xff0c;在专门处理大流量的情境下&#xff0c; 我们复用wireshark去做…