Vue3企业级项目开发实战课-笔记记录

news2025/1/12 2:55:31

基础篇

vue3的编译和非编译模式

Vue.js 代码经过编译后才能在浏览器运行,而且,Vue.js 代码编译后的结果就是基于非编译语法来运行的。
在这里插入图片描述
vue3代码编译过程主要进行了一下操作

  • 把 Vue.js 代码里的模板编译成基于 JavaScript 代码描述的 VNode(虚拟节点);
  • 把 Vue.js 代码里 JavaScript 逻辑代码,编译成运行时对应生命周期的逻辑代码;
  • 把内置的 CSS 样式代码抽离出来。

webpack/vite构建vue3项目

Webpack 和 Vite 的定位是不一样的

Vite 定位是 Web“开发工具链”,其内置了一些打包构建工具,让开发者开箱即用,例如预设了 Web 开发模式直接使用 ESM 能力,开发过程中可以通过浏览器的 ESM 能力按需加载当前开发页面的相关资源。

Webpack 定位是构建“打包工具”,面向的是前端代码的编译打包过程。Webpack 能力很单一,就是提供一个打包构建的能力,如果有特定的构建需要,必须让开发者来选择合适的 Loader 和 Plugin 进行组合配置,达到最终的想要的打包效果。

webpack配置

安装依赖包

npm i --save vue

npm i --save-dev css-loader mini-css-extract-plugin vue-loader webpack webpack-cli

添加webpack.config.js

const path = require("path");
const { VueLoaderPlugin } = require("vue-loader/dist/index");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  mode: "production",
  entry: {
    "index": path.join(__dirname, "src/index.js"),
  },
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name].js",
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: ["vue-loader"],
      },
      {
        test: /\.(css|less)$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
  ],
  externals: {
    "vue": "window.Vue",
  },
};

在package.json中添加命令配置

{
  "scripts": {
	"dev":"NODE_ENV=development webpack serve -c ./webpack.config.js",
	"build":"NODE_ENV=production webpack -c ./webpack.config.js"
  }
}
vite配置

第一步,项目目录和源码准备

.
├── dist
├── index.html
├── package.json
├── src
│ ├── app.vue
│ └── index.js
└── vite.config.js

第二步,安装依赖

npm i --save vue

npm i --save-dev vite @vitejs/plugin-vue

第三步,配置 Vite 的 Vue.js 3 编译配置,也就是在 vite.config.js 配置 Vite 的编译配置

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
  plugins: [vue()],
  base: "./",
});

第四步,package.json 配置开发模式和生产模式的脚本命令。

{
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  }
}

Vite 根据访问页面引用的 ESM 类型的 JavaScript 文件进行查找依赖,并将依赖通过 esbuild 编译成 ESM 模块的代码,保存在 node_modules/.vite/ 目录下;

浏览器的 ESM 加载特性会根据页面依赖到 ESM 模块自动进行按需加载。

  • 再次修改代码,再次访问页面,会自动执行 ESM 按需加载,同时触发依赖到的变更文件重新单独编译;
  • 修改代码只会触发刷新页面,不会直接触发代码编译,而且源码编译是浏览器通过 ESM 模块加载访问到对应文件才进行编译的;
  • 开发模式下因为项目源码是通过 esbuild 编译,所以速度比 Rollup 快,同时由于是按页面里请求依赖进行按需编译,所以整体打包编译速度理论上是比 Rollup 快一些。

在这里插入图片描述

组件间的通信

props:父组件向子组件单向传递数据
在这里插入图片描述
emits:子组件的数据传递给父组件
在这里插入图片描述
子组件代码:

<template>
  <div class="v-text">
    <span>
      地址:
    </span>
    <input :value="props.text" @ input="onInput" />
  </div>
</template>
<script setup>
const props = defineProps({
  text: String,
});
const emits = defineEmits(["onChangeText"]);
const onInput = (e) => {
  emits("onChangeText", e.target.value);
};
</script>

父组件代码

