框架设计的核心要素
我们的框架应该给用户提供哪些构建产物?产物的模块格式如何?当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验,让用户快速定位问题?开发版本的构建和生产版本的构建有何区别?热更新(hot module replacement,HMR)需要框架层面的支持,我们是否也应该考虑?另外,当你的框架提供了多个功能,而用户只需要其中几个功能时,用户能否选择关闭其他功能从而减少最终资源的打包体积?
1、提升用户的开发体验
衡量一个框架是否足够优秀的指标之一就是看它的开发体验如何,这里我们拿 Vue.js 3 举个例子:
01 createApp(App).mount('#not-exist')
这条信息告诉我们挂载失败了,并说明了失败的原因:Vue.js 根据我们提供的选择器无法找到相应的DOM 元素(返回 null)。这条信息让我们能够清晰且快速地定位问题。试想一下,如果 Vue.js 内部不做任何处理,那么我们很可能得到的是JavaScript 层面的错误信息,例如 Uncaught TypeError: Cannot read property ‘xxx’ of null,而根据此信息我们很难知道问题出在哪里。
在 Vue.js 的源码中,我们经常能够看到 warn 函数的调用:
01 warn(
02 `Failed to mount app: mount target selector "${container}" returned null.`
03 )
对于 warn 函数来说,由于它需要尽可能提供有用的信息,因此它需要收集当前发生错误的组件栈信息。如果你去看源码,就会发现有些复杂,但其实最终就是调用了 console.warn 函数。
除了提供必要的警告信息外,还有很多其他方面可以作为切入口,进一步提升用户的开发体验。例如,在 Vue.js 3 中,当我们在控制台打印一个 ref 数据时:
01 const count = ref(0)
02 console.log(count)
打开控制台查看输出:
可以发现,打印的数据非常不直观。当然,我们可以选择直接打印 count.value 的值,这样就只会输出 0,非常直观。那么有没有办法在打印 count 的时候让输出的信息更友好呢?当然可以,浏览器允许我们编写自定义的 formatter,从而自定义输出形式。在 Vue.js 3 的源码中,你可以搜索到名为 initCustomFormatter 的函数,该函数就是用来在开发环境下初始化自定义 formatter 的。以Chrome 为例,我们可以打开 DevTools 的设置,然后勾选“Console”→“Enable custom formatters”选项:
然后刷新浏览器并查看控制台,会发现输出内容变得非常直观:
2、控制框架代码的体积
框架的大小也是衡量框架的标准之一。在实现同样功能的情况下,当然是用的代码越少越好,这样体积就会越小,最后浏览器加载资源的时间也就越少。这时我们不禁会想,提供越完善的警告信息就意味着我们要编写更多的代码,这不是与控制代码体积相悖吗?没错,所以我们要想办法解决这个问题。
如果我们去看 Vue.js 3 的源码,就会发现每一个warn 函数的调用都会配合 DEV 常量的检查,例如:
01 if (__DEV__ && !res) {
02 warn(
03 `Failed to mount app: mount target selector "${container}" returned null.`
04 )
05 }
可以看到,打印警告信息的前提是:DEV 这个常量一定要为 true,这里的 DEV 常量就是达到目的的关键。
Vue.js 使用 rollup.js 对项目进行构建,这里的__DEV__ 常量实际上是通过 rollup.js 的插件配置来预定义的,其功能类似于 webpack 中的DefinePlugin 插件。
Vue.js 在输出资源的时候,会输出两个版本,其中一个用于开发环境,如 vue.global.js,另一个用于生产环境,如 vue.global.prod.js,通过文件名我们也能够区分。
当 Vue.js 构建用于开发环境的资源时,会把__DEV__ 常量设置为 true,这时上面那段输出警告信息的代码就等价于:
01 if (true && !res) {
02 warn(
03 `Failed to mount app: mount target selector "${container}" returned null.`
04 )
05 }
可以看到,这里我们把 DEV 常量替换成字面量 true,所以这段代码在开发环境中是肯定存在的。
当 Vue.js 用于构建生产环境的资源时,会把__DEV__ 常量设置为 false,这时上面那段输出警告信息的代码就等价于:
01 if (false && !res) {
02 warn(
03 `Failed to mount app: mount target selector "${container}" returned null.`
04 )
05 }
可以看到,DEV 常量替换为字面量 false,这时我们发现这段分支代码永远都不会执行,因为判断条件始终为假,这段永远不会执行的代码称为dead code,它不会出现在最终产物中,在构建资源的时候就会被移除,因此在 vue.global.prod.js 中是不会存在这段代码的。
这样我们就做到了在开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积。
3、框架要做到良好的 Tree-Shaking
上文提到通过构建工具设置预定义的常量__DEV__,就能够在生产环境中使得框架不包含用于打印警告信息的代码,从而使得框架自身的代码量不随警告信息的增加而增加。但是从用户的角度来看,这么做仍然不够,还是拿 Vue.js 来举个例子。我们知道 Vue.js 内建了很多组件,例如<Transition>
组件,如果我们的项目中根本就没有用到该组件,那么它的代码需要包含在项目最终的构建资源中吗?答案是“当然不需要”,那么如何做到这一点呢?这就不得不提到本节的主角Tree-Shaking。
什么是 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 如何工作,其目录结构如下:
01 ├── demo
02 │ └── package.json
03 │ └── input.js
04 │ └── utils.js
首先安装 rollup.js:
01 yarn add rollup -D
02 # 或者 npm install rollup -D
下面是 input.js 和 utils.js 文件的内容:
01 // input.js
02 import { foo } from './utils.js'
03 foo()
04 // utils.js
05 export function foo(obj) {
06 obj && obj.foo
07 }
08 export function bar(obj) {
09 obj && obj.bar
10 }
代码很简单,我们在 utils.js 文件中定义并导出了两个函数,分别是 foo 函数和 bar 函数,然后在input.js 中导入了 foo 函数并执行。注意,我们并没有导入 bar 函数。
接着,我们执行如下命令进行构建:
01 npx rollup input.js -f esm -o bundle.js
这句命令的意思是,以 input.js 文件为入口,输出ESM,输出的文件叫作 bundle.js。命令执行成功后,我们打开 bundle.js 来查看一下它的内容:
01 // bundle.js
02 function foo(obj) {
03 obj && obj.foo
04 }
05 foo();
可以看到,其中并不包含 bar 函数,这说明 Tree-Shaking 起了作用。由于我们并没有使用 bar 函数,因此它作为 dead code 被删除了。但是仔细观察会发现,foo 函数的执行也没有什么意义,仅仅是读取了对象的值,所以它的执行似乎没什么必要。既然把这段代码删了也不会对我们的应用程序产生影响,那么为什么 rollup.js 不把这段代码也作为 dead code 移除呢?
这就涉及 Tree-Shaking 中的第二个关键点——副作用。如果一个函数调用会产生副作用,那么就不能将其移除。什么是副作用?简单地说,副作用就是,当调用函数的时候会对外部产生影响,例如修改了全局变量。这时你可能会说,上面的代码明显是读取对象的值,怎么会产生副作用呢?其实是有可能的,试想一下,如果 obj 对象是一个通过Proxy 创建的代理对象,那么当我们读取对象属性时,就会触发代理对象的 get 夹子(trap),在get 夹子中是可能产生副作用的,例如我们在 get 夹子中修改了某个全局变量。而到底会不会产生副作用,只有代码真正运行的时候才能知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 很有难度,上面只是举了一个简单的例子。
因为静态地分析 JavaScript 代码很困难,所以像rollup.js 这类工具都会提供一个机制,让我们能明确地告诉 rollup.js:“放心吧,这段代码不会产生副作用,你可以移除它。”具体怎么做呢?如以下代码所示,我们修改 input.js 文件:
01 import {foo} from './utils'
02
03 /*#__PURE__*/ foo()
注意注释代码 /#PURE/,其作用就是告诉rollup.js,对于 foo 函数的调用不会产生副作用,你可以放心地对其进行 Tree-Shaking,此时再次执行构建命令并查看 bundle.js 文件,就会发现它的内容是空的,这说明 Tree-Shaking 生效了。
基于这个案例,我们应该明白,在编写框架的时候需要合理使用 /#PURE/ 注释。如果你去搜索 Vue.js 3 的源码,会发现它大量使用了该注释,例如下面这句:
01 export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)
这会不会对编写代码造成很大的心智负担呢?其实不会,因为通常产生副作用的代码都是模块内函数的顶级调用。什么是顶级调用呢?如以下代码所示:
01 foo() // 顶级调用
02
03 function bar() {
04 foo() // 函数内调用
05 }
可以看到,对于顶级调用来说,是可能产生副作用的;但对于函数内调用来说,只要函数 bar 没有被调用,那么 foo 函数的调用自然不会产生副作用。因此,在 Vue.js 3 的源码中,基本都是在一些顶级调用的函数上使用 /#PURE/ 注释。当然,该注释不仅仅作用于函数,它可以应用于任何语句上。该注释也不是只有 rollup.js 才能识别,webpack 以及压缩工具(如 terser)都能识别它。
4、框架应该输出怎样的构建产物
上文提到 Vue.js 会为开发环境和生产环境输出不同的包,例如 vue.global.js 用于开发环境,它包含必要的警告信息,而 vue.global.prod.js 用于生产环境,不包含警告信息。实际上,Vue.js 的构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物。本节中,我们将讨论这些产物的用途以及在构建阶段如何输出这些产物。
不同类型的产物一定有对应的需求背景,因此我们从需求讲起。首先我们希望用户可以直接在HTML 页面中使用 <script>
标签引入框架并使用:
01 <body>
02 <script src="/path/to/vue.js"></script>
03 <script>
04 const { createApp } = Vue
05 // ...
06 </script>
07 </body>
为了实现这个需求,我们需要输出一种叫作 IIFE 格式的资源。IIFE 的全称是 Immediately Invoked Function Expression,即“立即调用的函数表达式”,易于用 JavaScript 来表达:
01 (function () {
02 // ...
03 }())
如以上代码所示,这是一个立即执行的函数表达式。实际上,vue.global.js 文件就是 IIFE 形式的资源,它的代码结构如下所示:
01 var Vue = (function(exports){
02 // ...
03 exports.createApp = createApp;
04 // ...
05 return exports
06 }({}))
这样当我们使用 <script>
标签直接引入vue.global.js 文件后,全局变量 Vue 就是可用的了。
在 rollup.js 中,我们可以通过配置 format: 'iife’来输出这种形式的资源:
01 // rollup.config.js
02 const config = {
03 input: 'input.js',
04 output: {
05 file: 'output.js',
06 format: 'iife' // 指定模块形式
07 }
08 }
09
10 export default config
不过随着技术的发展和浏览器的支持,现在主流浏览器对原生 ESM 的支持都不错,所以用户除了能够使用 <script>
标签引用 IIFE 格式的资源外,还可以直接引入 ESM 格式的资源,例如 Vue.js 3 还会输出 vue.esm-browser.js 文件,用户可以直接用 <script type="module">
标签引入:
01 <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 文件看一下:
01 {
02 "main": "index.js",
03 "module": "dist/vue.runtime.esm-bundler.js",
04 }
其中 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 设置为 true 或 false,而要使用(process.env.NODE_ENV !== ‘production’) 替换 DEV 常量。例如下面的源码:
01 if (__DEV__) {
02 warn(`useCssModule() is not supported in the global build.`)
03 }
在带有 -bundler 字样的资源中会变成:
01 if ((process.env.NODE_ENV !== 'production')) {
02 warn(`useCssModule() is not supported in the global build.`)
03 }
这样做的好处是,用户可以通过 webpack 配置自行决定构建资源的目标环境,但是最终效果其实一样,这段代码也只会出现在开发环境中。
用户除了可以直接使用 <script>
标签引入资源外,我们还希望用户可以在 Node.js 中通过require 语句引用资源,例如:
01 const Vue = require('vue')
为什么会有这种需求呢?答案是“服务端渲染”。当进行服务端渲染时,Vue.js 的代码是在 Node.js 环境中运行的,而非浏览器环境。在 Node.js 环境中,资源的模块格式应该是 CommonJS,简称cjs。为了能够输出 cjs 模块的资源,我们可以通过修改 rollup.config.js 的配置 format: ‘cjs’ 来实现:
01 // rollup.config.js
02 const config = {
03 input: 'input.js',
04 output: {
05 file: 'output.js',
06 format: 'cjs' // 指定模块形式
07 }
08 }
09
10 export default config
5、特性开关
在设计框架时,框架会给用户提供诸多特性(或功能),例如我们提供 A、B、C 三个特性给用户,同时还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为 true 或 false 来代表开启或关闭对应的特性,这将会带来很多益处。
- 对于用户关闭的特性,我们可以利用 Tree-Shaking 机制让其不包含在最终的资源中。
- 该机制为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性,而不用担心资源体积变大。同时,当框架升级时,我们也可以通过特性开关来支持遗留 API,这样新用户可以选择不使用遗留 API,从而使最终打包的资源体积最小化。
那怎么实现特性开关呢?其实很简单,原理和上文提到的 DEV 常量一样,本质上是利用rollup.js 的预定义常量插件来实现。拿 Vue.js 3源码中的一段 rollup.js 配置来说:
01 {
02 __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
03 }
其中 FEATURE_OPTIONS_API 类似于__DEV__。在 Vue.js 3 的源码中搜索,可以找到很多类似于如下代码的判断分支:
01 // support for 2.x options
02 if (__FEATURE_OPTIONS_API__) {
03 currentInstance = instance
04 pauseTracking()
05 applyOptions(instance, Component)
06 resetTracking()
07 currentInstance = null
08 }
当 Vue.js 构建资源时,如果构建的资源是供打包工具使用的(即带有 -bundler 字样的资源),那么上面的代码在资源中会变成:
01 // support for 2.x options
02 if (__VUE_OPTIONS_API__) { // 注意这里
03 currentInstance = instance
04 pauseTracking()
05 applyOptions(instance, Component)
06 resetTracking()
07 currentInstance = null
08 }
其中 VUE_OPTIONS_API 是一个特性开关,用户可以通过设置 VUE_OPTIONS_API 预定义常量的值来控制是否要包含这段代码。通常用户可以使用 webpack.DefinePlugin 插件来实现:
01 // webpack.DefinePlugin 插件配置
02 new webpack.DefinePlugin({
03 __VUE_OPTIONS_API__: JSON.stringify(true) // 开启特性
04 })
最后详细解释 VUE_OPTIONS_API 开关有什么用。在 Vue.js 2 中,我们编写的组件叫作组件选项 API:
01 export default {
02 data() {}, // data 选项
03 computed: {}, // computed 选项
04 // 其他选项
05 }
但是在 Vue.js 3 中,推荐使用 Composition API 来编写代码,例如:
01 export default {
02 setup() {
03 const count = ref(0)
04 const doubleCount = computed(() => count.value * 2) // 相当于 Vue.js 2 中的 computed 选项
05 }
06 }
但是为了兼容 Vue.js 2,在 Vue.js 3 中仍然可以使用选项 API 的方式编写代码。但是如果明确知道自己不会使用选项 API,用户就可以使用__VUE_OPTIONS_API__ 开关来关闭该特性,这样在打包的时候 Vue.js 的这部分代码就不会包含在最终的资源中,从而减小资源体积。
6、错误处理
错误处理是框架开发过程中非常重要的环节。框架错误处理机制的好坏直接决定了用户应用程序的健壮性,还决定了用户开发时处理错误的心智负担。
为了让大家更加直观地感受错误处理的重要性,我们从一个小例子说起。假设我们开发了一个工具模块,代码如下:
01 // utils.js
02 export default {
03 foo(fn) {
04 fn && fn()
05 }
06 }
该模块导出一个对象,其中 foo 属性是一个函数,接收一个回调函数作为参数,调用 foo 函数时会执行该回调函数,在用户侧使用时:
01 import utils from 'utils.js'
02 utils.foo(() => {
03 // ...
04 })
大家思考一下,如果用户提供的回调函数在执行的时候出错了,怎么办?此时有两个办法,第一个办法是让用户自行处理,这需要用户自己执行try…catch:
01 import utils from 'utils.js'
02 utils.foo(() => {
03 try {
04 // ...
05 } catch (e) {
06 // ...
07 }
08 })
但是这会增加用户的负担。试想一下,如果utils.js 不是仅仅提供了一个 foo 函数,而是提供了几十上百个类似的函数,那么用户在使用的时候就需要逐一添加错误处理程序。
第二个办法是我们代替用户统一处理错误,如以下代码所示:
01 // utils.js
02 export default {
03 foo(fn) {
04 try {
05 fn && fn()
06 } catch(e) {/* ... */}
07 },
08 bar(fn) {
09 try {
10 fn && fn()
11 } catch(e) {/* ... */}
12 },
13 }
在每个函数内都增加 try…catch 代码块,实际上,我们可以进一步将错误处理程序封装为一个函数,假设叫它 callWithErrorHandling:
01 // utils.js
02 export default {
03 foo(fn) {
04 callWithErrorHandling(fn)
05 },
06 bar(fn) {
07 callWithErrorHandling(fn)
08 },
09 }
10 function callWithErrorHandling(fn) {
11 try {
12 fn && fn()
13 } catch (e) {
14 console.log(e)
15 }
16 }
可以看到,代码变得简洁多了。但简洁不是目的,这么做真正的好处是,我们能为用户提供统一的错误处理接口,如以下代码所示:
01 // utils.js
02 let handleError = null
03 export default {
04 foo(fn) {
05 callWithErrorHandling(fn)
06 },
07 // 用户可以调用该函数注册统一的错误处理函数
08 registerErrorHandler(fn) {
09 handleError = fn
10 }
11 }
12 function callWithErrorHandling(fn) {
13 try {
14 fn && fn()
15 } catch (e) {
16 // 将捕获到的错误传递给用户的错误处理程序
17 handleError(e)
18 }
19 }
我们提供了 registerErrorHandler 函数,用户可以使用它注册错误处理程序,然后在callWithErrorHandling 函数内部捕获错误后,把错误传递给用户注册的错误处理程序。
这样用户侧的代码就会非常简洁且健壮:
01 import utils from 'utils.js'
02 // 注册错误处理程序
03 utils.registerErrorHandler((e) => {
04 console.log(e)
05 })
06 utils.foo(() => {/*...*/})
07 utils.bar(() => {/*...*/})
这时错误处理的能力完全由用户控制,用户既可以选择忽略错误,也可以调用上报程序将错误上报给监控系统。
实际上,这就是 Vue.js 错误处理的原理,你可以在源码中搜索到 callWithErrorHandling 函数。另外,在 Vue.js 中,我们也可以注册统一的错误处理函数:
01 import App from 'App.vue'
02 const app = createApp(App)
03 app.config.errorHandler = () => {
04 // 错误处理程序
05 }
7、良好的 TypeScript 类型支持
TypeScript 是由微软开源的编程语言,简称 TS,它是 JavaScript 的超集,能够为 JavaScript 提供类型支持。现在越来越多的开发者和团队在项目中使用 TS。使用 TS 的好处有很多,如代码即文档、编辑器自动提示、一定程度上能够避免低级bug、代码的可维护性更强等。因此对 TS 类型的支持是否完善也成为评价一个框架的重要指标。
如何衡量一个框架对 TS 类型支持的水平呢?这里有一个常见的误区,很多读者以为只要是使用 TS 编写框架,就等价于对 TS 类型支持友好,其实这是两件完全不同的事。考虑到有的读者可能没有接触过 TS,所以这里不会做深入讨论,我们只举一个简单的例子。下面是使用 TS 编写的函数:
01 function foo(val: any) {
02 return val
03 }
这个函数很简单,它接收参数 val 并且该参数可以是任意类型(any),该函数直接将参数作为返回值,这说明返回值的类型是由参数决定的,如果参数是 number 类型,那么返回值也是 number 类型。然后我们尝试使用一下这个函数:
在调用 foo 函数时,我们传递了一个字符串类型的参数 ‘str’,按照之前的分析,得到的结果 res 的类型应该也是字符串类型,然而当我们把鼠标指针悬浮到 res 常量上时,可以看到其类型是 any,这并不是我们想要的结果。为了达到理想状态,我们只需要对 foo 函数做简单的修改即可:
01 function foo<T extends any>(val: T): T {
02 return val
03 }
大家不需要理解这段代码,我们直接来看现在的表现:
可以看到,res 的类型是字符字面量 ‘str’ 而不是any 了,这说明我们的代码生效了。
通过这个简单的例子我们认识到,使用 TS 编写代码与对 TS 类型支持友好是两件事。在编写大型框架时,想要做到完善的 TS 类型支持很不容易,大家可以查看 Vue.js 源码中的 runtime-core/src/apiDefineComponent.ts 文件,整个文件里真正会在浏览器中运行的代码其实只有 3行,但是全部的代码接近 200 行,其实这些代码都是在为类型支持服务。由此可见,框架想要做到完善的类型支持,需要付出相当大的努力。
相关文章:

框架设计的核心要素
我们的框架应该给用户提供哪些构建产物?产物的模块格式如何?当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验,让用户快速定位问题?开发版本的构建和生产版本的构建有何区别&#…...

LeetCode - 26. 删除有序数组中的重复项 (C语言,快慢指针,配图)
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 思路一:快慢指针 在数组中,快慢指针就是两个整数下标,定义 fast 和 slow 这里我们从下标1开始(下标0的数据就1个,没有重复项)&…...

C#不安全代码
在C#中,“不安全代码”(unsafe code)通常指的是那些直接操作内存地址的代码。它允许开发者使用指针等低级别的数据结构,这些在通常的安全代码(safe code)中是不允许的。C# 的不安全代码提供了一种方式&…...

《C++避坑神器·二十二》VS能正常运行程序,但运行exe程序无响应解决办法
原因是某个文件只是放在了项目路径下,没有放在exe路径下,比如Json文件原来只放在了mlx项目下,导致VS可以运行,但运行exe无响应或报错如下: 两种方式修改: 1、把Json文件拷贝一份放到exe路径下 2、利用生成…...

lua调用C/C++的函数,十分钟快速掌握
系列文章目录 lua调用C\C动态库函数 系列文章目录摘要环境使用步骤你需要有个lua环境引入库码代码lua代码 摘要 在现代软件开发中,Lua作为一种轻量级脚本语言,在游戏开发、嵌入式系统等领域广泛应用。Lua与C/C的高度集成使得开发者能够借助其灵活性和高…...

