《Vue.js 设计与实现》—— 02 框架设计核心要素

news2024/12/24 8:20:13

框架设计并非仅仅实现功能那么简单,里面有很多学问。例如:

  • 框架应该给用户提供哪些构建产物?产物的模块格式如何?

  • 当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验,让用户快速定位问题?

  • 开发版本和生产版本的构建有何区别?

  • 热更新(hot module replacement,HMR)需要框架层面的支持,是否也应该考虑?

  • 当框架提供了多个功能,而用户只需要其中几个功能时,用户能否选择关闭其他功能从而减少最终资源的打包体积?

1. 提升开发体验

衡量一个框架是否足够优秀的指标之一就是看它的开发体验如何,以 Vue.js 3 为例:

createApp(App).mount('#not-exist')

当创建一个组件并试图将其挂载到一个不存在的 DOM 节点时,就会收到一条警告信息:

[Vue warn]: Failed to mount app: mount target selector "#not-exist" returned null.

这条信息让我们能够清晰且快速地定位问题。如果 Vue.js 内部不做任何处理,那么很可能得到的是 JavaScript 层面的错误信息,如 Uncaught TypeError: Cannot read property 'xxx' of null,而根据此信息很难知道问题所在。

因此,在框架设计和开发过程中,提供友好的警告信息至关重要。始终提供友好的警告信息不仅能够帮助用户快速定位问题,节省用户的时间,还能够让框架收获良好的口碑,让用户认可框架的专业性。

在 Vue.js 的源码中,经常能够看到 warn 函数的调用,例如:

warn(`Failed to mount app: mount target selector "${container}" returned null.`)

除了提供必要的警告信息外,还有很多其他方面可以作为切入口,进一步提升用户的开发体验。例如,在 Vue.js 3 中,当在控制台打印一个 ref 数据时:

const count = ref(0)
console.log(count)

打印结果是一个 ref 对象,很不直观,但调用 count.value 后,得到的就是响应式对象的值,变得非常直观。

那么有没有办法在直接打印 count 时让输出的信息更友好呢?当然可以,浏览器允许我们编写自定义的 formatter,从而自定义输出形式。

在 Vue.js 3 的源码中,有一个名为 initCustomFormatter 的函数,用来在开发环境下初始化自定义 formatter。

以 Chrome 为例,打开 DevTools 的设置,然后勾选 “Console” -> “Enable custom formatters” 选项,如下:

然后刷新浏览器并查看控制台,会发现输出内容变得非常直观,如下:

2. 控制代码体积

框架的大小也是衡量框架的标准之一。在实现同样功能的情况下,代码越少越好,这样体积就会越小,最后浏览器加载资源的时间也就越少。前面说到,框架提供越完善的警告信息越好,但这意味着要编写更多的代码,那么如何在这个基础上实现代码体积的控制呢?

如果去看 Vue.js 3 的源码,就会发现每一个 warn 函数的调用都会配合 __DEV__ 常量的检查,例如:

if (__DEV__ && !res) { // 打印警告信息的前提时 __DEV__ 为 true
  warn(
    `Failed to mount app: mount target selector "${container}" returned null.`
  )
}

Vue.js 使用 rollup.js 对项目进行构建,这里的 __DEV__ 常量实际上是通过 rollup.js 的插件配置来预定义的,其功能类似于 webpack 中的 DefinePlugin 插件。

Vue.js 在输出资源的时候,会输出两个版本,其中一个用于开发环境,如 vue.global.js,另一个用于生产环境,如 vue.global.prod.js。

当 Vue.js 构建用于开发环境的资源时,会把 __DEV__ 常量设置为 true;当构建生产环境的资源时,会把 __DEV__ 常量设置为 false

因为生产环境下判断条件始终为假,这段永远不会执行的代码称为 dead code,它不会出现在最终产物中,在构建资源时就会被移除。这样就做到了在开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积。