<template>
  <div>订单信息:{{ text }}</div>
  <div class="app">
    <v-text v-bind:text="text" v-on:onChangeText="onChangeText" />
  </div>
</template>
<script setup>
import { ref } from "vue";
import VText from "./text.vue";
const text = ref("888号");
const onChangeText = (newText) => {
  text.value = newText;
};
</script>

课程完整资料库

多层级跨组件传递数据,使用Pinia

Pinia 就是一个基于 Proxy 实现的 Vue.js 公共状态数据管理的 JavaScript 库,可以提供组件间的数据通信

在这里插入图片描述
Pinia 可以定义一个公共的数据 store,在这个公共数据里管理多个数据的操作和计算。各个组件,无论是父子组件关系还是兄弟组件管理,都基于这个 store 来进行读数据展示和写数据更新状态,读写过程都是分开管理。读数据基于内置的 Getter 和 State 属性,写数据基于内部的 Action 方法。

import { defineStore } from "pinia";
export const useMyStore = defineStore("my-store", {
  state: () => ({
    text: "888号",
    list: [
      {
        name: "苹果",
        price: 20,
        count: 0,
      },
      {
        name: "香蕉",
        price: 12,
        count: 0,
      },
      {
        name: "梨子",
        price: 15,
        count: 0,
      },
    ],
  }),
  getters: {
    totalPrice(state) {
      let total = 0;
      state.list.forEach((item) => {
        total += item.price * item.count;
      });
      return;
      total;
    },
  },
  actions: {
    updateText(text) {
      this.text = text;
    },
    increase(index) {
      this.list[index].count += 1;
    },
    decrease(index) {
      if (this.list[index].count > 0) {
        this.list[index].count -= 1;
      }
    },
  },
});

课程完整资料库

搭建自研组件库

定制化组件库可以更好的满足公司自己业务逻辑需求。作为前端工程师,你就必须掌握自研组件库的开发能力,为可能出现的定制化组件的要求做好准备。
分为三个技术要点

  • monorepo 管理组件代码;
  • Vue.js 3.x 源文件的多种模块格式编译;
  • 基于 Less 开发 CSS 样式文件和独立编译。

不同类型的组件可能存在互相依赖或者引用的关系,要保证能在一个代码仓库中快速调试多个 npm 模块的代码效果。一个仓库管理多个 npm 模块(多个子项目),就需要用到 monorepo 的项目管理形式。

必须支持组件库能够按需加载,使用将源码编译成 ES Module 和 CommonJS 格式。

如何搭建monorepo项目