自定义GPT已经出现,并将影响人工智能的一切,做好被挑战的准备了吗?
原创 | 文 BFT机器人 OpenAI凭借最新突破:定制GPT站在创新的最前沿。预示着个性化数字协助的新时代到来,ChatGPT以前所未有的精度来满足个人需求和专业需求。 从本质上讲,自定义GPT是之前的ChatGPT的高度专业化版本或代理,但自定…...

vue中一个页面引入多个相同组件重复请求的问题?
⚠️!!!此内容需要了解一下内容!!! 1、会使用promise??? 2、 promise跟 async 的区别??? async 会终止后面的执行,后续…...

Uniapp连接iBeacon设备——实现无线定位与互动体验(实现篇)
export default { data() { return { iBeaconDevices: [], // 存储搜索到的iBeacon设备 deviceId: [], data: [], url: getApp().globalData.url, innerAudioContext: n…...

【ceph】ceph集群删除pool报错: “EPERM: pool deletion is disabled“
本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》:python零基础入门学习 《python运维脚本》: python运维脚本实践 《shell》:shell学习 《terraform》持续更新中:terraform_Aws学习零基础入门到最佳实战 《k8…...

【微信小程序】使用npm包
1、小程序对npm的支持与限制2、Vant Weapp通过 npm 安装修改 app.json修改 project.config.json构建 npm 包 3、使用4、定制全局主题样式5、API Promise化 1、小程序对npm的支持与限制 目前,小程序中已经支持使用npm安装第三方包, 从而来提高小程序的开发…...

【开发记录篇】第二篇:SQL创建分区表
实现分区表注意事项 分区字段必须在主键中存在 使用时间分区时,字段类型不支持 timestamp,需改为 datetime 年分区示例 下表中使用 insert_time 时间进行分区 CREATE TABLE t_log (id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 日志ID,inse…...

vue 使用 this.$router.push 传参数,接参数的 query或params 两种方法示例
背景:vue项目 使用this.$router.push进行路由跳转时,可以通过query或params参数传递和接收参数。 通过query参数传递参数: // 传递参数 this.$router.push({path: /target,query: {id: 1,name: John} }); // 接收参数 this.$route.query.id …...

rk3588 usb网络共享连接
出门在外总会遇到傻 X 地方 没有能连接公网的 网口给香橙派连网 而我的香橙派5plus 没有wifi模块。。。话不多说 在手机上看一眼手机的mac地址, 在rk3588 上执行以下命令: sudo ifconfig usb0 down sudo ifconfig usb0 hw ether 58:F2:FC:5D:D4:7A //该m…...

shell 拒绝恶意连接脚本 centos7.x拒绝恶意连接脚本
1. crontab -l 脚本频率: */2 * * * * /bin/bash /home/shell/deny.sh 2. 脚本: rm -rf /home/shell/ip_list cat /var/log/secure | grep "Failed password for" | awk {print$(NF-3)} | sort | uniq -c > /home/shell/ip_list #cat /va…...

【系统架构设计】计算机公共基础知识: 2 计算机系统基础知识
目录 一 计算机系统组成 二 操作系统 三 文件系统 四 系统性能 一 计算机系统组成...

什么是代理模式,用 Python 如何实现 Proxy(代理 或 Surrogate)对象结构型模式?
什么是代理模式? 代理(Proxy)是一种结构型设计模式,其目的是通过引入一个代理对象来控制对另一个对象的访问。代理对象充当目标对象的接口,这样客户端就可以通过代理对象间接地访问目标对象,从而在访问过程…...

国内领先的五大API接口供应商
API(Application Programming Interface)接口,现在很多应用系统中常用的开放接口,对接相应的系统、软件功能,简化专业化的程序开发。作者用过的国内比较稳定的API接口供应商有如下几家,大家可以参考选择&am…...

第十九章 Java绘图
一,Java绘图类 19.1.1Graphics类 Graphics类是所有图形上下文的抽象基类,它允许应用程序在组件以及闭屏图像上进行绘制。 可实现直线,矩形,多边形,椭圆,圆弧等形状和文本,图片的绘制制作。 …...

【C++面向对象】13. 接口 / 抽象类*
文章目录 【 1. 抽象类 】1.1 抽象类的定义1.2 抽象类的应用条件1.3 实例 【 2. 设计策略 】 接口描述了类的行为和功能,而不需要完成类的特定实现。C 接口是使用 抽象类(abstract base class,也称为ABC) 来实现的。 【 1. 抽象类…...

LeetCode热题100——二分查找
二分查找 1. 搜索插入位置2. 搜素二维矩阵3. 在排序数组中查找第一个和最后一个元素位置 1. 搜索插入位置 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 // 题…...

使用VC++实现分段线性变换,直方图均衡化、锐化处理(使用拉普拉斯算子)
图像锐化1 实验要求 5.1实验目的、要求 实验目的: (1)掌握图像增强的原理与相关方法。 (2)能使用VC实现图像增强的一些相关功能。 实验要求: A部分: (1)对一幅256级灰度…...

react class改hooks写法
类头修改 export default class EditUseTable extends Component 改为 export default function EditUseTable({})参数修改 constructor(props) {super(props)const {dbRecord, type, currentRecord, readOnly, updateTaxAmount} this.props改为(主函数的参数&a…...

桂院校园导航 | 云上高校导航 云开发项目 二次开发教程 1.3
Gitee代码仓库:桂院校园导航小程序 GitHub代码仓库:GLU-Campus-Guide 演示视频 中国大学生计算机设计大赛-移动应用与开发-云上高校导航 升级日志 1.3 优化了小程序的数据存储方式,对部分页面进行了调整,调整了功能和代码。 引…...

sscanf提取相应字符到数组
代码如下 #include<stdio.h> #include<string.h>int main(int argc, char const *argv[]) {char buf[128] {0};int m1 0, m2 0;int s1 0, s2 0;char lrc[128] "";sscanf("[02:16.33][04:11.44]我想大声宣布对你恋恋不舍","[%*1d%d…...

本地开发环境和服务器传输数据的几种方法
❤️觉得内容不错的话,欢迎点赞收藏加关注😊😊😊,后续会继续输入更多优质内容❤️ 👉有问题欢迎大家加关注私戳或者评论(包括但不限于NLP算法相关,linux学习相关,读研读博…...

LeetCode之二叉树
发现更多计算机知识,欢迎访问Cr不是铬的个人网站 最近数据结构学到二叉树,就刷了刷力扣,写这篇文章也是辅助记忆。 103二叉树锯齿形遍历 要解出本道题,首先要会层次遍历。层次遍历我们都知道用一个队列去实现就行。但是力扣这里…...

论文学习——THE USTC SYSTEM FOR ADRESS-M CHALLENGE
文章目录 引言正文Abstract模型基本结构模型效果汇总 Introduction介绍跨语言任务的独特性思路启发和变化如何使用预定义好的音频特征如何使用预定义好的语言模型——语言模型中获取韵律信息结果说明 Dataset数据集Mthods方法使用设计好的特征进行AD检测使用的特征分类和训练方…...

第一百七十五回 如何创建放射形状渐变背景
文章目录 1. 概念介绍2. 实现方法3. 代码与效果3.1 示例代码3.2 运行效果 4. 内容总结 我们在 上一章回中介绍了"如何创建扇形渐变背景"相关的内容,本章回中将介绍" 如何创建放射形状渐变背景"。闲话休提,让我们一起Talk Flutter吧…...

vue实现调用手机拍照、录像功能
目录 前言 准备工作 在这个示例中,我们将使用Vue.js框架来实现我们的目标。如果你还不熟悉Vue.js,推荐先学习一下Vue.js的基础知识。 接下来,我们需要创建一个基于Vue.js的项目。你可以使用Vue CLI来创建一个全新的Vue项目:# 安…...

WPF播放视频
在WPF中,你可以使用MediaElement来播放本地视频。下面是一个简单的例子: <Window x:Class"WPFVideoPlayer.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsof…...