3. 良好的 Tree-Shaking

仅仅通过 __dev__ 变量控制代码量是远远不够的。还以 Vue.js 为例,其内建了很多组件,例如 <Transition> 组件,如果项目中没有用到该组件,其对应的代码就不需要也不应该包含在最终的构建资源中。那么如何做到这一点呢?答案就是 Tree-Shaking

在前端领域,这个概念是由 rollup.js 普及的。简单地说,Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是排除 dead code,现在无论是 rollup.js 还是webpack,都支持 Tree-Shaking

想要实现 Tree-Shaking,必须满足一个条件,即模块必须是 ESM(ES Module),因为 Tree-Shaking 依赖 ESM 的静态结构。以 rollup.js 为例看看 Tree-Shaking 如何工作,其目录结构如下:

- demo
	- package.json
	- input.js
	- utils.js

首先安装 rollup.js:

yarn add rollup -D # 或 npm install rollup -D

input.js 和 utils.js 文件的内容如下:

// input.js
import { foo } from './utils.js'
foo()

// utils.js
export function foo(obj) {
    obj && obj.foo
}
export function bar(obj) { // bar 函数未被使用
    obj && obj.bar
}

接着,执行如下命令进行构建:

npx rollup input.js -f esm -o bundle.js

构建后,输出的 bundle.js 的内容为:

export function foo(obj) {
    obj && obj.foo
}
foo()

这说明 Tree-Shaking 起了作用,我们并没有使用 bar 函数,因此它作为 dead code 被删除了。但是仔细观察会发现,foo 函数的执行也没有什么意义,仅仅是读取了对象的值,所以它的执行似乎没什么必要。既然把这段代码删了也不会对程序产生影响,那为什么 rollup.js 不把这段代码也作为 dead code 移除呢?

这涉及 Tree-Shaking 中的第二个关键点 —— 副作用。如果一个函数调用会产生副作用,那么就不能将其移除

简单地说,副作用就是,当调用函数的时候会对外部产生影响,例如修改了全局变量。

但是,上面的代码只读取对象的值,怎么会产生副作用呢?其实是有可能的,如果 obj 对象是一个通过 Proxy 创建的代理对象,那么读取对象属性时,就会触发代理对象的 get 夹子(trap),在 get 夹子中是可能产生副作用的。至于到底会不会产生副作用,只有代码真正运行的时候才能知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 很有难度。

即然静态地分析代码很困难,所以像 rollup.js 这类工具都会提供一个机制,让我们手动明确地告诉 rollup.js 该段代码是一个纯函数,不会产生副作用,可以移除它。具体实现如下:

import {foo} from './utils'

/*#__PURE__*/ foo() // 前面的 __PURE__ 告知是一个纯函数,不会产生副作用,可以移除

此时再次执行构建命令并查看 bundle.js 文件,就会发现它的内容是空的。

因此,我们在编写框架的时候需要合理使用 /*#__PURE__* 注释。Vue.js 3 的源码里面大量使用了该注释。

那么,这会不会对编写代码造成很大的心智负担呢?其实不会,因为通常产生副作用的代码都是模块内函数的顶级调用。

什么是顶级调用呢?如下:

foo() // 顶级调用

function bar() {
    foo() // 函数内调用 -- 没有副作用,除非 bar() 顶级调用
}

/*#__PURE__*/ 注释不仅仅作用于函数,它可以应用于任何语句上。该注释也不是只有 rollup.js 才能识别,webpack 以及压缩工具(如 terser)都能识别它。

4. 输出构建产物

前面说到 Vue.js 会为开发环境和生产环境输出不同的包,如 vue.global.js 用于开发环境,它包含必要的警告信息,而 vue.global.prod.js 用于生产环境,不包含警告信息。实际上,Vue.js 的构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物。

不同类型的产物一定有对应的需求背景,因此需要从需求讲起。首先我们希望用户可以直接在 HTML 页面中使用 <script> 标签引入框架并使用:

<body>
  <script src="/path/to/vue.js"></script>
    <script>
    const { createApp } = Vue
    // ...
  </script>
</body>

为了实现这个需求,需要输出一种叫作 IIFE 格式的资源。IIFE 的全称是 Immediately Invoked Function Expression,即“立即调用的函数表达式”,易于用 JavaScript 来表达:

(function () {
    // ...
})

如以上代码所示,这是一个立即执行的函数表达式。实际上,vue.global.js 文件就是 IIFE 形式的资源,它的代码结构如下所示:

var Vue = (function(exports){
  // ...
  exports.createApp = createApp;
  // ...
  return exports
}({}))

这样当我们使用 <script> 标签直接引入 vue.global.js 文件后,全局变量 Vue 就是可用的了。在 rollup.js 中,可以通过配置 format: 'iife' 来输出这种形式的资源:

// rollup.config.js
const config = {
  input: 'input.js',
  output: {
    file: 'output.js',
    format: 'iife' // 指定模块形式
  }
}

export default config

随着技术的发展和浏览器的支持,现在主流浏览器对原生 ESM 的支持都不错,所以用户除了能够使用 <script> 标签引用 IIFE 格式的资源外,还可以直接引入 ESM 格式的资源,例如 Vue.js 3 还会输出 vue.esm-browser.js 文件,用户可以直接用 <script type="module"> 标签引入:

<script type="module" src="/path/to/vue.esm-browser.js"></script>

为了输出 ESM 格式的资源,rollup.js 的输出格式需要配置为:format: 'esm'

为什么 vue.esm-browser.js 文件中会有 -browser 字样?其实对于 ESM 格式的资源来说,Vue.js 还会输出一个 vue.esm-bundler.js 文件,其中 -browser 变成了 -bundler。为什么这么做呢?我们知道,无论是 rollup.js 还是 webpack,在寻找资源时,如果 package.json 中存在 module 字段,那么会优先使用 module 字段指向的资源来代替 main 字段指向的资源。

可以打开 Vue.js 源码中的 packages/vue/package.json 文件看一下:

{
  "main": "index.js",
  "module": "dist/vue.runtime.esm-bundler.js",
}

其中 module 字段指向的是 vue.runtime.esm-bundler.js 文件,意思是说,如果项目是使用webpack 构建的,那么你使用的 Vue.js 资源就是 vue.runtime.esm-bundler.js,也就是说,带有 -bundler 字样的 ESM 资源是给 rollup.js 或 webpack 等打包工具使用的,而带有 -browser 字样的 ESM 资源是直接给 <script type="module"> 使用的。它们之间有何区别?这就不得不提到上文中的 __DEV__ 常量。当构建用于 <script> 标签的 ESM 资源时,如果是用于开发环境,那么 __DEV__ 会设置为 true;如果是用于生产环境,那么 __DEV__ 常量会设置为 false,从而被 Tree-Shaking 移除。但是当我们构建提供给打包工具的 ESM 格式的资源时,不能直接把 __DEV__ 设置为 truefalse,而要使用(process.env.NODE_ENV !== 'production')替换 __DEV__ 常量。例如下面的源码:

if (__DEV__) {
  warn(`useCssModule() is not supported in the global build.`)
}

在带有 -bundler 字样的资源中会变成:

if ((process.env.NODE_ENV !== 'production')) {
  warn(`useCssModule() is not supported in the global build.`)
}

这样做的好处是,用户可以通过 webpack 配置自行决定构建资源的目标环境,但是最终效果其实一样,这段代码也只会出现在开发环境中。用户除了可以直接使用 <script> 标签引入资源外,我们还希望用户可以在 Node.js 中通过 require 语句引用资源,例如:

const Vue = require('vue')