利用 pnpm 天然支持 monorepo 的管理能力,同时 pnpm 安装 node_modules 也能更省体积空间。

  • 初始化代码目录;
  • 基于 pnpm 配置 monorepo 项目;
  • 安装所有子项目依赖。
    在这里插入图片描述
    业务组件库子项目(@my/business)里依赖了基础组件库的子项目(@my/components),通过 pnpm 管理的 monorepo 项目方式,将依赖的 @my/components 子项目通过“软链接”形式指向了真正的 components/* 目录。
对组件库做编译设置

代码编译分成以下三个步骤

  • 编译 TypeScript 和 Vue.js 3.x 源码为 ES Module 和 CommonJS 模块的两种 JavaScript 代码文件;
  • 编译出所有 JavaScript 文件的 TypeScript 类型描述文件
  • 把文件编译 Less 成 CSS 文件

编译 TypeScript 和 Vue.js 3.x 源码成 ES Module 和 CommonJS 模块的两种 JavaScript 代码文件。在项目的 scripts/* 目录下编写以下编译脚本
脚本文件是 scripts/build-module.ts

import fs from "node:fs";
import { rollup } from "rollup";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import VueMacros from "unplugin-vue-macros/rollup";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import esbuild from "rollup-plugin-esbuild";
import glob from "fast-glob";
import type { OutputOptions } from "rollup";
import { resolvePackagePath } from "./util";

const getExternal = async (pkgDirName: string) => {
  const pkgPath = resolvePackagePath(pkgDirName, "package.json");
  const manifest = require(pkgPath) as any;
  const {
    dependencies = {},
    peerDependencies = {},
    devDependencies = {},
  } = manifest;
  const deps: string[] = [
    ...new Set([
      ...Object.keys(dependencies),
      ...Object.keys(peerDependencies),
      ...Object.keys(devDependencies),
    ]),
  ];
  return (id: string) => {
    if (id.endsWith(".less")) {
      return true;
    }
    return deps.some((pkg) => id === pkg || id.startsWith(`${pkg}/`));
  };
};
const build = async (pkgDirName: string) => {
  const pkgDistPath = resolvePackagePath(pkgDirName, "dist");
  if (fs.existsSync(pkgDistPath) && fs.statSync(pkgDistPath).isDirectory()) {
    fs.rmSync(pkgDistPath, {
      recursive: true,
    });
  }
  const input = await glob(["**/*.{js,jsx,ts,tsx,vue}", "!node_modules"], {
    cwd: resolvePackagePath(pkgDirName, "src"),
    absolute: true,
    onlyFiles: true,
  });
  const bundle = await rollup({
    input,
    plugins: [
      VueMacros({
        setupComponent: false,
        setupSFC: false,
        plugins: {
          vue: vue({
            isProduction: true,
          }),
          vueJsx: vueJsx(),
        },
      }),
      nodeResolve({
        extensions: [".mjs", ".js", ".json", ".ts"],
      }),
      commonjs(),
      esbuild({
        sourceMap: true,
        target: "es2015",
        loaders: {
          ".vue": "ts",
        },
      }),
    ],
    external: await getExternal(pkgDirName),
    treeshake: false,
  });
  const options: OutputOptions[] = [
    // CommonJS 模块格式的编译
    {
      format: "cjs",
      dir: resolvePackagePath(pkgDirName, "dist", "cjs"),
      exports: "named",
      preserveModules: true,
      preserveModulesRoot: resolvePackagePath(pkgDirName, "src"),
      sourcemap: true,
      entryFileNames: "[name].cjs",
    },
    // ES Module 模块格式的编译
    {
      format: "esm",
      dir: resolvePackagePath(pkgDirName, "dist", "esm"),
      exports: undefined,
      preserveModules: true,
      preserveModulesRoot: resolvePackagePath(pkgDirName, "src"),
      sourcemap: true,
      entryFileNames: "[name].mjs",
    },
  ];
  return Promise.all(options.map((option) => bundle.write(option)));
};
console.log("[TS] 开始编译所有子模块···");
await build("components");
await build("business");
console.log("[TS] 编译所有子模块成功!");

编译出ts文件,需要脚本文件是 scripts/build-dts.ts,

import process from 'node:process'
import path from 'node:path';
import fs from 'node:fs'
import * as vueCompiler from 'vue/compiler-sfc'
import glob from 'fast-glob';
import { Project } from 'ts-morph'
import type { CompilerOptions, SourceFile } from 'ts-morph'
import { resolveProjectPath, resolvePackagePath } from './util';

const tsWebBuildConfigPath = resolveProjectPath('tsconfig.web.build.json');

// 检查项目的类型是否正确
function checkPackageType(project: Project) {
  const diagnostics = project.getPreEmitDiagnostics();
  if (diagnostics.length > 0) {
    console.error(project.formatDiagnosticsWithColorAndContext(diagnostics))
    const err = new Error('TypeScript类型描述文件构建失败!')
    console.error(err)
    throw err
  }
}

// 将*.d.ts文件复制到指定格式模块目录里
async function copyDts(pkgDirName: string) {
  const dtsPaths = await glob(['**/*.d.ts'], {
    cwd: resolveProjectPath('dist', 'types', 'packages', pkgDirName, 'src'),
    absolute: false,
    onlyFiles: true,
  });

  dtsPaths.forEach((dts: string) => {
    const dtsPath =  resolveProjectPath('dist', 'types', 'packages', pkgDirName, 'src', dts)
    const cjsPath = resolvePackagePath(pkgDirName, 'dist', 'cjs', dts);
    const esmPath = resolvePackagePath(pkgDirName, 'dist', 'esm', dts);
    const content = fs.readFileSync(dtsPath, { encoding: 'utf8' });
    fs.writeFileSync(cjsPath, content);
    fs.writeFileSync(esmPath, content);
  });
}

