当前位置: 首页 > news >正文

webpack打包基本原理——实现webpack打包核心功能

webpack打包的基本原理

核心功能就是把我们写的模块化代码转换成浏览器能够识别运行的代码,话不多说我们一起来了解它

首先我们建一个空项目用 npm init -y 创建一个初始化的,在跟目录下创建src文件夹,src下创建index.js,add.js,square.js,tip.js。在根目录下创建index.html
在这里插入图片描述
index.html引入index.js,index.js引入add.js、square.js。square.js引入tip.js

//add.js
export default (a, b) => {return a + b;
};//square.js
import tip from "tip.js";
const square = (a) => {console.log(tip);return a * a;
};
export { square };//tip.js
export default "我是提示-----";

index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body></body><script src="./src/index.js"></script>
</html>

运行index.html会报错,不能识别es6的模块化导入
在这里插入图片描述
让我们来实现webpack打包的核心功能

实现webpack打包核心功能

首先在根目录下建一个bundle.js用来对刚刚写的index.js进行打包
webpack官网对打包流程的介绍

it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle)

根据上面的说明进行分析,打包的工作基本流程如下

  1. 读取入口文件中的内容(也就是index.js文件)
  2. 分析入口文件,递归读取模块所依赖的文件 内容,生成依赖图
  3. 根据依赖图生成浏览器能运行的代码

1、处理单个模块内容

const fs = require("fs");
const getModuleInfo = (file) => {const body = fs.readFileSync(file, "utf-8");//获取文件内容,是字符串console.log(body);
};
getModuleInfo("./src/index.js");

打印出来是文件内容的字符串
在这里插入图片描述
借助安装@babel/parser 把,js文件代码转换成js对象,叫做抽象语法树(ast)

const fs = require("fs");
const parser = require("@babel/parser");
const getModuleInfo = (file) => {const body = fs.readFileSync(file, "utf-8");const ast = parser.parse(body, {//表示我们要解析的是es6模块sourceType: "module",});console.log(ast.program.body);
};
getModuleInfo("./src/index.js");

打印ast.program.body的结构:

[Node {type: 'ImportDeclaration', start: 0,end: 27,loc: SourceLocation {      start: [Position],       end: [Position],filename: undefined,     identifierName: undefined},specifiers: [ [Node] ],source: Node {type: 'StringLiteral',start: 16,end: 26,loc: [SourceLocation],extra: [Object],value: './add.js'}},Node {type: 'ImportDeclaration',start: 29,end: 66,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},specifiers: [ [Node] ],source: Node {type: 'StringLiteral',start: 52,end: 65,loc: [SourceLocation],extra: [Object],value: './square.js'}},Node {type: 'VariableDeclaration',start: 68,end: 90,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},declarations: [ [Node] ],kind: 'const'},Node {type: 'VariableDeclaration',start: 92,end: 114,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},declarations: [ [Node] ],kind: 'const'},Node {type: 'ExpressionStatement',start: 116,end: 143,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},expression: Node {type: 'CallExpression',start: 116,end: 142,loc: [SourceLocation],callee: [Node],arguments: [Array]}},Node {type: 'ExpressionStatement',start: 145,end: 172,loc: SourceLocation {start: [Position],end: [Position],filename: undefined,identifierName: undefined},expression: Node {type: 'CallExpression',start: 145,end: 171,loc: [SourceLocation],callee: [Node],arguments: [Array]}}
]

type属性是ImportDeclaration的节点,其source.value属性是引入这个模块的相对路径,上面打印出两个ImportDeclaration节点,说明对应的是入口文件中的两个import

对ast.program.body做处理,本质上是对这个数组遍历,循环做处理,借助安装@babel/traverse来完成这项工作

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;const getModuleInfo = (file) => {//1、把入口文件字符串ast对象const body = fs.readFileSync(file, "utf-8");const ast = parser.parse(body, {//表示我们要解析的是es6模块sourceType: "module",});//2、获取类型为ImportDeclaration的所依赖模块的信息地址const deps = {};//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径traverse(ast, {ImportDeclaration({ node }) {const dirname = path.dirname(file);let absPath = "./" + path.join(dirname, node.source.value);absPath = absPath.replace("\\", "/");deps[node.source.value] = absPath;},});console.log(deps);
};
getModuleInfo("./src/index.js");