为什么会有这种需求呢?因为当进行服务端渲染时,Vue.js 的代码是在 Node.js 环境中运行的。在 Node.js 环境中,资源的模块格式应该是 CommonJS,简称 cjs。为了能够输出 cjs 模块的资源,可以通过修改 rollup.config.js 的配置 format: 'cjs' 来实现:

5. 特征开关

在设计框架时,框架会给用户提供诸多特性(或功能),例如提供 A、B、C 三个特性给用户,同时还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为 truefalse 来代表开启或关闭对应的特性,这将会带来很多益处,如:

  • 对于用户关闭的特性,可以利用 Tree-Shaking 机制让其不打包在最终的资源中。
  • 该机制为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性,而不用担心资源体积变大。
  • 当框架升级时,也可以通过特性开关来支持遗留 API,这样新用户可以选择不使用遗留 API,从而使最终打包的资源体积最小化。

那如何实现特性开关呢?其原理和前面提到的 __DEV__ 常量一样,本质上是利用 rollup.js 的预定义常量插件来实现。拿 Vue.js 3 源码中的一段 rollup.js 配置来说:

{
  __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
}

其中 __FEATURE_OPTIONS_API__ 类似于 __DEV__。在 Vue.js 3 的源码中搜索,可以找到很多类似于如下代码的判断分支:

// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

当 Vue.js 构建资源时,如果构建的资源是供打包工具使用的(即带有 -bundler 字样的资源),那么上面的代码在资源中会变成:

// support for 2.x options
if (__VUE_OPTIONS_API__) { // 这里不一样
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

其中 __VUE_OPTIONS_API__ 是一个特性开关,用户可以通过设置 __VUE_OPTIONS_API__ 预定义常量的值来控制是否要包含这段代码。通常用户可以使用 webpack.DefinePlugin 插件来实现:

// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
  __VUE_OPTIONS_API__: JSON.stringify(true) // 开启特性
})

最后解释一下 __VUE_OPTIONS_API__ 开关有什么用。在 Vue.js 2 中,我们编写的组件叫作Options API;在 Vue.js 3 中,推荐使用 Composition API 来编写代码。为了兼容 Vue.js 2,在 Vue.js 3 中仍然可以使用 Options API 的方式编写代码。但是如果明确知道自己不会使用选项 API,用户就可以使用 __VUE_OPTIONS_API__ 开关来关闭该特性,这样在打包的时候 Vue.js 的这部分代码就不会包含在最终的资源中,从而减小资源体积。

6. 错误处理

错误处理是框架开发过程中非常重要的环节。框架错误处理机制的好坏直接决定了用户应用程序的健壮性,还决定了用户开发时处理错误的心智负担。

假设我们开发了一个工具模块,代码如下:

// utils.js
export default {
    foo(fn) {
        fn && fn()
    }
}

该模块导出一个对象,其中 foo 属性是一个函数,接收一个回调函数作为参数,调用 foo 函数时会执行该回调函数,在用户侧使用时:

import utils from 'utils.js'
utils.foo( () => {
    // ...
})

如果用户提供的回调函数在执行的时候出错了,怎么办?有两个办法,第一个办法是让用户自行处理,这需要用户自己执行 try ... catch

import utils from 'utils.js'
utils.foo( () => {
    try {
        // ...
    } catch(e) {
        // ...
    }
})

但是这会增加用户的负担。如果 utils.js 提供了几十上百个类似的函数,那么用户在使用的时候就需要逐一添加错误处理程序。

第二个办法是我们代替用户统一处理错误,如以下代码所示:

// utils.js
export default {
    foo(fn) {
        try {
            fn && fn()
        } catch(e) {
            /* ... */
        }
    },
    bar(fn) {
        try {
            fn && fn()
        } catch(e) {
            /* ... */
        }
    },
    // ...
}

事实上,可以进一步将错误处理程序封装在一个函数上,假设称为 callWithErrorHandling