// 添加源文件到项目里
async function addSourceFiles(project: Project, pkgSrcDir: string) {
  project.addSourceFileAtPath(resolveProjectPath('env.d.ts'))

  const globSourceFile = '**/*.{js?(x),ts?(x),vue}'
  const filePaths = await glob([globSourceFile], {
    cwd: pkgSrcDir,
    absolute: true,
    onlyFiles: true,
  })

  const sourceFiles: SourceFile[] = []
  await Promise.all([
    ...filePaths.map(async (file) => {
      if (file.endsWith('.vue')) {
        const content = fs.readFileSync(file, { encoding: 'utf8' })
        const hasTsNoCheck = content.includes('@ts-nocheck')

        const sfc = vueCompiler.parse(content)
        const { script, scriptSetup } = sfc.descriptor
        if (script || scriptSetup) {
          let content =
            (hasTsNoCheck ? '// @ts-nocheck\n' : '') + (script?.content ?? '')

          if (scriptSetup) {
            const compiled = vueCompiler.compileScript(sfc.descriptor, {
              id: 'temp',
            })
            content += compiled.content
          }

          const lang = scriptSetup?.lang || script?.lang || 'js'
          const sourceFile = project.createSourceFile(
            `${path.relative(process.cwd(), file)}.${lang}`,
            content
          )
          sourceFiles.push(sourceFile)
        }
      } else {
        const sourceFile = project.addSourceFileAtPath(file)
        sourceFiles.push(sourceFile)
      }
    }),
  ])

  return sourceFiles
}

// 生产Typescript类型描述文件
async function generateTypesDefinitions(
  pkgDir: string,
  pkgSrcDir: string,
  outDir: string
){
  const compilerOptions: CompilerOptions = {
    emitDeclarationOnly: true,
    outDir,
  }
  const project = new Project({
    compilerOptions,
    tsConfigFilePath: tsWebBuildConfigPath
  })

  const sourceFiles = await addSourceFiles(project, pkgSrcDir)
  checkPackageType(project);
  await project.emit({
    emitOnlyDtsFiles: true,
  })

  const tasks = sourceFiles.map(async (sourceFile) => {
    const relativePath = path.relative(pkgDir, sourceFile.getFilePath())

    const emitOutput = sourceFile.getEmitOutput()
    const emitFiles = emitOutput.getOutputFiles()
    if (emitFiles.length === 0) {
      throw new Error(`异常文件: ${relativePath}`)
    }

    const subTasks = emitFiles.map(async (outputFile) => {
      const filepath = outputFile.getFilePath()
      fs.mkdirSync(path.dirname(filepath), {
        recursive: true,
      });
    })

    await Promise.all(subTasks)
  })
  await Promise.all(tasks)
}

async function build(pkgDirName) {
  const outDir = resolveProjectPath('dist', 'types');
  const pkgDir = resolvePackagePath(pkgDirName);
  const pkgSrcDir = resolvePackagePath(pkgDirName, 'src');
  await generateTypesDefinitions(pkgDir, pkgSrcDir, outDir);
  await copyDts(pkgDirName);
}

console.log('[Dts] 开始编译d.ts文件···')
await build('components');
await build('business');
console.log('[Dts] 编译d.ts文件成功!')

编译样式文件less到css,编译脚本文件是 scripts/build-css.ts

import fs from 'node:fs';
import path from 'node:path';
import glob from 'fast-glob';
import less from 'less';
import { resolvePackagePath, wirteFile } from './util';

function compileLess(file: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const content = fs.readFileSync(file, { encoding: 'utf8' });
    less.render(content, {
      paths: [ path.dirname(file) ],
      filename: file,
      plugins: [],
      javascriptEnabled: true
    }).then((result) => {
      resolve(result.css);
    }).catch((err) => {
      reject(err);
    })
  })
}