deps打印出入口文件引入的依赖地址
在这里插入图片描述
获取依赖后,需要对ast做语法转换,把es6转成es5的语法,安装@babel/core以及@babel/preset-env完成

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");const getModuleInfo = (file) => {//1、把入口文件字符串ast对象const body = fs.readFileSync(file, "utf-8");const ast = parser.parse(body, {//表示我们要解析的是es6模块sourceType: "module",});//2、获取类型为ImportDeclaration的所依赖模块的信息地址const deps = {};//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径traverse(ast, {ImportDeclaration({ node }) {const dirname = path.dirname(file);let absPath = "./" + path.join(dirname, node.source.value);absPath = absPath.replace("\\", "/");deps[node.source.value] = absPath;},});//3、把es6语法转换成es5const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],});const moduleInfo = { file, deps, code };console.log(moduleInfo);return moduleInfo;
};
getModuleInfo("./src/index.js");

moduleInfo 打印结果为:
在这里插入图片描述
最终把一个模块的代码转化为一个对象形式的信息,这个对象包含文件的绝对路径,文件所依赖模块的信息,以及模块内部经过babel转化后的代码

接下去需要递归查找所有的模块,比如square.js里引用了tip.js。
这个过程也是获取依赖图(dependency graph)的过程

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");const getModuleInfo = (file) => {//1、把入口文件字符串ast对象const body = fs.readFileSync(file, "utf-8");const ast = parser.parse(body, {//表示我们要解析的是es6模块sourceType: "module",});//2、获取类型为ImportDeclaration的所依赖模块的信息地址const deps = {};//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径traverse(ast, {ImportDeclaration({ node }) {const dirname = path.dirname(file);let absPath = "./" + path.join(dirname, node.source.value);absPath = absPath.replace("\\", "/");deps[node.source.value] = absPath;},});//3、把es6语法转换成es5const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],});const moduleInfo = { file, deps, code };return moduleInfo;
};//4、递归获取所有模块信息和之间的依赖关系
const parseModules = (file) => {//定义依赖图const depsGraph = {};// 首先获取入口的信息const entry = getModuleInfo(file);const temp = [entry];for (let i = 0; i < temp.length; i++) {const item = temp[i];const deps = item.deps;if (deps) {//遍历模块的依赖,递归获取模块信息for (const key in deps) {if (deps.hasOwnProperty(key)) {temp.push(getModuleInfo(deps[key]));}}}}temp.forEach((moduleInfo) => {depsGraph[moduleInfo.file] = {deps: moduleInfo.deps,code: moduleInfo.code,};});console.log(depsGraph);return depsGraph;
};
parseModules("./src/index.js");

获得的depsGraph对象如下:

{file: './src/index.js',deps: { './add.js': './src/add.js', './square.js': './src/square.js' },code: '"use strict";\n' +'\n' +'var _add = _interopRequireDefault(require("./add.js"));\n' +'var _square = require("./square.js");\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'var sum = (0, _add["default"])(2, 3);\n' +'var sqr = (0, _square.square)(4);\n' +'console.log("sum===", sum);\n' +'console.log("sqr===", sqr);'
}
PS C:\Users\keyuan04\Desktop\webpack> node bundle.js
{'./src/index.js': {deps: { './add.js': './src/add.js', './square.js': './src/square.js' },code: '"use strict";\n' +'\n' +'var _add = _interopRequireDefault(require("./add.js"));\n' +'var _square = require("./square.js");\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'var sum = (0, _add["default"])(2, 3);\n' +'var sqr = (0, _square.square)(4);\n' +'console.log("sum===", sum);\n' +'console.log("sqr===", sqr);'},'./src/add.js': {deps: {},code: '"use strict";\n' +'\n' +'Object.defineProperty(exports, "__esModule", {\n' +'  value: true\n' +'});\n' +'exports["default"] = void 0;\n' +'var _default = function _default(a, b) {\n' +'  return a + b;\n' +'};\n' +'exports["default"] = _default;'},'./src/square.js': {deps: { 'tip.js': './src/tip.js' },code: '"use strict";\n' +'\n' +'Object.defineProperty(exports, "__esModule", {\n' +'  value: true\n' +'});\n' +'exports.square = void 0;\n' +'var _tip = _interopRequireDefault(require("tip.js"));\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'var square = function square(a) {\n' +'  console.log(_tip["default"]);\n' +'  return a * a;\n' +'};\n' +'exports.square = square;'},'./src/tip.js': {deps: {},code: '"use strict";\n' +'\n' +'Object.defineProperty(exports, "__esModule", {\n' +'  value: true\n' +'});\n' +'exports["default"] = void 0;\n' +'var _default = "我是提示-----";\n' +'exports["default"] = _default;'}
}

我们最终得到的模块分析数据如上图所示,接下来,我们就要根据这里获得的模块分析数据,来生产最终浏览器运行的代码。

上面打印的依赖图可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理,把依赖图对象中的内容转换成能够执行的代码,以字符串形式输出。 我们把整个代码放在自执行函数中,参数是依赖图对象
把取得入口文件 的code信息,去执行。使用eval函数执行。
最后把生成的内容写入到dist文件中

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");const getModuleInfo = (file) => {//1、把入口文件字符串ast对象const body = fs.readFileSync(file, "utf-8");const ast = parser.parse(body, {//表示我们要解析的是es6模块sourceType: "module",});//2、获取类型为ImportDeclaration的所依赖模块的信息地址const deps = {};//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径traverse(ast, {ImportDeclaration({ node }) {const dirname = path.dirname(file);let absPath = "./" + path.join(dirname, node.source.value);absPath = absPath.replace("\\", "/");deps[node.source.value] = absPath;},});//3、把es6语法转换成es5const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],});const moduleInfo = { file, deps, code };return moduleInfo;
};//4、递归获取所有模块信息和之间的依赖关系
const parseModules = (file) => {//定义依赖图const depsGraph = {};// 首先获取入口的信息const entry = getModuleInfo(file);const temp = [entry];for (let i = 0; i < temp.length; i++) {const item = temp[i];const deps = item.deps;if (deps) {//遍历模块的依赖,递归获取模块信息for (const key in deps) {if (deps.hasOwnProperty(key)) {temp.push(getModuleInfo(deps[key]));}}}}temp.forEach((moduleInfo) => {depsGraph[moduleInfo.file] = {deps: moduleInfo.deps,code: moduleInfo.code,};});console.log(depsGraph);return depsGraph;
};//生成最终代码
const bundle = (file) => {//把依赖图转字符串,放在自执行函数中执行const depsGraph = JSON.stringify(parseModules(file));return `(function(graph){function require(file){var exports = {};function absRequire(relPath){return require(graph[file].deps[relPath])}(function(require,exports,code){eval(code)})(absRequire,exports,graph[file].code)return exports}require('${file}')})(${depsGraph})`;
};const content = bundle("./src/index.js");
fs.rmdirSync("./dist", { recursive: true });
// 写入到dist/bundle.js
fs.mkdirSync("./dist");
fs.writeFileSync("./dist/build.js", content);

最后在index.html中入这个./dist/bundle.js,在控制台就可以看到正确的输出结果
在这里插入图片描述

相关文章:

webpack打包基本原理——实现webpack打包核心功能

webpack打包的基本原理 核心功能就是把我们写的模块化代码转换成浏览器能够识别运行的代码&#xff0c;话不多说我们一起来了解它 首先我们建一个空项目用 npm init -y 创建一个初始化的&#xff0c;在跟目录下创建src文件夹&#xff0c;src下创建index.js&#xff0c;add.js…...