// utils.js
export default {
    foo(fn) {
        callWithErrorHandling(fn)
    },
    bar(fn) {
        callWithErrorHandling(fn)
    },
    // ...
}

function callWithErrorHandling(fn) {
    try {
        fn && fn()
    } catch(e) {
        /* ... */
    } 
}

简洁还不是封装函数的主要目的,我们能为用户提供统一的错误处理接口,如:

// utils.js
let handleError = null

export default {
  foo(fn) {
    callWithErrorHandling(fn)
  },
  // 用户可以调用该函数注册统一的错误处理函数
  registerErrorHandler(fn) {
    handleError = fn
  }
}

function callWithErrorHandling(fn) {
  try {
    fn && fn()
  } catch (e) {
    // 将捕获到的错误传递给用户的错误处理程序
    handleError(e)
  }
}

这样用户侧的代码就会非常简洁且健壮:

import utils from 'utils.js'

// 注册错误处理程序
utils.registerErrorHandler((e) => {
  console.log(e)
})
utils.foo(() => {/*...*/})
utils.bar(() => {/*...*/})

这时错误处理的能力完全由用户控制,用户既可以选择忽略错误,也可以调用上报程序将错误上报给监控系统。实际上,这就是 Vue.js 错误处理的原理,可以在源码中搜索到 callWithErrorHandling 函数。另外,在 Vue.js 中,也可以注册统一的错误处理函数:

import App from 'App.vue'

const app = createApp(App)
app.config.errorHandler = () => {
  // 错误处理程序
}

7. 良好的 TS 支持

TypeScript 是由微软开源的编程语言,简称 TS,它是 JavaScript 的超集,能够为 JavaScript 提供类型支持。使用 TS 的好处有很多,如代码即文档、编辑器自动提示、一定程度上能够避免低级 bug、代码的可维护性更强等。因此对 TS 类型的支持是否完善也成为评价一个框架的重要指标。

如何衡量一个框架对 TS 类型支持的水平呢?这里有一个常见的误区,很多人以为只要是使用 TS 编写框架,就等价于对 TS 类型支持友好,其实这两种完全不同。

举例来说。下面是使用 TS 编写的函数:

function foo(val: any) {
    return val
}

这个函数直接将参数作为返回值,这说明返回值的类型是由参数决定的,如果参数是 number 类型,那么返回值也是 number 类型。但是,假设有下面的代码:

const res = foo('str') // 参数为字符串类型,理论上 res 也为字符串类型,但是却推断成了 any 类型

为了达到理想状态,只需要对 foo 函数做简单的修改即可:

function foo<T extends any>(val: T): T {
    return val
}

const res = foo('str') // 这时就会将 res 推断为 "str" 字符串字面量了

通过这个例子可以认识到,使用 TS 编写代码与对 TS 类型支持友好是两件事。在编写大型框架时,想要做到完善的 TS 类型支持很不容易,可以查看 Vue.js 源码中的 runtime-core/src/apiDefineComponent.ts 文件,整个文件里真正会在浏览器中运行的代码其实只有 3 行,但是全部的代码接近 200 行,其实这些代码都是在为类型支持服务。由此可见,框架想要做到完善的类型支持,需要付出相当大的努力。

更多文章可关注:GopherBlog、GopherBlog副站

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

优化性能测试分析:如何科学利用CPU异常曲线

性能测试为保证软件质量起到重要作用&#xff0c;对于交易量较大的应用系统&#xff0c;性能测试更是一个必不可少的环节。 测试人员通常通过监测响应时间、吞吐量、应用服务器和数据库服务器的CPU及内存来衡量系统的性能是否达标&#xff0c;那么&#xff0c;在性能测试过程中…

LabVIEWCompactRIO 开发指南13 网络发布的共享变量

LabVIEWCompactRIO 开发指南13 网络发布的共享变量 跨网络共享标签的一种方法是网络共享变量。术语网络变量是指网络上可以在程序、应用程序、远程计算机和硬件之间进行通信的软件项。网络共享变量非常适合1:N或N:1设置&#xff0c;因为它们有一个内置的连接管理器来管理传入…