async function build(pkgDirName: string) {
  const pkgDir = resolvePackagePath(pkgDirName, 'src');
  const filePaths = await glob(['**/style/index.less'], {
    cwd: pkgDir,
  });
  const indexLessFilePath = resolvePackagePath(pkgDirName, 'src', 'index.less');
  if (fs.existsSync(indexLessFilePath)) {
    filePaths.push('index.less')
  }
  for (let i = 0; i < filePaths.length; i ++) {
    const file = filePaths[i];
    const absoluteFilePath = resolvePackagePath(pkgDirName, 'src', file);
    const cssContent = await compileLess(absoluteFilePath);
    const cssPath = resolvePackagePath(pkgDirName, 'dist', 'css', file.replace(/.less$/, '.css'));
    wirteFile(cssPath, cssContent);
  }
  
}
console.log('[CSS] 开始编译Less文件···')
await build('components');
await build('business');
console.log('[CSS] 编译Less成功!')

组件库开发的三个要素

  • 用 monorepo 管理多种类型组件库,这类项目的代码管理方式,可以一个仓库同时聚合管理多个项目,让项目之间代码依赖使用更方便;

  • 源码要编译成多种模块格式(CommonJS 和 ES Module),主要考虑到前端代码 npm 模块的时候,目前主流是 ES
    Module 模块格式,但还是存在很多传统的 CommonJS 模块格式的使用兼容。所以在开发自研组件库的时候,尽量要考虑这两种模块格式;

  • 基于 Less 等预处理 CSS 语言来开发组件库的样式,由于 CSS 语言能力有限,无法像 JavaScript
    那样可以使用各种编程逻辑和特性,所以需要借助 CSS 预处理语言进行开发 CSS。

动态渲染组件

动态渲染组件就是通过“动态”的方式来“渲染”组件,不需要像常规 Vue.js 3.x 组件那样,把组件注册到模板里使用。
动态渲染组件的两个技术特点

  • 以直接函数式地使用来执行渲染,使用者不需要写代码来挂载组件
  • 组件内部实现了动态挂载和卸载节点的操作。

Vue.js 3.x 动态渲染组件在页面上是独立于“Vue.js 主应用”之外的渲染。
动态渲染组件整个生命周期,最核心的就是“动态挂载”和“动态卸载”两个步骤
动态组件在其生命周期,可以这么来设计

import { Module } from 'xxxx'

// 创建动态组件 mod1
const mod1 = Module.create({  /* 组件参数 */ });
// 挂载渲染 mod1
mod1.open();
// 更新组 mod1 件内容
mod1.update({ /* 更新内容参数 */ })
// 卸载动态组件 mod1
mod1.close();

用最简单的 Vue.js 3.x 代码实现

import { defineComponent, createApp, h } from 'vue';

// 用 JSX 语法实现一个Vue.js 3.x的组件
const ModuleComponent = defineComponent({
  setup(props, context) {
    return () => {
      return (
        <div>这是一个动态渲染的组件</div>
      );
    };
  }
});

// 实现动态渲染组件的过程

export const createModule = () => {
  // 创建动态节点DOM
  const dom = document.createElement('div');
  // 把 DOM 追加到页面 body标签里
  const body = document.querySelector('body') as HTMLBodyElement;
  const app = createApp({
    render() {
      return h(DialogComponent, {});
    }
  });
 

  // 返回当前组件的操作实例
  // 其中封装了挂载和卸载组件的方法
  return {
    open(): () => {
      // 把组件 ModuleComponent 作为一个独立应用挂载在 DOM 节点上
      app.mount(dom);
    },
    close: () => {
      // 卸载组件
      app.unmount();
      // 销毁动态节点
      dom.remove();
    }
  }
}

上面实现的组件可以这样使用

import { createModule } from './xxxx';

// 创建和渲染组件
const mod = createModule();

// 挂载渲染组件
mod.open();

// 卸载关闭组件
mod.close();
实现Dialog 组件
// ./dialog.tsx
import { defineComponent } from 'vue';
import { prefixName } from '../theme/index';

