• 发布vue3组件到npm


    目录

    准备你的组件

    创建项目

    创建步骤

    创建组件 

    封装我们要得组件

    导出组件 

    打包 

     使用组件

    发布包

    创建npm账户

    登录npm

    切换npm源

    发布到npm

    package.json

    package.json字段

    确定版本号

    版本号格式

    版本号递增逻辑

    vite 打包库模式 

    库模式配置

    build配置项

    build.lib

    build.rollupOptions


    当你开发Vue 3应用程序时,有时你可能会需要将自己创建的组件发布到npm上,以便其他开发者可以轻松地在他们的项目中使用这些组件。本文将指导你如何将Vue 3组件发布到npm。
     

    准备你的组件

    首先,确保你已经创建了你的Vue 3组件,并且它在本地正常工作。你可以使用Vue CLI或手动创建组件,然后使用Vue 3的组件系统进行开发。

    创建项目

    npm init vite@latest
    

    创建步骤

    提示我们要安装create-vite@4.1.0得依赖,选择y

    起一个组件名字,然后我们选择vue

    这里我选择的是javascript,然后回车

    安装完成

    因为我们需要了element-ui组件库,所以我们要手动安装一下依赖

    npm install element-plus --save
    

    创建组件 

    首先,我们要在src\components目录下,创建一个femessage文件夹,

     

    在在femessage文件夹下创建el-form-renderer.vue 文件,(自定义名字)


    封装我们要得组件

    1. <template>
    2. <div>
    3. <el-form ref="myelForm" v-bind="$attrs" :model="value" class="el-form-renderer">
    4. <template v-for="item in innerContent" :key="item.id">
    5. <slot :name="`id:${item.id}`" />
    6. <slot :name="`$id:${item.id}`" />
    7. <component
    8. :is="item.type === GROUP ? RenderFormGroup : RenderFormItem"
    9. :ref="
    10. (el) => {
    11. customComponent[item.id] = el;
    12. }
    13. "
    14. :data="item"
    15. :value="value"
    16. :item-value="value[item.id]"
    17. :disabled="
    18. disabled ||
    19. (typeof item.disabled === 'function' ? item.disabled(value) : item.disabled)
    20. "
    21. :readonly="readonly || item.readonly"
    22. :options="options[item.id]"
    23. @updateValue="updateValue"
    24. />
    25. </template>
    26. <slot />
    27. </el-form>
    28. </div>
    29. </template>
    30. <script setup>
    31. import RenderFormGroup from "./components/render-form-group.vue";
    32. import RenderFormItem from "./components/render-form-item.vue";
    33. import {
    34. reactive,
    35. computed,
    36. ref,
    37. watch,
    38. onMounted,
    39. nextTick,
    40. provide,
    41. getCurrentInstance,
    42. } from "vue";
    43. import transformContent from "./util/transform-content";
    44. import _set from "lodash.set";
    45. import _isequal from "lodash.isequal";
    46. import _clonedeep from "lodash.clonedeep";
    47. import {
    48. collect,
    49. mergeValue,
    50. transformOutputValue,
    51. transformInputValue,
    52. correctValue,
    53. } from "./util/utils";
    54. let GROUP = "group";
    55. /**
    56. * inputFormat 让整个输入机制复杂了很多。value 有以下输入路径:
    57. * 1. 传入的 form => inputFormat 处理
    58. * 2. updateForm => inputFormat 处理
    59. * 3. 但 content 中的 default 没法经过 inputFormat 处理,因为 inputFormat 要接受整个 value 作为参数
    60. * 4. 组件内部更新 value,不需要走 inputFormat
    61. */
    62. let value = reactive({}); // 表单数据对象
    63. let options = reactive({});
    64. let initValue = reactive({});
    65. let myelForm = ref();
    66. let methods = {};
    67. const customComponent = ref([]);
    68. let emit = defineEmits(["update:FormData"]);
    69. // 注入 element ui form 方法
    70. /**
    71. * 与 element 相同,在 mounted 阶段存储 initValue
    72. * @see https://github.com/ElemeFE/element/blob/6ec5f8e900ff698cf30e9479d692784af836a108/packages/form/src/form-item.vue#L304
    73. */
    74. onMounted(async () => {
    75. initValue = _clonedeep(value);
    76. await nextTick();
    77. // 检查 myelForm 是否已经初始化
    78. if (myelForm && myelForm.value) {
    79. Object.keys(myelForm.value).forEach((item) => {
    80. // 检查属性是否存在于 methods 对象中
    81. if (myelForm.value[item] && !(item in methods)) {
    82. methods[item] = myelForm.value[item];
    83. }
    84. });
    85. }
    86. /**
    87. * 有些组件会 created 阶段更新初始值为合法值,这会触发 validate。目前已知的情况有:
    88. * - el-select 开启 multiple 时,会更新初始值 undefined 为 []
    89. * @hack
    90. */
    91. methods.clearValidate();
    92. });
    93. let props = defineProps({
    94. //表单项
    95. content: {
    96. type: Array,
    97. required: true,
    98. },
    99. // 禁用
    100. disabled: {
    101. type: [Boolean, Function],
    102. default: false,
    103. },
    104. //只读
    105. readonly: {
    106. type: Boolean,
    107. default: false,
    108. },
    109. /**
    110. * v-model 的值。传入后会优先使用
    111. */
    112. FormData: {
    113. type: Object,
    114. default: undefined,
    115. },
    116. });
    117. //兼容处理
    118. let innerContent = computed(() => transformContent(props.content));
    119. // 初始化默认值
    120. let setValueFromModel = () => {
    121. if (innerContent.length) return;
    122. /**
    123. * 没使用 v-model 时才从 default 采集数据
    124. * default 值没法考虑 inputFormat
    125. * 参考 value-format.md 的案例。那种情况下,default 该传什么?
    126. */
    127. let newValue = props.FormData
    128. ? transformInputValue(props.FormData, innerContent.value)
    129. : collect(innerContent.value, "default");
    130. correctValue(newValue, innerContent.value);
    131. if (!_isequal(value, newValue)) value = Object.assign(value, newValue);
    132. };
    133. // v-model初始化默认数据
    134. watch(
    135. () => props.FormData,
    136. (newForm) => {
    137. if (!newForm) return;
    138. setValueFromModel();
    139. },
    140. { immediate: true, deep: true }
    141. );
    142. // 初始化默认数据
    143. watch(
    144. innerContent,
    145. (newContent) => {
    146. try {
    147. if (!newContent) return;
    148. // 如果 content 没有变动 remote 的部分,这里需要保留之前 remote 注入的 options
    149. Object.assign(options, collect(newContent, "options"));
    150. setValueFromModel();
    151. } catch (error) {
    152. console.log(error);
    153. }
    154. },
    155. { immediate: true }
    156. );
    157. // v-model 传递值
    158. watch(value, (newValue, oldValue) => {
    159. try {
    160. if (!newValue) return;
    161. if (props.FormData) {
    162. let data = Object.assign(
    163. props.FormData,
    164. transformOutputValue(newValue, innerContent)
    165. );
    166. emit("update:FormData", data);
    167. }
    168. } catch (error) {
    169. console.log(error, "-----");
    170. }
    171. // deep: true, // updateValue 是全量更新,所以不用
    172. });
    173. /**
    174. * 更新表单数据
    175. * @param {String} options.id 表单ID
    176. * @param {All} options.value 表单数据
    177. */
    178. let updateValue = ({ id, value: v }) => {
    179. value[id] = v;
    180. };
    181. /**
    182. * 重置表单为初始值
    183. *
    184. * @public
    185. */
    186. let resetFields = async () => {
    187. /**
    188. * 之所以不用 el-form 的 resetFields 机制,有以下原因:
    189. * - el-form 的 resetFields 无视 el-form-renderer 的自定义组件
    190. * - el-form 的 resetFields 不会触发 input & change 事件,无法监听
    191. * - bug1: https://github.com/FEMessage/el-data-table/issues/176#issuecomment-587280825
    192. * - bug2:
    193. * 0. 建议先在监听器 watch.value 里 console.log(v.name, oldV.name)
    194. * 1. 打开 basic 示例
    195. * 2. 在 label 为 name 的输入框里输入 1,此时 log:'1' ''
    196. * 3. 点击 reset 按钮,此时 log 两条数据: '1' '1', '' ''
    197. * 4. 因为 _isequal(v, oldV),所以没有触发 v-model 更新
    198. */
    199. value = _clonedeep(initValue);
    200. await nextTick();
    201. methods.clearValidate();
    202. };
    203. /**
    204. * 当 strict 为 true 时,只返回设置的表单项的值, 过滤掉冗余字段, 更多请看 update-form 示例
    205. * @param {{strict: Boolean}} 默认 false
    206. * @return {object} key is item's id, value is item's value
    207. * @public
    208. */
    209. let getFormValue = ({ strict = false } = {}) => {
    210. return transformOutputValue(value, innerContent, { strict });
    211. };
    212. /**
    213. * update form values
    214. * @param {object} newValue - key is item's id, value is the new value
    215. * @public
    216. */
    217. let updateForm = (newValue) => {
    218. newValue = transformInputValue(newValue, innerContent);
    219. mergeValue(value, newValue, innerContent);
    220. };
    221. /**
    222. * update select options
    223. * @param {string} id
    224. * @param {array} options
    225. * @public
    226. */
    227. let setOptions = (id, O) => {
    228. _set(options, id, O);
    229. options = Object.assign(options); // 设置之前不存在的 options 时需要重新设置响应式更新
    230. };
    231. /**
    232. * get custom component
    233. * @param {string} id
    234. * @public
    235. */
    236. const getComponentById = (id) => {
    237. let content = [];
    238. props.content.forEach((item) => {
    239. if (item.type === GROUP) {
    240. const items = item.items.map((formItem) => {
    241. formItem.groupId = item.id;
    242. return formItem;
    243. });
    244. content.push(...items);
    245. } else {
    246. content.push(item);
    247. }
    248. });
    249. const itemContent = content.find((item) => item.id === id);
    250. if (!itemContent) {
    251. return undefined;
    252. }
    253. if (!itemContent.groupId) {
    254. return customComponent.value[id].customComponent;
    255. } else {
    256. const componentRef = customComponent.value[itemContent.groupId].customComponent;
    257. return componentRef[`formItem-${id}`].customComponent;
    258. }
    259. };
    260. provide("methods", methods);
    261. provide("updateForm", updateForm);
    262. provide("setOptions", setOptions);
    263. defineExpose({
    264. updateValue,
    265. resetFields,
    266. getFormValue,
    267. updateForm,
    268. setOptions,
    269. methods,
    270. getComponentById,
    271. });
    272. </script>
    273. <script>
    274. export default {
    275. name: "ElFormRenderer",
    276. };
    277. </script>

    导出组件 

    src 根目录中创建index.js文件,代码如下:

    1. import elFormRenderer from "./components/femessage/el-form-renderer.vue"; // 引入封装好的组件
    2. export { elFormRenderer }; //实现按需引入*
    3. const coms = [elFormRenderer]; // 将来如果有其它组件,都可以写到这个数组里
    4. const components = [elFormRenderer];
    5. const install = function (App, options) {
    6. components.forEach((component) => {
    7. App.component(component.name, component);
    8. });
    9. };
    10. export default { install }; // 批量的引入*

    使用vite构建
    编辑vite.config.js文件,新增build属性 vite中文文档icon-default.png?t=N7T8https://cn.vitejs.dev/guide/build.html#library-mode

    1. build:这是一个包含构建选项的对象。

    2. lib:这是构建库的选项。

      • entry:指定了库的入口文件,通常是一个 JavaScript 文件的路径。
      • name:定义了库的名称,这里是 "el-form-renderer-vue3"。
      • fileName:定义了输出文件的命名规则,使用了一个函数来生成不同格式的文件名。
    3. rollupOptions:这是用于 Rollup 构建工具的选项,Rollup 通常用于打包 JavaScript 库。

      • external:指定了需要排除的外部依赖项,这里只有 "vue"。

      • output:定义了输出选项。

    • globals:在 UMD 构建模式下,指定了外部依赖的全局变量名,这里将 "vue" 映射到 "Vue",以确保在使用库时可以访问到 Vue.js。

    1. import { defineConfig } from "vite";
    2. import vue from "@vitejs/plugin-vue";
    3. import path from "path";
    4. // https://vitejs.dev/config/
    5. export default defineConfig({
    6. plugins: [vue()],
    7. build: {
    8. lib: {
    9. entry: path.resolve(__dirname, "src/index.js"),
    10. name: "el-form-renderer-vue3",
    11. fileName: (format) => `el-form-renderer-vue3.${format}.js`,
    12. },
    13. rollupOptions: {
    14. // 确保外部化处理那些你不想打包进库的依赖
    15. external: ["vue"],
    16. output: {
    17. // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
    18. globals: {
    19. vue: "Vue",
    20. },
    21. },
    22. },
    23. },
    24. });

    修改package.json文件

    1. {
    2. "name": "el-form-renderer-vue3",
    3. "version": "1.0.1",
    4. "type": "module",
    5. "scripts": {
    6. "dev": "vite",
    7. "build": "vite build",
    8. "preview": "vite preview"
    9. },
    10. "files": [
    11. "dist"
    12. ],
    13. "main": "./dist/el-form-renderer-vue3.umd.js",
    14. "module": "./dist/el-form-renderer-vue3.es.js",
    15. "exports": {
    16. ".": {
    17. "import": "./dist/el-form-renderer-vue3.es.js",
    18. "require": "./dist/el-form-renderer-vue3.umd.js"
    19. }
    20. },
    21. "dependencies": {
    22. "@element-plus/icons-vue": "^2.1.0",
    23. "axios": "^1.5.1",
    24. "element-plus": "^2.3.14",
    25. "lodash.clonedeep": "^4.5.0",
    26. "lodash.frompairs": "^4.0.1",
    27. "lodash.get": "^4.4.2",
    28. "lodash.has": "^4.5.2",
    29. "lodash.includes": "^4.3.0",
    30. "lodash.isequal": "^4.5.0",
    31. "lodash.isplainobject": "^4.0.6",
    32. "lodash.kebabcase": "^4.1.1",
    33. "lodash.set": "^4.3.2",
    34. "lodash.topairs": "^4.3.0",
    35. "vue": "^3.3.4",
    36. "vue-router": "4"
    37. },
    38. "devDependencies": {
    39. "@vitejs/plugin-vue": "^4.2.3",
    40. "vite": "^4.4.5"
    41. }
    42. }

     

    打包 

    当我们都配置好以后,我们就要打包了,这是我们要上传得文件

    打包,生成dist文件

    npm run build
    

     

    注册npm账号 官网地址

    想要发布到npm仓库,就必须要有一个账号,先去npm官网注册一个账号,注意记住用户名、密码和邮箱,发布的时候可能会用到
    有些小伙伴可能本地的npm镜像源采用的是淘宝镜像源或者其它的,如果想要发布npm包,我们得吧我们得npm源切换为官方得源,命令如下:
     

    npm config set registry=https://registry.npmjs.org
    

     发布前准备
    dist文件生成package.json文件,自定义组件名(唯一,重名报错重新起一个就行),版本号每次上传要高于前一次版本号
    dist根目录中运行:

    npm init -y
    
    1. {
    2. "name": "el-form-renderer-vue3",
    3. "version": "1.0.2",
    4. "description": "",
    5. "main": "el-form-renderer-vue3.es.js",
    6. "scripts": {
    7. "test": "echo \"Error: no test specified\" && exit 1"
    8. },
    9. "keywords": [],
    10. "author": "",
    11. "license": "ISC"
    12. }

    登录npm用户 

    npm login
    
    • Username 用户名
    • Password 密码
    • Email 邮箱地址
    • Enter one-time password 验证码

    执行发布命令

    npm publish
    

    已经上传成功 

     使用组件

     当我们要在项目中使用的时候就复制 npm i el-form-renderer-vue3

    package.json文件中就有了我们安装的组件

    这个时候只要像element ui 那样引入就可以全局使用了,在main.js中引入

    在我们要用到得.vue中使用 

    发布包

    创建npm账户

    如果你还没有npm账户,你需要先创建一个。前往npm官方网站注册一个账户。

    注意:记住你填写的用户名、邮箱、密码,等下你在本地是需要用这些信息登录的。

    登录npm

    在本地登录你刚刚注册的账号

    如果是npm,最后还会给你的邮箱发个验证码,填上之后再回车,才算真正登录成功。

    执行 npm login,输入用户名、密码以及你注册时的邮箱。

    使用以下命令在终端中登录到你的npm账户:

    npm login
    

     这将提示你输入你的npm用户名、密码和邮箱地址。

    切换npm源

    因为我们要发布到官方源上面,所以要确保源地址为官方地址 http://registry.npmjs.org 或 https://registry.npmjs.com

    可以通过 npm config get registry 命令查看当前 registry 源。

    推荐使用nrm管理本地npm源。

    全局安装 nrm:  npm install -g nrm

    查看npm源:nrm ls
    会把所有的源都列出来,其中带*的是当前使用的源

    添加npm源:nrm add xxx http://xxxnpm.cn/
    xxx 是你给这个源起的名字,后面跟上源的URL 

     删除源:nrm del xxx
    xxx 是你给这个源起的名字

    发布到npm

    最后,运行以下命令来发布你的Vue 3组件到npm:

    npm publish
    

    package.json

    创建一个Node.js项目时,package.json文件是非常重要的,它包含了项目的配置信息和依赖项。下面是一个典型的package.json文件及其各个字段的介绍:

    package.json字段

    • name: 项目的名称,应该是唯一的。
    • version: 项目的版本号,遵循语义化版本规则。
    • description: 项目的简要描述。
    • main: 指定项目的入口文件。
    • scripts: 自定义命令脚本,例如,你可以运行 npm start 启动应用。
    • keywords: 一组关键字,有助于其他人找到你的项目。
    • author: 作者的信息。
    • license: 项目的许可证,常见的包括 MIT、Apache-2.0、GPL-3.0 等。
    • repository: 指定项目的代码仓库信息,包括仓库类型和URL。
    • bugs: 定义问题跟踪系统的URL,以及可选的问题报告邮箱。
    • homepage: 项目的主页URL,通常是项目在代码托管平台上的页面。

    接下来是依赖项:

    • dependencies: 生产依赖项,这些包在生产环境中需要。
    • devDependencies: 开发依赖项,这些包在开发和测试过程中需要,但不会包含在生产环境中。
    1. {
    2. "name": "my-node-app",
    3. "version": "1.0.0",
    4. "description": "My Node.js Application",
    5. "main": "index.js",
    6. "scripts": {
    7. "start": "node index.js",
    8. "test": "mocha"
    9. },
    10. "keywords": ["node", "javascript"],
    11. "author": "Your Name",
    12. "license": "MIT",
    13. "repository": {
    14. "type": "git", // 仓库类型
    15. "url": "https://github.com/yourusername/my-node-app.git" // 仓库 URL
    16. },
    17. "bugs": {
    18. "url": "https://github.com/yourusername/my-node-app/issues", // 问题跟踪系统 URL
    19. "email": "youremail@example.com" // 可选的问题报告邮箱
    20. },
    21. "homepage": "https://github.com/yourusername/my-node-app", // 项目主页 URL
    22. "dependencies": {
    23. "express": "^4.17.1",
    24. "body-parser": "^1.19.0"
    25. },
    26. "devDependencies": {
    27. "mocha": "^8.0.1",
    28. "chai": "^4.2.0"
    29. }
    30. }

    要发布一个npm 包,name 和 version 字段是必填的;

    包名(name 字段)命名规则:

    • 包名长度不能超过 214 个字符(命名空间也算在里面);
    • 包名所有字符必须小写;
    • 包名可以由连字符 - 组成;
    • 包名不能包含空格,不能以 . 或者 _ 开头,不能包含 ~)('!* 中的任意一个字符;
    • 包名不能包含任何非 url 安全字符(因为包名将作为 url 的一部分);
    • 包名不能与 Node.js / io.js 的核心模块、保留字或黑名单相同,例如 http。

    版本号(version 字段)则需要遵循 semver 规范。
     

    确定版本号

    填写好 package.json 字段后,接下来就是确定我们要发布的版本号,每次对包的更改都应该对应一个版本。

    版本号格式

    格式:MAJOR.MINOR.PATCH ,值非负整数,且禁止在数字前面补 0

    • MAJOR:主版本号
    • MINOR:次版本号
    • PATCH::修订号

    版本号递增逻辑

    • 当有破坏性不兼容的 API 变更时,升级主版本号
    • 当新增一些功能特性时,升级次版本号
    • 当做一些 bug 修复时,升级修订号

    当某个版本还不稳定的时候,还可能要先发布一个先行版本,具体可看 semver 规范。

    vite 打包库模式 

    当你开发面向浏览器的库时,你可能会将大部分时间花在该库的测试/演示页面上。在 Vite 中你可以使用 index.html 获得如丝般顺滑的开发体验。

    当这个库要进行发布构建时,请使用 build.lib 配置项,以确保将那些你不想打包进库的依赖进行外部化处理,例如 vue 或 react

    库模式配置

    1. // vite.config.js
    2. import { resolve } from 'path'
    3. import { defineConfig } from 'vite'
    4. export default defineConfig({
    5. build: {
    6. lib: {
    7. // 也可以是字典或多个入口点的数组
    8. entry: resolve(__dirname, 'lib/main.js'),
    9. name: 'MyLib',
    10. // the proper extensions will be added
    11. fileName: 'my-lib',
    12. },
    13. rollupOptions: {
    14. // 确保外部化处理那些你不想打包进库的依赖
    15. external: ['vue'],
    16. output: {
    17. // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
    18. globals: {
    19. vue: 'Vue',
    20. },
    21. },
    22. },
    23. },
    24. })

    入口文件将包含可以由你的包的用户导入的导出:

    1. // lib/main.js
    2. import Foo from './Foo.vue'
    3. import Bar from './Bar.vue'
    4. export { Foo, Bar }

    使用如上配置运行 vite build 时,将会使用一套面向库的 Rollup 预设,并且将为该库提供两种构建格式:es 和 umd (可在 build.lib 中配置):

    1. $ vite build
    2. building for production...
    3. dist/my-lib.js 0.08 KiB / gzip: 0.07 KiB
    4. dist/my-lib.umd.cjs 0.30 KiB / gzip: 0.16 KiB

    荐在你库的 package.json 中使用如下格式:

    1. {
    2. "name": "my-lib",
    3. "type": "module",
    4. "files": ["dist"],
    5. "main": "./dist/my-lib.umd.cjs",
    6. "module": "./dist/my-lib.js",
    7. "exports": {
    8. ".": {
    9. "import": "./dist/my-lib.js",
    10. "require": "./dist/my-lib.umd.cjs"
    11. }
    12. }
    13. }

    或者,如果暴露了多个入口起点:

    1. {
    2. "name": "my-lib",
    3. "type": "module",
    4. "files": ["dist"],
    5. "main": "./dist/my-lib.cjs",
    6. "module": "./dist/my-lib.js",
    7. "exports": {
    8. ".": {
    9. "import": "./dist/my-lib.js",
    10. "require": "./dist/my-lib.cjs"
    11. },
    12. "./secondary": {
    13. "import": "./dist/secondary.js",
    14. "require": "./dist/secondary.cjs"
    15. }
    16. }
    17. }

    注意

    如果 package.json 不包含 "type": "module",Vite 会生成不同的文件后缀名以兼容 Node.js。.js 会变为 .mjs 而 .cjs 会变为 .js 。

    环境变量

    在库模式下,所有 import.meta.env.* 用法在构建生产时都会被静态替换。但是,process.env.* 的用法不会被替换,所以你的库的使用者可以动态地更改它。如果不想允许他们这样做,你可以使用 define: { 'process.env.NODE_ENV': '"production"' } 例如静态替换它们。

     

    build配置项

    build.lib

    • 类型: { entry: string | string[] | { [entryAlias: string]: string }, name?: string, formats?: ('es' | 'cjs' | 'umd' | 'iife')[], fileName?: string | ((format: ModuleFormat, entryName: string) => string) }

    构建为库。

    1. entry 是必需的,因为库不能使用 HTML 作为入口。
    2. name 则是暴露的全局变量,并且在 formats 包含 'umd' 或 'iife' 时是必需的。默认 formats 是 ['es', 'umd'],如果使用了多个配置入口,则是 ['es', 'cjs']
    3. fileName 是输出的包文件名,默认 fileName 是 package.json 的 name 选项,同时,它还可以被定义为参数为 format 和 entryAlias 的函数。

    build.rollupOptions

    自定义底层的 Rollup 打包配置。这与从 Rollup 配置文件导出的选项相同,并将与 Vite 的内部 Rollup 选项合并。查看 Rollup 选项文档 获取更多细节。

  • 相关阅读:
    RTT学习笔记12-串口外设和串口驱动框架
    Spring BeanDefinition详解
    单例模式java
    c# 面试题
    「运维有小邓」域密码策略强化器
    Jmeter性能测试指南
    【工具使用】卸载VS(Visual Studio)
    2022 SWPU新生赛&HNCTF web部分题目
    Spring Cloud 学习笔记(3 3)
    别再乱写git commit了
  • 原文地址:https://blog.csdn.net/qq_63358859/article/details/133808112