git的使用(终端输入指令) 上

git目录前言1.创建仓库2.创建文件和修改数据状态分区![分区](https://img-blog.csdnimg.cn/d124dec6b2b14769ad20b75490f29cae.png)3 .删除、撤销重置 、和比较前言 今天带大家手把手敲一遍 git 流程&#xff1a; 安装一下git&#xff08;详细观看我之前发的git文档&#xff0…...

react定义css样式,使用less,css模块化

引入外部 css文件 import ./index.css此时引入的样式是全局样式 使用less 安装 npm i style-loader css-loader sass-loader node-sass -D生成config文件夹 npm run eject配置 以上代码运行完&#xff0c;会在根目录生成config文件夹 进入 config > webpack.config.js 查找…...

基于JavaWeb的学生管理系统

文章目录 项目介绍主要功能截图:登录用户信息管理院系信息管理班级信息管理新增学生课程管理成绩管理部分代码展示设计总结项目获取方式🍅 作者主页:Java韩立 🍅 简介:Java领域优质创作者🏆、 简历模板、学习资料、面试题库【关注我,都给你】 🍅文末获取源码联系�…...

win11右键新建菜单添加选项

需要操作 2 处注册表&#xff0c; 以下以在右键新建菜单中添加 .html 为例 在主键 HKEY_CLASSES_ROOT 中&#xff0c;搜索 .html 找到后 &#xff0c;右键点击它&#xff0c;选 新建 ->项&#xff0c; 在这里插入图片描述 项目名字是&#xff1a;ShellNew 新建后&#x…...

leetcode Day5(卡线复试,放弃版)

Day5 最后一个单词长度&#xff08;要求最后一个&#xff0c;可以反向计数&#xff09; int lens.length()-1; while(s.charAt(len)){len--;//最后是一个空格&#xff0c;就是无字符时 } int wordlen0;//记录字符长度 /*charAt() 方法用于返回指定索引处的字符。索引范围为从 0…...

cmake 入门二 库的编译,安装与使用

工程描述 &#xff11;&#xff0c;建立一个静态库和动态库&#xff0c;提供HelloFunc 函数供其他程序编程使用&#xff0c;HelloFunc 向终端输出Hello World字符串。 &#xff12;&#xff0c;安装头文件与共享库。 1 库的工程结构 1.1 工程目录下的CMakeLists.txt PROJECT…...

Python中实现将内容进行base64编码与解码

一、需求说明需要使用Python实现将内容转为base64编码&#xff0c;解码&#xff0c;方便后续的数据操作。二、base64简介Base64是一种二进制到文本的编码方式【是一种基于 64 个可打印字符来表示二进制数据的表示方法&#xff08;由于 2^664&#xff0c;所以每 6 个比特为一个单…...

集合TreeSet的使用-java

TreeSet的特点&#xff1a;可排序、不重复、无索引。可排序&#xff1a;按照元素的大小默认升序排序&#xff1b;底层是基于红黑树的数据结构实现排序的&#xff0c;增删改查性能都较好。对于数值、字符串类型的&#xff08;Integer 、Double、String&#xff09;TreeSet可以排…...

Mybatis-plus 分页集成以及基本使用总结 入门和案例 注解连表查询分页案例等

简介 Mybaits-plus 是mybits 的升级版&#xff0c;从mybaits 升级到mybaits-plus 可以实现平滑升级 Mybaits-plus 本身提供了大量的基本查询方法以及强大的 Wrapper(包装) 类 用于查询的 QueryWrapper 以及 更新的 UpdateWrapper &#xff0c;使用Wrapper 基本已经可以构建大…...

5个设计师常用素材库

推荐5个设计素材网站&#xff0c;免费下载&#xff01; 1、菜鸟图库 菜鸟图库-免费设计素材下载 菜鸟图库是一个素材量非常丰富的网站&#xff0c;该网站聚合了平面、UI、淘宝电商、高清背景图、图片、插画等高质量素材。平面设计模板非常多&#xff0c;很多都能免费下载&…...

PHP/7.2.11 缺少 apache2/logs/httpd.pid 文件

启动服务时&#xff1a;systemctl restart httpd.service&#xff0c;报错&#xff1a;● httpd.service - httpd serviceLoaded: loaded (/etc/systemd/system/httpd.service; enabled; vendor preset: disabled)Active: failed (Result: exit-code) since 五 2023-02-24 16:1…...

【centos7下部署mongodb】

一.安装环境 CentOS7MongoDB4.0.13正式版。 二.下载MongoDB 1.1 官网下载地址&#xff1a;https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.13.tgz 1.2 将压缩包通过xftp上传到服务器/opt目录&#xff0c;然后解压、改名 三. 配置环境变量及配置文件 3.1配置系…...

pycharm首次使用爬虫框架scrapy遇到的问题和解决方法(二)

在首次使用scrapy框架的过程中,一直是对着别人的框架步骤撸代码的。然而,撸着撸着发现好像别人的也用不了。后面就只能自己找踏坑了。 问题报错: 1,IndexError: list index out of range 2,pymysql.err.ProgrammingError: (1064, "You have an error in your SQL s…...

pyflink学习笔记(二):table_apisql

Joins Inner Join 官网说明&#xff1a;和 SQL 的 JOIN 子句类似。关联两张表。两张表必须有不同的字段名&#xff0c;并且必须通过 join 算子或者使用 where 或 filter 算子定义至少一个 join 等式连接谓词。先创建2个表&#xff0c;两个表的字段是相同的&#xff0c;我想验证…...

嵌入式 STM32 实现STemwin移植+修改其配置文件,驱动LCD显示文本 (含源码,建议收藏)

目录 一、STemwin 简介 二、源码下载 1、在移植STemwin源码之前&#xff0c;需要一个已经具备LCD读写&#xff0c;填充指定颜色等函数功能的一个工程&#xff1b; 2、STemwin 3、源码下载 三、STemwin移植 1、解压源码路径 2、STemwin文件介绍 四、修改配置文件&…...

[计算机网络(第八版)]第一章 概述(学习笔记)

1.1 计算机网络在信息时代中的作用 21世纪是以网络为核心的信息时代&#xff0c;21世纪的重要重要特征&#xff1a;数字化、网络化与信息化。 三大类网络 电信网络&#xff1a;向用户提供电话、电报、传真等服务&#xff1b;有线电视网络&#xff1a;向用户传送各种电视节目&am…...

AI绘图:常用镜头和视角

镜头 Establishing Shot 通过宽广或超宽广的视角交待故事发生的大地理环境 Master Shot 众多人物都能完整地出现在镜头里。 Wide Shot 广角镜头。又称“长镜头” Long Shot。人物全身展现&#xff0c;但在画面中所占比例相对较少 Ultrawide Shot 超广角镜头 Low Angle Sho…...

TCP四次挥手

TCP 四次挥手过程是怎样的&#xff1f; TCP 断开连接是通过四次挥手方式。 双方都可以主动断开连接&#xff0c;断开连接后主机中的「资源」将被释放&#xff0c;四次挥手的过程如下图&#xff1a; 客户端打算关闭连接&#xff0c;此时会发送一个 TCP 首部 FIN 标志位被置为 1…...

Tomcat源码分析-类加载器

类加载器 在分析 tomcat 类加载之前&#xff0c;我们简单的回顾下 java 体系的类加载器 启动类加载器&#xff08;Bootstrap ClassLoader)&#xff1a;加载对象是java的核心类库&#xff0c;把一些的 java 类加载到 jvm 中&#xff0c;它并不是我们熟悉的 ClassLoader&#x…...

昇腾CANN amct:模型压缩工具的量化和部署实践

amct&#xff08;Ascend Model Compression Toolkit&#xff09;是 CANN 内置的模型压缩工具&#xff0c;不是 AtomGit 上的独立开源仓库——它在 CANN AOE 调优引擎里作为一个子模块运行。amct 做三件事&#xff1a;量化&#xff08;INT8/FP16&#xff09;、剪枝&#xff08;结…...

调查研究-141 全球机器人产业深度调研报告【03篇】机器人产业六大利润池:从核心零部件到软件平台的商业逻辑

TL;DR 场景&#xff1a;关注机器人产业商业模式、利润分配和投资机会的投资者、从业者、分析人士结论&#xff1a;机器人产业利润集中在核心零部件&#xff08;减速器/伺服/电机&#xff09;、软件AI平台和医疗机器人耗材&#xff1b;本体和集成利润率有限产出&#xff1a;六大…...

向量化映射框架优化图着色问题的FPGA实现

1. 问题背景与核心挑战图着色问题作为组合优化领域的经典NP难问题&#xff0c;在集成电路布局分解、寄存器分配、逻辑最小化等场景中具有广泛应用。传统Ising机采用独热编码&#xff08;one-hot encoding&#xff09;方案&#xff0c;将每个节点的q种颜色状态映射为q个物理比特…...

AI赋能·精准适配——API风险监测系统筑牢教育数据流转安全防线

一、概要提示&#xff1a;本文围绕数据流转安全与静态数据安全的核心差异&#xff0c;结合教育行业数字化转型特性&#xff0c;系统阐述API风险监测系统的核心逻辑、核心能力、常见疑问及发展趋势&#xff0c;全面呈现系统在教育场景中的数据化应用成效&#xff0c;凸显“AI赋能…...

Go语言实现DCI架构:用角色扮演解耦对象行为与数据

1. 从“是什么”到“做什么”&#xff1a;DCI架构如何重塑对象行为建模在面向对象编程的世界里&#xff0c;我们总在试图用代码“复刻”现实。一个“人”是什么&#xff1f;我们定义一个People类&#xff0c;拥有姓名、年龄等属性。这个人能做什么&#xff1f;我们为People类添…...

AI时代中小企业还要不要上ERP?2026年最新思考

最近DeepSeek爆火&#xff0c;AI Agent层出不穷&#xff0c;不少老板问我&#xff1a;都2026年了&#xff0c;AI这么厉害&#xff0c;中小企业还有必要上ERP吗&#xff1f;我的答案是&#xff1a;不仅要上&#xff0c;而且要上得更聪明。一、AI再强&#xff0c;也替代不了ERP的…...

WarcraftHelper:5分钟解决魔兽争霸III现代兼容性问题的终极指南

WarcraftHelper&#xff1a;5分钟解决魔兽争霸III现代兼容性问题的终极指南 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper 还在为经典魔兽争霸III在W…...

Unity+C#开发万人MMO服务器的实战架构与同步优化

1. 这不是“写个服务器”那么简单&#xff1a;先撕开“万人在线”的真实含义很多人看到“UnityC#开发万人MMO服务器”这个标题&#xff0c;第一反应是&#xff1a;“哦&#xff0c;用Unity做客户端&#xff0c;C#写个后端&#xff0c;Socket连一连&#xff0c;再加个数据库&…...

小红书内容管理困境与XHS-Downloader的优雅解决方案

小红书内容管理困境与XHS-Downloader的优雅解决方案 【免费下载链接】XHS-Downloader 小红书&#xff08;XiaoHongShu、RedNote&#xff09;链接提取/作品采集工具&#xff1a;提取账号发布、收藏、点赞、专辑作品链接&#xff1b;提取搜索结果作品、用户链接&#xff1b;采集小…...

5大核心功能深度解析:如何用wvp-GB28181-pro构建企业级视频监控系统

5大核心功能深度解析&#xff1a;如何用wvp-GB28181-pro构建企业级视频监控系统 【免费下载链接】wvp-GB28181-pro 基于GB28181-2016、部标808、部标1078标准实现的开箱即用的网络视频平台。自带管理页面&#xff0c;支持NAT穿透&#xff0c;支持海康、大华、宇视等品牌的IPC、…...