export const DialogComponent = defineComponent({
  props: {
    text: String
  },
  emits: ['onOk'],
  setup(props, context) {
    const { emit } = context;
    const onOk = () => {
      emit('onOk');
    };
    return () => {
      return (
        <div class={`${prefixName}-dialog-mask`}>
          <div class={`${prefixName}-dialog`}>
            <div class={`${prefixName}-dialog-text`}>{props.text}</div>
            <div class={`${prefixName}-dialog-footer`}>
              <button class={`${prefixName}-dialog-btn`} onClick={onOk}>
                确定
              </button>
            </div>
          </div>
        </div>
      );
    };
  }
});

以下是封装了函数方法调用的动态渲染组件的方式

import { createApp, h } from 'vue';
import { DialogComponent } from './dialog';

function createDialog(params: { text: string; onOk: () => void }) {
  const dom = document.createElement('div');
  const body = document.querySelector('body') as HTMLBodyElement;
  body.appendChild(dom);
  const app = createApp({
    render() {
      return h(DialogComponent, {
        text: params.text,
        onOnOk: params.onOk
      });
    }
  });
  app.mount(dom);

  return {
    close: () => {
      app.unmount();
      dom.remove();
    }
  };
}

const Dialog: { createDialog: typeof createDialog } = {
  createDialog
};

export default Dialog;

代码单元测试

单元测试,英文是 Unit Test,也可以称之为“模块测试”,主要是对代码最小单位逐一进行测试验证功能。这里的“代码最小单位”可以是一个函数、一个组件、一个类,甚至是一个变量。只要是能执行功能的代码模块,都可以称之为一个“最小单位”。

市面支持测试“断言”或“测试管理”的主流前端 JavaScript 单元测试工具,有 Mocha、Jest 和 Vitest:

  • Mocha是面向 Node.js 环境的 JavaScript 单元测试,不能直接支持浏览器的 API,断言可以使用 Node.js 自带 assert 模块或者第三方断言工具,例如 Chai;
  • Jest是同时支持 Node.js 和在 Node.js 里模拟浏览器 API 的测试工具,内部自带测试“断言”和“管理”工具,是 React.js 官方维护的测试工具。
  • Vitest跟 Jest 一样,都能支持 Node.js 和浏览器 API,也自带测试“断言”和“管理”工具,是 Vue.js 官方维护的测试工具,对 Vue.js 的支持能力比较友好。

用 Vitest,给 Vue.js 3.x 组件库做单元测试

安装依赖

npm i -D vitest @vue/test-utils @vitejs/plugin-vue @vitejs/plugin-vue-jsx jsdom

pnpm i -D vitest @vue/test-utils @vitejs/plugin-vue @vitejs/plugin-vue-jsx jsdom

vitest.config.js配置文件

import { defineConfig } from 'vitest/config';
import PluginVue from '@vitejs/plugin-vue';
import PluginJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
  plugins: [PluginVue(), PluginJsx()],
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      // 覆盖率统计工具
      provider: 'c8',
      // 覆盖率的分母,packages/ 目录里
      // 所有src的源文件作为覆盖率统计的分母
      include: ['packages/*/src/**/*'],
      // 全量覆盖率计算
      all: true
    }
  }
});

新建文件./packages/components/tests/demo.test.ts,小试一下单元测试

import { describe, test, expect } from 'vitest';

describe('Demo', () => {
  test('Test case', () => {
    const a = 1;
    const b = 2;
    expect(a + b).toBe(3);
  });
});
添加button行为测试

./packages/components/tests/button/index.test.ts文件中

import { describe, test, expect } from 'vitest';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import ButtonTest from './index.test.vue';

describe('Button', () => {
  test('click event', async () => {
    const wrapper = mount(ButtonTest, { props: { num: 123 } });
    const textDOM = wrapper.find('.display-text');
    const btnDOM = wrapper.find('.btn-add');
    expect(textDOM.text()).toBe('当前数值=123');
    btnDOM.trigger('click');
    await nextTick();
    expect(textDOM.text()).toBe('当前数值=124');
  });
});

