阅读 241

Vite 源码(二)vite启动流程以及如何获取config配置

当在控制台输入yarn run dev时,执行对应源码的位置是node/cli.ts

import { cac } from 'cac' // 创建CLI实例,'vite'表示,在 help 和 version 命令中显示的名称 const cli = cac('vite') // 添加命令项 cli   .option('-c, --config <file>', `[string] use specified config file`)   // ...    // dev cli   .command('[root]') // default command   .alias('serve') // 设置命令别名   .option('--host [host]', `[string] specify hostname`)   // ...   .option('--cors', `[boolean] enable CORS`) // 使用 CORS   // 如果指定端口号则退出   .option('--strictPort', `[boolean] exit if specified port is already in use`)   .option(     '--force',     `[boolean] force the optimizer to ignore the cache and re-bundle`   ) // 忽略预构建缓存,重新构建   // 当命令与用户输入匹配时,调用这个回调函数   .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {})      cli.help()   cli.version(require('../../package.json').version)   cli.parse() 复制代码

Vite 使用 cscjs搭建的命令

当执行这个命令会调用action的回调函数,看下主要代码

// root: 如果执行的是 yarn run dev -- test 或者 npx vite test,则 root 参数为 'test'  // options:命令行中的参数 .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {     const { createServer } = await import('./server')     try {       const server = await createServer({         root, // 命令名         base: options.base, // 公共基础路径         // 环境模式,development | production         mode: options.mode,         configFile: options.config, // 配置文件目录         // 调整控制台输出的级别,默认为 'info',可选值 'info' | 'warn' | 'error' | 'silent'         logLevel: options.logLevel,         clearScreen: options.clearScreen, // 是否清空终端打印的信息         // 开发服务器配置,比如 host、port、open、https、cors、strictPort、force         server: cleanOptions(options)       })       await server.listen()   }) 复制代码

createServer

createServer函数定义在src/node/server/index.ts里面,由于createServer函数代码比较多,这里只捡重要的说

// 删减版,包含主要流程 export async function createServer(inlineConfig) {     // 获取config配置     const config = await resolveConfig(inlineConfig, 'serve', 'development')     // 获取项目根路径     const root = config.root     // 获取本地服务器相关的配置     const serverConfig = config.server     // 创建中间件实例     const middlewares = connect() as Connect.Server     // 创建 http 服务器     const httpServer = await resolveHttpServer(         serverConfig,         middlewares,         httpsOptions     )     // 创建 WebSocket 服务器     const ws = createWebSocketServer(httpServer, config, httpsOptions)     // ignored:忽略监听的文件;watchOptions:对应 server.watch 配置,传递给 chokidar 的文件系统监视器选项     const { ignored = [], ...watchOptions } = serverConfig.watch || {}     // 通过 chokidar 监听文件     const watcher = chokidar.watch(path.resolve(root), {         ignored: [             '**/node_modules/**',             '**/.git/**',             ...(Array.isArray(ignored) ? ignored : [ignored]),         ],         ignoreInitial: true,         ignorePermissionErrors: true,         disableGlobbing: true,         ...watchOptions,     }) as FSWatcher     // 获取 所有插件     const plugins = config.plugins     // 创建插件容器,是一个对象,对象的属性是 vite 支持的 rollup 的钩子函数,后面会介绍     // 比如 options、resolveId、load、transform     const container = await createPluginContainer(config, watcher)     // 创建Vite 的 ModuleGraph 实例,后面也会介绍     const moduleGraph = new ModuleGraph(container)     // 声明 server 对象     const server: ViteDevServer = {         config, // 包含命令行传入的配置 和 配置文件的配置         middlewares,         get app() {             return middlewares         },         httpServer, // http 服务器         watcher, // 通过 chokidar 监听文件         pluginContainer: container, // vite 支持的 rollup 的钩子函数         ws, // WebSocket 服务器         moduleGraph, // ModuleGraph 实例         transformWithEsbuild,         transformRequest(url, options) {},         listen(port?: number, isRestart?: boolean) {},         _optimizeDepsMetadata: null,         _isRunningOptimizer: false,         _registerMissingImport: null,         _pendingReload: null,         _pendingRequests: Object.create(null),     }     // 被监听文件发生变化时触发     watcher.on('change', async (file) => {})     // 添加文件时触发     watcher.on('add', (file) => {})     watcher.on('unlink', (file) => {})     // 执行插件中的 configureServer 钩子函数     // configureServer:https://vitejs.cn/guide/api-plugin.html#configureserver     const postHooks: ((() => void) | void)[] = []     for (const plugin of plugins) {         if (plugin.configureServer) {             // configureServer 可以注册前置中间件,就是在内部中间件之前执行;也可以注册后置中间件             // 如果configureServer 返回一个函数,这个函数内部就是注册后置中间件,并将这些函数收集到 postHooks 中             postHooks.push(await plugin.configureServer(server))         }     }     // 接下来就是注册中间件     // base     if (config.base !== '/') {         middlewares.use(baseMiddleware(server))     }     // ...     // 主要转换中间件     middlewares.use(transformMiddleware(server))     // ...       // 如果请求路径是 /结尾,则将路径修改为 /index.html     if (!middlewareMode || middlewareMode === 'html') {         middlewares.use(spaFallbackMiddleware(root))     }     // 调用用户定义的后置中间件     postHooks.forEach((fn) => fn && fn())     if (!middlewareMode || middlewareMode === 'html') {         // 如果请求的url是 html 则调用插件中所有的 transformIndexHtml 钩子函数,转换html,并将转换后的 html 代码发送给客户端         middlewares.use(indexHtmlMiddleware(server))         // handle 404s         middlewares.use(function vite404Middleware(_, res) {             res.statusCode = 404             res.end()         })     }     if (!middlewareMode && httpServer) {         // 重写 httpServer.listen,在服务器启动前预构建         const listen = httpServer.listen.bind(httpServer)         httpServer.listen = (async (port: number, ...args: any[]) => {}) as any     } else {}     return server } 复制代码