《Netty》从零开始学netty源码(五十六)之RecvByteBufAllocator

RecvByteBufAllocator 在创建channel的过程中会创建一个相应的配置类&#xff0c;该类存储了一些关于channel的属性&#xff0c;包括分配内存的ByteBufAllocator和预估大小的RecvByteBufAllocator&#xff0c;通过前面的学习我们知道ByteBufAllocator分配内存的时候最终会委托…

Consensus洞察|2023,Web3“脱虚向实”元年

前言 2023年对于Web3来说&#xff0c;是一个被推到主流社会前台的关键时期。 出品&#xff5c;欧科云链研究院 作者&#xff5c;毕良寰 Web3作为新兴科技&#xff0c;其发展路径在近几年尤为艰难&#xff0c;充斥着“丑闻”的2022年&#xff0c;以Luna/UST的崩溃为起点开启了…

c++类与对象(二)——赋值运算符重载与取地址操作符重载

文章目录 一.运算符重载1.运算符重载的概念2.实现Date类&#xff08;1&#xff09;> < > < ! 重载&#xff08;2&#xff09; - - 重载&#xff08;3&#xff09;前置与后置重载&#xff08;4&#xff09;日期-日期的实现&#xff08;5&#xff09;<< 与 &g…

代码随想录之额外题目

数组 1207 独一无二的出现次数 看数组的大小和长度都没有很大&#xff0c;所以可以直接用数组来做哈希表&#xff0c;用一个数组来记录出现次数&#xff0c;再用一个数组来标记出现次数的值是否出现过。就是O(n) class Solution {public boolean uniqueOccurrences(int[] arr…

Spring IOC:IOC在Spring底层中如何实现?

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 操作系统&#xff1a;win10 x64 位 家庭版 Maven版本&#xff1a;apache-maven-3.6.3 Mybatis版本&#xff1a;3.5.6 spring版本&#xff1a;5.3.1 文章目录 Spring系列专栏文章目录一. 什么是IOC?二. IOC在spring中的实现2.1…

java 基础

第一章 计算机认识 1 概述 计算机包括**硬件&#xff08;hardware&#xff09;和软件&#xff08;software&#xff09;**两部分。硬件包括计算机可以看得见的物理部分&#xff0c;而软件提供看不见的指令。 2 计算机硬件介绍 3 计算机硬件——中央处理器 中央处理器&#xff0…

PyQGIS 加载单个shp文件到图层面板

打开QGIS Desktop 3.22.16&#xff0c;点击菜单栏 【设置】——>【Python控制台】 在Python控制台中点击【显示编辑器】按钮&#xff0c;打开Python编辑器 点击第一个按钮 【打开脚本文件】&#xff0c;选择加载图层列表到图层面板源码 # 加载图层列表到图层面板中 from qgi…

DataGridXL中快速搜索单元格和底部全屏模式区域隐藏

DataGridXL表格是在2020年发布&#xff0c;DataGridXL在设计时就考虑到了性能。提供最快、最简单、最可靠的数据网格。DataGridXL支持所有常用所有的浏览器&#xff0c;为 Web 应用程序提供类似于 Microsoft Excel 的体验&#xff0c;它支持前端框架有Vue、React、Angular等。 …

Acid burn(★★)

运行程序 先是弹出一个neg 然后是真正的程序界面 有一个输入Serial和Name的判断 还有一个只输入Serial的判断 查壳 没有壳&#xff0c;是Delphi程序 先除去一个Neg 找到Neg弹出的程序&#xff0c;在程序头下个断&#xff0c;运行程序&#xff0c;此时栈顶是调用此功能的…

希亦、米家、必胜家用洗地机测评,洗地机十年老用户告诉你哪款好用