单元测试验证代码

<template>
  <div class="display-text">当前数值={{ num }}</div>
  <Button class="btn-add" @click="onClick">点击加1</Button>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { Button } from '../../src';
const props = defineProps<{ num: number }>();
const num = ref<number>(props.num);
const onClick = () => {
  num.value++;
};
</script>

课程完整资料库

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

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

相关文章

计算机技术基础 (bat 批处理)Note3

计算机技术基础 &#xff08;bat 批处理&#xff09;Note3 本节主要讲 Goto ; Find ; TYPE ; START ; CALL 命令 Goto 命令 Goto 命令&#xff1a;指定跳转到标签行&#xff0c;找到标签行后&#xff0c;程序将处理下一行开始的命令。&#xff08;它本身不执行任何操作&…

快速认识EA(Enterprise Architecture)

前言 企业架构&#xff0c;英文是&#xff1a;Enterprise Architecture&#xff0c;简称&#xff1a;EA&#xff0c;是承接企业战略规划与IT建设之间的桥梁&#xff0c;是企业信息化的核心&#xff0c;主要包括业务架构和IT架构。 架构的本质是管理和解决系统的复杂性&#x…

Windows及Linux系统加固

君衍. 一、Windows加固1、配置简介2、账户配置3、本地配置4、安全设置 二、Linux加固1、配置简介2、网络配置3、日志和审计配置4、访问认证和授权配置5、系统运维配置 一、Windows加固 1、配置简介 通常在Windows安全配置中有两类对象 一类是Windows Server&#xff0c;如win …

用Python实现Cmpp协议的教程

引言&协议概述 &#xff08;CMPP&#xff09;是中国移动为实现短信业务而制定的一种通信协议&#xff0c;用于在客户端&#xff08;SP&#xff0c;Service Provider&#xff09;和中国移动短信网关之间传输短消息&#xff0c;有时也叫做移动梦网短信业务。CMPP3.0是该协议…

重磅:2024上海不锈钢工业展览会-相聚12月

2024上海不锈钢工业展览会-相聚12月 时间&#xff1a;2024年12月18-20日 地点&#xff1a;上海新国际博览中心 2024上海国际不锈钢工业展览会规划30000平方米展览规模&#xff0c;预设展位1200个&#xff0c;将为国内外不锈钢产业提供一个集“展示、合作、交易、发展”于一…

使用 Web APi - MediaRecorder 获取麦克风资源,报错:Cannot find name ‘MediaRecorder‘ 的解决方法

目录 一、背景&#xff1a; 二、具体解决方法 一、背景&#xff1a; angular 调用 MediaRecorder 来使用麦克风获取声音&#xff0c;&#xff08;具体要求&#xff1a;angular 前端 按键调用 麦克风&#xff0c;松开按键生成音频文件&#xff09;代码如下&#xff08;来自通…

上市企业金融错配、信贷错配、资本错配程度,原始数据测算代码和结果整(1998-2023年)

1、数据来源&#xff1a;根据沪深A股上市公司数据进行测算 2、时间跨度&#xff1a;1998-2023年 3、区域范围&#xff1a;沪深A股上市公司 4、指标说明&#xff1a; 参考邵挺(2010)、周煜皓和张胜勇(2014)的研究&#xff0c;运用金融错配负担水平来衡量信贷错配&#xff08…

数据结构【没头单链表】

目录 ​ 概念与结构 结点 链表的性质 链表的打印分析 实现单链表&#xff1a; 创建单链表数据 申请空间 尾插数据 打印 头插数据 尾删 头删 查询数据 指定位置前插入数据 指定位置后插入数据 删除pos节点 删除pos后面的节点 销毁 链表的分类 链表说明&#…

unittest框架和pytest框架区别及示例

unittest框架和pytest框架区别及示例 类型unittest框架pytest框架unittest框架示例pytest框架示例安装python内置的一个单元测试框架,标准库&#xff0c;不需要安装第三方单元测试库&#xff0c;需要安装使用时直接引用 import unittest安装命令&#xff1a;pip3 install pyte…