createServer函数的大体流程如下

  • 获取config配置

  • 创建 http 服务器httpServer

  • 创建 WebSocket 服务器ws

  • 通过 chokidar 创建监听器watcher

  • 创建一个兼容rollup钩子函数的对象container

  • 创建模块图谱实例moduleGraph

  • 声明server对象

  • 注册watcher回调

  • 执行插件中的configureServer钩子函数(注册用户定义的前置中间件),并收集用户定义的后置中间件

  • 注册中间件

  • 注册用户定义的后置中间件

  • 注册转换html文件的中间件和未找到文件的404中间件

  • 重写 httpServer.listen

  • 返回server对象

整体逻辑就是,调用createServer函数,拿到返回值后调用server.listen(),在这个过程中会对预构建依赖包并开启本地开发服务器。

小结

Vite 冷启动为什么快

Vite 运行 Dev 命令后只做了两件事情,一是启动了本地服务器并注册了一些中间件;二是使用 ESbuild 预构建模块。之后就一直躺着,直到浏览器以 http 方式发来 ESM 规范的模块请求时,Vite 才开始“「按需编译」”被请求的模块。

相对于 Webpack

Webpack 启动后会做一堆事情,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,在 Node 运行时下性能必然有问题。

继续

调用server.listen()的逻辑在预构建一节中会详细介绍。回到createServer函数,接下来会详细分析下面几个点

  1. 如何获取config配置

  2. 创建一个兼容 rollup 钩子函数的对象container,这个是一个什么对象

  3. 模块图谱实例moduleGraph是什么样子的

如何获取config配置

createServer函数中,调用resolveConfig获取config配置

// inlineConfig 命令行传入的配置 const config = await resolveConfig(inlineConfig, 'serve', 'development') 复制代码

resolveConfig函数也有很多内容,我们分块来看。