如今&#xff0c;各种清洁设备早已进入我们的生活。其中&#xff0c;各种扫地机和洗地机更是出现在各大商场、酒店、餐饮、医院等领域。 洗地机是一种集洗地、吸尘于一体的清洗设备&#xff0c;它适合清扫如粉尘、烟头、纸屑、厨余、毛发等垃圾。它的工作效率高&#xff0c;可…

如何解决 :libstdc++.so.6: version `GLIBCXX_3.4.30‘ not found

在使用 python 以下的命令时&#xff0c; from scipy.signal import convolve出现报错&#xff1a; /home/anaconda3/envs/norm/lib/python3.9/site-packages/scipy/linalg/../../../../libstdc.so.6: version GLIBCXX_3.4.30 not found (required by /home/anaconda3/envs/no…

上海车展有哪些让人过目不忘的电驱技术?

前言 2023年的第二十届上海国际汽车工业展览会&#xff08;以下简称“上海车展”&#xff09;中&#xff0c;扁线、SiC、800V、油冷成为了大多数车企电驱动力总成的标配。下面选取几家非常有特色的电驱技术进行解析&#xff0c;并探讨下电驱技术的发展方向。 舍弗勒对标件电驱…

前几天面了个32岁的测试员,年薪50w问题基本都能回答上,应该刷了不少八股文···

互联网行业竞争是一年比一年严峻&#xff0c;作为测试工程师的我们唯有不停地学习&#xff0c;不断的提升自己才能保证自己的核心竞争力从而拿到更好的薪水&#xff0c;进入心仪的企业&#xff08;阿里、字节、美团、腾讯等大厂.....&#xff09; 所以&#xff0c;大家就迎来了…

2023.05.09-使用AI克隆孙燕姿的声音来进行唱歌

文章目录 1. 简介 2. 资源合集 3. 准备数据集3.1. 人声分离3.2. 音频进行切片化处理3.2.1. 3.3. 数据集存放格式要求 4. 训练4.1. 启动web UI.bat 4.2. 识别数据集4.3. 数据预处理4.4. 设置训练超参数4.4.1. 选择模型分支 4.5. 进行训练4.5.1. 关于显存的说明 5. 推理5.1. 加载…

从bootanimation出发分析OpenHarmony下Gralloc buffer管理机制

从bootanimation出发分析OpenHarmony下Gralloc buffer管理机制 引言 这个文档主要记录从bootanimation角度出发&#xff0c;分析OpenHarmony下对gralloc buffer的管理&#xff01;由于OpenHarmony图形子系统过于复杂&#xff0c;且个人由于能力有限&#xff0c;这里我仅从grall…

玩机搞机--定制系统 隐藏app桌面图标 反编译app【二】

在定制rom的过程中。客户要求内置某些app。个别需求内置的app不能显示在桌面&#xff0c;那么对应的内置app方法就不做细阐述。可以参考原固件操作 那么今天主要讲下隐藏app的操作解析 &#x1f494;&#x1f494;&#x1f494;&#x1f494;&#x1f494;&#x1f494;&#…

C++知识点 -- C++的类型转换

C知识点 – C的类型转换 文章目录 C知识点 -- C的类型转换一、C语言中的类型转换二、C的强制类型转换1.static_cast2.reinterpret_cast3.const_cast4.dynamic_cast 一、C语言中的类型转换 void test() {int i 0;//隐式类型转换&#xff08;意义相近的类型&#xff09;double …

只下载rpm包而不安装(用于内网虚拟机使用)

这里写目录标题 问题&#xff1a;解决&#xff1a;1. 安装yum-utils2. 下载rpm包3. 将rpm包拷贝到离线的虚拟机并安装 问题&#xff1a; 公司虚拟机仅使用内网无法通过yum下载依赖&#xff1b; 解决方法之一就是从一台连了公网的虚拟机(NAT) 下载需要的rpm包&#xff0c;然后…