博客建站4 - ssh远程连接服务器

1. 什么是SSH?2. 下载shh客户端3. 配置ssh密钥4. 连接服务器5. 常见问题 5.1. IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! 1. 什么是SSH? SSH&#xff08;Secure Shell&#xff09;是一种加密的网络协议&#xff0c;用于在不安全的网络中安全地远程登录到其他…

刚刚 威尼斯影评人周公布 2024 年电影阵容 包括敏感纪录片《本土》

《本土》 威尼斯影评人周是威尼斯电影节专门为首次拍摄电影的人设立的侧边活动&#xff0c;该活动公布了第 39 届威尼斯电影节的七部竞赛片和两部非竞赛片的入选名单&#xff0c;第 39 届威尼斯电影节将于 8 月 28 日至 9 月 7 日举行。 较为及时的作品之一是美国导演迈克尔普…

工业互联网带来什么变革?详解工业互联网产业模式与业务模式!

随着互联网技术的不断进步&#xff0c;工业互联网产业模式应运而生&#xff0c;成为制造业服务化延伸的新引擎。这种模式突破了传统制造业的局限&#xff0c;将服务与产品全生命周期紧密结合&#xff0c;实现了从单一产品制造向提供综合服务的转变。本文将分析工业互联网如何利…

1.30、基于卷积神经网络的手写数字旋转角度预测(matlab)

1、卷积神经网络的手写数字旋转角度预测原理及流程 基于卷积神经网络的手写数字旋转角度预测是一个常见的计算机视觉问题。在这种情况下&#xff0c;我们可以通过构建一个卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;来实现该任务。以下…

操作线程的方法

文章目录 前言一、线程的生命周期二、线程的操作方法 1.休眠2.加入3.中断4.礼让总结 前言 将线程看作一个生命的开始和结束&#xff0c;更好理解它各个状态的变化。同时该文会介绍操作线程的主要方法来控制线程的生命周期。这些方法的使用和线程生命周期的变化是密切相关的。 一…

甄选范文“论面向方面的编程技术及其应”,软考高级论文,系统架构设计师论文

论文真题 针对应用开发所面临的规模不断扩大、复杂度不断提升的问题,面向方面的编程(Aspect Oriented Programming,AOP)技术提供了一种有效的程序开发方法。为了理解和完成一个复杂的程序,通常要把程序进行功能划分和封装。一般系统中的某些通用功能,如安全性、持续性、日…

Intellij IDEA 的Plugins加载不出来的解决方法

一、点开插件---右上角设置---HTTP代理设置 二、勾选自动检测代理设置 输入url&#xff1a; https://plugins.jetbrains.com/ 配置完成后&#xff0c;点击确定。 然后点击检查连接&#xff0c;再一次输入那个URL&#xff0c;一般来说可以连接成功了 然后 重启IDEA以刷新缓…

详解数据结构之二叉树(堆)

详解数据结构之二叉树(堆) 树 树的概念 树是一个非线性结构的数据结构&#xff0c;它是由 n(n>0)个有限节点组成的一个具有层次关系的集合&#xff0c;它的外观形似一颗倒挂着的树&#xff0c;根朝上&#xff0c;叶朝下&#xff0c;所以称呼为树。每颗子树的根节点有且只…

7. 聚类算法 KMeans

聚类算法 KMeans 1. 应用&#xff1a;大数据杀熟2. 迭代法3. 代码 1. 应用&#xff1a;大数据杀熟 618、双十一&#xff0c;平台要对用户进行分类&#xff1a;用户&#xff1a; 脑残粉&#xff08;不降价&#xff0c;或者涨点价&#xff09;墙头草&#xff08;给点小优惠券&am…

二叉树基础及实现(一)

目录&#xff1a; 一. 树的基本概念 二. 二叉树概念及特性 三. 二叉树的基本操作 一. 树的基本概念&#xff1a; 1 概念 &#xff1a; 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0 &#xff09;个有限结点组成一个具有层次关系的集合。 把它叫做树是因…