export async function resolveConfig(     inlineConfig: InlineConfig,     command: 'build' | 'serve', // 命令     defaultMode = 'development' // 环境   ): Promise<ResolvedConfig> {     let config = inlineConfig // 命令行中的配置项     let configFileDependencies: string[] = []     let mode = inlineConfig.mode || defaultMode        const configEnv = {       mode, // 环境,开发环境下是 development       command // 命令,开发环境下是 serve     }        let { configFile } = config // 配置文件路径          // ... 复制代码

刚进入函数,定义了 5 个变量,后面会用

Vite 是怎么找到配置文件的

继续向下

if (configFile !== false) {     // 查找配置文件并获取配置文件中的 config 配置     const loadResult = await loadConfigFromFile(         configEnv,         configFile, // 命令行传入的配置文件路径         config.root,         config.logLevel     )     if (loadResult) {         // 合并配置         config = mergeConfig(loadResult.config, config)         // 获取配置文件绝对路径         configFile = loadResult.path         // 获取 vite.config.js 中导入的非第三方文件列表(比如自定义插件、方法文件等)         configFileDependencies = loadResult.dependencies     } } 复制代码

loadConfigFromFile函数中,也分为两步

  1. 查找、校验配置文件的路径,并判断文件类型(是不是ts、是不是遵循ESM规范)

  2. 根据路径和类型获取文件内容

获取配置文件路径

// loadConfigFromFile 函数内 let resolvedPath: string | undefined let isTS = false let isMjs = false // 是不是 ESM 规范的文件 let dependencies: string[] = [] try {     // 如果 package.json 中的 type 属性是 module,则说明配置文件遵循 ESM 规范     const pkg = lookupFile(configRoot, ['package.json'])     if (pkg && JSON.parse(pkg).type === 'module') {         isMjs = true     } } catch (e) {} // 如果命令行中指定了配置文件路径,则获取此路径的绝对路径,并判断是不是 ts文件或者遵循 ESM 规范的文件 if (configFile) {     resolvedPath = path.resolve(configFile)     isTS = configFile.endsWith('.ts')     if (configFile.endsWith('.mjs')) {         isMjs = true     } } else {     // 此时没有指定配置文件路径     // 在项目根路径上查找 vite.config.js     const jsconfigFile = path.resolve(configRoot, 'vite.config.js')     // 如果存在,则将路径赋值给 resolvedPath     if (fs.existsSync(jsconfigFile)) {         resolvedPath = jsconfigFile     }     // 和上述逻辑相同,在项目根路径上查找 vite.config.mjs     // 如果找到了,将路径赋值给 resolvedPath。并将 isMjs 置为 true     if (!resolvedPath) {         /* ... */     }     // 和上述逻辑相同,在项目根路径上查找 vite.config.ts     // 如果找到了,将路径赋值给 resolvedPath。并将 isTS 置为 true     if (!resolvedPath) {         /* ... */     } } // 如果没有,抛出异常 if (!resolvedPath) {     debug('no config file found.')     return null } 复制代码

查找过程很简单

  • package.json中判断配置文件是否遵循 ESM 规范

  • 如果指定了配置文件路径,则校验该路径并判断文件类型

  • 如果没有指定配置文件,从项目根目录按顺序查找vite.config.jsvite.config.mjsvite.config.ts;并判断文件类型

找到配置文件后,开始获取配置文件内容

获取配置文件内容

let userConfig: UserConfigExport | undefined // 如果遵循 ESM 规范 if (isMjs) {     const fileUrl = require('url').pathToFileURL(resolvedPath)     // 如果是 ts 文件     if (isTS) {         // 通过 ESbuild 打包文件,第二个参数表示打包后的文件类型,true是遵循ESM规范         const bundled = await bundleConfigFile(resolvedPath, true)         // bundleConfigFile内调用的esbuild 的配置中设置了 metafile: true 用于生成依赖关系         // 并且手写了一个 esbuild 的 plugin,不会将第三方库打包在 bundle 中,即生成的依赖也不会包含第三方库         // 所以这里的 dependencies 内容,只包含用户自己写的文件         dependencies = bundled.dependencies         // 新建 js 文件,并将打包后的代码写入文件中         fs.writeFileSync(resolvedPath + '.js', bundled.code)         // 通过 import() 动态加载刚创建的 js 文件,并获取导出内容         userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`))             .default         // 删除刚创建的文件         fs.unlinkSync(resolvedPath + '.js')     } else {         // 直接动态加载该文件,并获取导出内容         userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`)).default     } } 复制代码

如果明确知道配置文件遵循ESM规范,则通过import()的方式加载文件获取导出内容。对于ts文件通过 ESbuild 打包文件。

还有一种情况就是配置文件是js文件,并且package.json中没有明确指出type: "module",此时这个js文件要么遵循ESM,要么遵循CommonJS。继续看代码

try {     let userConfig: UserConfigExport | undefined     if (isMjs) {         const fileUrl = require('url').pathToFileURL(resolvedPath)         if (isTS) {} else {}     }     // 如果 userConfig 为空,先尝试直接加载文件,假设遵循commonjs     if (!userConfig && !isTS && !isMjs) {         try {             // 清空 require 中的缓存             delete require.cache[require.resolve(resolvedPath)]             // 重新 require             userConfig = require(resolvedPath)         } catch (e) {}     }     // 如果 userConfig 依然没有     // 说明配置文件有几种可能,ts文件、遵循ESM、package.json中设置type是module但是配置文件遵循CommonJS规范     if (!userConfig) {         // 通过 esbuild 打包成 CommonJS,因为不确定该配置文件到底是遵循什么规范         const bundled = await bundleConfigFile(resolvedPath)         // 获取依赖信息         dependencies = bundled.dependencies         // 获取配置         // 这里用这个函数的主要作用是即可以获取遵循ESM规范的导出,又可以获取遵循CommonJS规范的导出         userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)     }     // 如果 配置文件导出的是一个函数,则执行该函数     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,         dependencies, // 自定义组件列表     } } catch (e) {} 复制代码

这里就不过多解释了,注释已经很清楚了。最后就是获取到了配置文件的导出内容,并返回一个对象。要注意下对象内的属性

path: 配置文件路径, config: 配置文件导出内容, dependencies: 非第三方导入(包含用户自定义的插件) 复制代码

到此,已经拿到了配置文件内容,接下来就是需要合并喝规范化配置项,方便后续使用

合并和规范配置项

// 查找配置文件并获取配置文件中的 config 配置 const loadResult = await loadConfigFromFile(/* ... */) if (loadResult) {     // 合并配置     config = mergeConfig(loadResult.config, config)     // 获取配置文件绝对路径     configFile = loadResult.path     // 非第三方导入的文件(比如自定义插件)     configFileDependencies = loadResult.dependencies } 复制代码

调用mergeConfig函数合并命令行配置和vite.config.js的配置。配置合并完成之后,就开始处理配置项

处理配置

plugins

// 获取打包环境 development、production mode = inlineConfig.mode || config.mode || mode configEnv.mode = mode // 根据 apply 属性将当前环境不支持的 plugins 过滤掉 const rawUserPlugins = (config.plugins || []).flat().filter((p) => {     if (!p) {         return false     } else if (!p.apply) {         return true     } else if (typeof p.apply === 'function') {         return p.apply({ ...config, mode }, configEnv)     } else {         return p.apply === command     } }) as Plugin[] 复制代码

自定义插件的apply属性表示在什么环境下执行。上面这段代码的意思是

  • 如果没有apply,表示在开发、生产环境下都会添加到rawUserPlugins等待执行

  • 如果apply是一个函数,函数返回值是true,添加到rawUserPlugins等待执行

  • 如果apply的属性值等于当前环境字符串(servebuild),则添加到rawUserPlugins等待执行

过滤完之后,对rawUserPlugins中所有插件分类,根据enforce的属性值分类,代码如下

//  /**  * 属性值为 pre:表示提前执行的插件,放到 prePlugins 中  * 属性值为 post:表示最后执行的插件,放到 postPlugins 中  * 没有设置或者设置的是其他属性值:表示正常执行的插件,放到 normalPlugins 中  */ const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins) export function sortUserPlugins(plugins) {     const prePlugins: Plugin[] = []     const postPlugins: Plugin[] = []     const normalPlugins: Plugin[] = []     if (plugins) {         plugins.flat().forEach((p) => {             if (p.enforce === 'pre') prePlugins.push(p)             else if (p.enforce === 'post') postPlugins.push(p)             else normalPlugins.push(p)         })     }     return [prePlugins, normalPlugins, postPlugins] } 复制代码

接下来就是执行所有自定义插件的config钩子函数

const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] for (const p of userPlugins) {     // 执行所有自定义插件的 config 钩子函数     // 并传入vite的配置项和 configEnv     // configEnv(对象内部: mode: 'development'|'production', command: 'serve'|'build' )     if (p.config) {         const res = await p.config(config, configEnv)         // 也就是说 config 钩子函数可以修改配置项,并返回新的配置项         // 拿到新的配置项之后,让新的配置项和老的配置项合并         if (res) {             config = mergeConfig(config, res)         }     } } 复制代码

这里有一个需要注意的点就是,config钩子函数可以修改配置项并返回新的配置项,拿到新配置项之后会合并新老配置项

最后会将 Vite 自带的插件和用户自定义的插件合并,后面会说

root 处理

const resolvedRoot = normalizePath(     config.root ? path.resolve(config.root) : process.cwd() ) 复制代码

获取config.root配置项的绝对路径或当前node命令执行时所在的文件夹目录

注意区分一下process.cwd()__dirname

  • process.cwd():指当前node命令执行时所在的文件夹目录

  • __dirname是指被执行js文件所在的文件夹目录

alias

// 创建新的alias // /^[\/]?@vite\/env/ 替换成 'vite/dist/client/env.mjs' // /^[\/]?@vite\/client/ 替换成 'vite/dist/client/client.mjs' const clientAlias = [     { find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },     { find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY }, ] // 将 clientAlias 和 配置项中的 alias 合并并返回 const resolvedAlias = mergeAlias(     clientAlias,     config.resolve?.alias || config.alias || [] ) 复制代码

合并完成之后的alias数据结构和上面的clientAlias一致。

resolve

// 获取 resolve 所有配置项 const resolveOptions: ResolvedConfig['resolve'] = {     dedupe: config.dedupe,     ...config.resolve,     alias: resolvedAlias, } 复制代码

拼接 resolve 配置

.env 文件

  // 如果没有设置 config.envDir 则获取项目根路径 const envDir = config.envDir     ? normalizePath(path.resolve(resolvedRoot, config.envDir))     : resolvedRoot // 获取所有.env 文件中的属性 const userEnv =     inlineConfig.envFile !== false &&     loadEnv(mode, envDir, resolveEnvPrefix(config)) 复制代码

loadEnv函数根据envDir依次查找下面4个文件

  1. .env.development.local

  2. .env.development

  3. .env.local

  4. .env

如果找到了调用dotenv解析该.env文件。

如果设置了config.envPrefix则只获取config.envPrefix前缀的变量。如果没设置config.envPrefix则获取VITE_开头的变量。

其他配置

// 解析 base const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger) // 生产环境相关 const resolvedBuildOptions = resolveBuildOptions(config.build) // 获取 package.json 路径 const pkgPath = lookupFile(resolvedRoot, [`package.json`], true /* pathOnly */) // 获取/设置缓存目录,默认是 node_modules/.vite const cacheDir = config.cacheDir     ? path.resolve(resolvedRoot, config.cacheDir)     : pkgPath && path.join(path.dirname(pkgPath), `node_modules/.vite`) // 指定其他文件类型作为静态资源处理(这样导入它们就会返回解析后的 URL) const assetsFilter = config.assetsInclude     ? createFilter(config.assetsInclude) // 构造一个过滤函数,该函数可用于确定是否应该对某些模块进行操作     : () => false // 创建在特殊场景中使用的内部解析器 // 比如,预构建时,用于解析路径 const createResolver: ResolvedConfig['createResolver'] = (options) => {     let aliasContainer: PluginContainer | undefined     let resolverContainer: PluginContainer | undefined     return async (id, importer, aliasOnly, ssr) => {} } const { publicDir } = config // 获取静态资源地址 const resolvedPublicDir =     publicDir !== false && publicDir !== ''         ? path.resolve(               resolvedRoot,               typeof publicDir === 'string' ? publicDir : 'public'           )         : '' 复制代码

上述配置处理完成之后,创建resolved对象,并拼接配置

resolved对象

const resolved: ResolvedConfig = {     ...config,     configFile: configFile ? normalizePath(configFile) : undefined, // 配置文件路径     configFileDependencies, // vite.config.js 中非第三方包的导入,比如自定义插件     inlineConfig, // 命令行中的配置     root: resolvedRoot, // 项目根目录     base: BASE_URL, // 公共基础路径, /my-app/index.html     resolve: resolveOptions, // 文件解析时的相关配置     publicDir: resolvedPublicDir, // 静态资源服务的文件夹     cacheDir, // 缓存目录,默认 node_modules/.vite     command,  // serve | build     mode,     // development | production     isProduction, // 是否是生产环境     plugins: userPlugins, // 自定义 plugins     server: resolveServerOptions(resolvedRoot, config.server),     build: resolvedBuildOptions,     env: {         ...userEnv, // .env 文件         BASE_URL,         MODE: mode,         DEV: !isProduction,         PROD: isProduction,     },     assetsInclude(file: string) {         // 一个函数,用于获取传入的 file 是否能作为静态资源处理,如果能,导入它们就会返回解析后的 URL         return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)     },     logger,     createResolver, // 特殊场景中使用的内部解析器,预构建文件中会说     optimizeDeps: {         ...config.optimizeDeps,         esbuildOptions: { // esbuild 配置             keepNames: config.optimizeDeps?.keepNames,             preserveSymlinks: config.resolve?.preserveSymlinks,             ...config.optimizeDeps?.esbuildOptions,         },     }, } 复制代码

这个resolved对象就是最后处理完的所有配置。最后resolveConfig函数也会返回这个resolved对象。

从上面resolved.optimizeDeps.esbuildOptions可以看出,如果想配置esbuild的配置项,可以通过下面的方式

{     resolve: {         preserveSymlinks: boolean     }     optimizeDeps: {         keepNames: boolean         esbuildOptions: {}     } } 复制代码

Vite 自带的插件和用户自定义的插件合并

处理插件的时候说过最后还会整合Vite自带的插件,代码如下

// 将vite自带插件和用户定义插件安顺序组合,并返回 ;(resolved.plugins as Plugin[]) = await resolvePlugins(     resolved,     prePlugins,     normalPlugins,     postPlugins ) // 只包含开发环境中使用的插件 export async function resolvePlugins(     config: ResolvedConfig,     prePlugins: Plugin[],     normalPlugins: Plugin[],     postPlugins: Plugin[] ): Promise<Plugin[]> {     return [         preAliasPlugin(),         aliasPlugin({ entries: config.resolve.alias }),         ...prePlugins, // 自定义前置插件         config.build.polyfillModulePreload             ? modulePreloadPolyfillPlugin(config)             : null,         resolvePlugin({             ...config.resolve,             root: config.root,             isProduction: config.isProduction,             ssrConfig: config.ssr,             asSrc: true,         }),         htmlInlineScriptProxyPlugin(config),         cssPlugin(config),         config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,         jsonPlugin(             {                 namedExports: true,                 ...config.json,             }         ),         wasmPlugin(config),         webWorkerPlugin(config),         assetPlugin(config),         ...normalPlugins, // 自定义插件         definePlugin(config),         cssPostPlugin(config),         ...postPlugins // 自定义后置插件 } 复制代码

所有插件拼接好之后,调用所有自定义插件configResolved钩子函数

await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))


作者:zygg不含糖
链接:https://juejin.cn/post/7044086324306903048


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