实现一个聊天室可发送消息语音图片视频表情包(任意文件)

news/2024/10/22 5:05:18/

文章目录

  • 如何跑通
  • 代码仓库地址
  • 客户端
    • 登录
    • 发送消息
    • 接受消息
    • 发送文件
    • 接受文件
  • 服务端
    • 接受消息并发送给各个客户端
    • 接受文件并发送给各个客户端

如何跑通

  • 将手机和电脑都连自己的热点
  • 先运行服务器得到可监听的地址
  • 更新客户端安卓消息线程和文件线程的socker目标地址为可监听地址
  • 然后数据线连接手机运行,此时手机便多了个app,然后可以不需要数据线单独运行了

代码仓库地址

https://github.com/FULLK/llkchatroom/

客户端

登录

输入用户名。获取输入的用户名和通信IP

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);input_name = (TextView) findViewById(R.id.input_name);Button confirm = (Button) findViewById(R.id.confirm);confirm.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Name.name = input_name.getText().toString();//得到输入的字符串Name.IP = getLocalIpAddress();Log.e("Register", Name.IP + Name.name);if (!Name.name.equals("")) {//输入内容不为空那么点击就跳转到chatromm界面Intent intent = new Intent(MainActivity.this, Chatroom.class);startActivity(intent);}}});}public static String getLocalIpAddress() {try {for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {NetworkInterface intf = en.nextElement();for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {InetAddress inetAddress = enumIpAddr.nextElement();if (!inetAddress.isLoopbackAddress() && !inetAddress.isLinkLocalAddress()) {return inetAddress.getHostAddress().toString();}}}} catch (SocketException ex) {Log.e("WifiPreference IpAddre", ex.toString());}return null;}

首先是输入用户名存到Name结构体中

public class Name {public static String name ;public static  String IP;
}

这段Java代码遍历了本机的所有网络接口(NetworkInterface),然后对于每个网络接口,进一步遍历其绑定的所有IP地址(InetAddress)。其核心目的是找到并返回一个符合条件的IPv4或IPv6地址,该地址既不是回环地址(loopback address,如127.0.0.1),也不是链路本地地址(link-local address,这类地址仅用于同一链路上的通信,如IPv6的fe80::/10范围内的地址)。具体步骤如下:

  1. 获取网络接口枚举:首先通过NetworkInterface.getNetworkInterfaces()方法获取到本机所有网络接口的枚举(Enumeration)对象。网络接口可以理解为计算机上的物理或虚拟网卡。

  2. 遍历网络接口:使用hasMoreElements()nextElement()方法遍历所有的网络接口。对于每个网络接口intf

  3. 获取IP地址枚举:通过intf.getInetAddresses()方法获取该网络接口上绑定的所有IP地址的枚举。

  4. 遍历IP地址:再次使用hasMoreElements()nextElement()遍历这些IP地址。对于每个IP地址inetAddress

  5. 检查地址类型:使用isLoopbackAddress()方法检查这个IP地址是否是回环地址,使用isLinkLocalAddress()方法检查是否是链路本地地址。这两个条件都不满足,意味着这个IP地址是可外部访问的地址。

  6. 返回符合条件的IP地址:一旦找到一个既不是回环地址也不是链路本地地址的IP地址,就立即通过getHostAddress().toString()获取其字符串表示形式并返回。这意味着该方法最终返回的是本机的第一个非回环、非链路本地的IP地址。

发送消息

定义消息类

public class Msg {public static final int TYPE_RECEIVED = 0;//收到的消息public static final int TYPE_SENT = 1;//发出去的消息private String name;private String content;private int type;//content表示消息内容,type表示类型public Msg(String name,String content ,int type){this.name = name;this.content = content;this.type = type;}public String getContent(){return content;}public int getType(){return type;}public String getName() {return name;}
}

点击按钮后发送消息

send.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {String content = inputTest.getText().toString();Log.e("get from input", content);if (!"".equals(content)) {try {//将输入框的信息传递给msg,并标记Message handleMsg = new Message();handleMsg.what = 1;handleMsg.obj = inputTest.getText().toString();//将msg传递给发送子线程mClientThread.revHandler.sendMessage(handleMsg);//输入框变空inputTest.setText("");} catch (Exception e) {e.printStackTrace();}}}});

子线程不断循环运行实现发送消息

  Looper.prepare();//绑定发送线程的Handler//由chatroom点击事件跳转到这里发送消息revHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 1) {try {//发送消息String content;content =Name.IP+"#$#" + Name.name+"#$#" + msg.obj.toString() + "\r\n";mOutputStream.write(content.getBytes("utf-8"));} catch (IOException e) {e.printStackTrace();}}}};//Looper.loop(); 让Looper开始工作,从消息队列里取消息,处理消息。Looper.loop();} catch (IOException e) {e.printStackTrace();Log.d("error","");}

接受消息

子线程循环接受服务端的消息

      new Thread(){@Overridepublic void run() {super.run();try {String content = null;//一个新线程持续循环的接受从服务器的消息,再发送给chatroomwhile ((content = mBufferedReader.readLine()) != null) {Log.d("get from server",content);//将接受到的数据传递给msg对象,并标记Message handleMsg = new Message();handleMsg.what = 0;handleMsg.obj = content;mHandler.sendMessage(handleMsg);}}catch (IOException e){e.printStackTrace();}}}.start();//启动

然后发送给主线程,主线程根据接受到的消息来更新聊天界面

  mHandler = new Handler() {@Overridepublic void handleMessage(Message handleMsg) {if (handleMsg.what == 0) {//接受到消息后的操作String content = handleMsg.obj.toString();Log.d("recive", content);String[] arr = content.split("#\\$#");String ip = arr[0];String name = arr[1];String str = arr[2];Log.d("get ", ip + name + str);Msg msg;if (ip.equals(Name.IP)) {Log.e("recive from server", "it is me ");msg = new Msg(name, str, Msg.TYPE_SENT);} else {msg = new Msg(name, str, Msg.TYPE_RECEIVED);}msgList.add(msg);Log.e("TAG", "msg " + msgList.size());adapter.notifyItemInserted(msgList.size() - 1);//当有新消息时,刷新RecyclView中的显示msgRecyclerView.scrollToPosition(msgList.size() - 1);//将RecyclerView定位到最后一行inputTest.setText("");//清空输入框*/}}};

发送文件

首先选择文件

file.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 创建意图用于选择文件//Intent intent = new Intent(Intent.ACTION_GET_CONTENT);//intent.setType("*/*");/*if (intent.resolveActivity(getPackageManager()) != null) {startActivityForResult(Intent.createChooser(intent, "选择文件"), PICK_FILE_REQUEST_CODE);} else {Toast.makeText(context, "无法找到文件选择器", Toast.LENGTH_SHORT).show();}*/Intent intent = new Intent(Intent.ACTION_GET_CONTENT);intent.addCategory(Intent.CATEGORY_OPENABLE);intent.setType("*/*");startActivityForResult(intent, PICK_FILE_REQUEST_CODE);}});

然后对选择到的文件的返回结果进行处理

protected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);if (requestCode == PICK_FILE_REQUEST_CODE && resultCode == RESULT_OK) {try {if (data != null) {Uri uri = data.getData();Log.e("uri", ":" + uri);String filePath = "";// 根据Android版本的不同,获取文件路径的方式也有所不同// 在API 19(KitKat)及以上版本,需要通过ContentResolver查询文件的真实路径if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(this, uri)) {// 处理DocumentsProvider的情况ContentResolver resolver = getContentResolver();InputStream inputStream = null;inputStream = resolver.openInputStream(uri);String filename = getFileNameFromUri(this, uri);Log.e("filename: ", ":" + filename);Log.e("inputstream: ", ":" + inputStream);Log.e("uri: ", ":" + uri);if (uri != null) {handleSelectedFilePath(filename,uri);}}}}catch (FileNotFoundException e) {e.printStackTrace();}}}

其中处理返回结果调用下列函数得到了文件名,并且将选择的文件写到了一个新建的可知道文件路径的文件(因为不能根据返回结果得到文件路径)

public String getFileNameFromUri (Context context, Uri uri){String fileName = null;Cursor cursor = null;try {cursor = context.getContentResolver().query(uri, null, null, null, null);if (cursor != null && cursor.moveToFirst()) {int columnIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME);fileName = cursor.getString(columnIndex);}} finally {if (cursor != null) {cursor.close();}}return fileName;}private void handleSelectedFilePath (String filename, Uri file){// 在这里处理获取到的文件路径Log.e("TAG", "Selected file name" + filename);// 可以进一步上传文件、读取文件内容等操作try {// 假设你已经有了一个Uri对象Uri sourceUri = file;// 获取源文件的输入流InputStream inputStream = getContentResolver().openInputStream(sourceUri);// 定义目标文件路径,这里以应用程序的cache目录为例String destFilePath = getCacheDir().getPath() + "/"+filename;File destFile = new File(destFilePath);// 创建并获取目标文件的输出流FileOutputStream outputStream = new FileOutputStream(destFile);// 将源文件内容复制到新文件byte[] buffer = new byte[1024];int read;while ((read = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, read);}// 关闭输入输出流outputStream.flush();outputStream.close();inputStream.close();// 现在你可以得到新建文件的文件地址String newFileAddress = destFile.getAbsolutePath();Log.e("getAbsolutePath", ": "+newFileAddress );//将输入框的信息传递给msg,并标记Message handleMsg = new Message();handleMsg.what = 1;handleMsg.obj =newFileAddress ;//"/data/data/llk/files/"//将msg传递给发送子线程fClientThread.revfHandler.sendMessage(handleMsg);//输入框变空inputTest.setText("");} catch (Exception e) {e.printStackTrace();}}}

最后handleSelectedFilePath函数将包含新建文件地址发送到子线程,子线程将文件名字和文件长度和文件字节发送到服务端

  Looper.prepare();//绑定发送线程的Handler//由chatroom点击事件跳转到这里发送消息revfHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 1) {try {mOutputStream = fSocket.getOutputStream();//输出流,客户端到管道//发送消息String content;File file = new File(msg.obj.toString());Log.e("msg.obj.toString()",": "+msg.obj.toString());content = Name.IP + "#$#" + Name.name + "#$#" + file.getName() + "\r\n";Log.e("content", ": "+content );mOutputStream.write(content.getBytes("utf-8"));//先发送了name ip 消息再发送文件内容DataOutputStream fout = new DataOutputStream(fSocket.getOutputStream());DataInputStream fin = new DataInputStream(new FileInputStream(file));//将文件发送出去// 传送文件名字fout.writeUTF(file.getName());Log.e("file.getname()", ": "+file.getName() );fout.flush();// 传送长度fout.writeLong(file.length());Log.e("file.length()", ": "+file.length() );fout.flush();System.out.println("开始传送文件...(大小:" + file.getTotalSpace() + ")");// 传送文件int lengthout = -1;// 读取到的文件长度byte[] buffout = new byte[1024];double curLength = 0;// 循环读取文件,直到结束while ((lengthout = fin.read(buffout)) > 0) {Thread.sleep(10);Log.e(" ", "lengthout: "+lengthout );curLength+=lengthout;Log.e("curlength / length", ": "+curLength+"/"+file.length());fout.write(buffout, 0, lengthout);fout.flush();}System.out.println("传送文件完成");Thread.sleep(5000);byte[] bytes = "EOF".getBytes(Charset.forName("UTF-8"));fout.write(bytes);} catch (IOException | InterruptedException e) {e.printStackTrace();}}}};//Looper.loop(); 让Looper开始工作,从消息队列里取消息,处理消息。Looper.loop();}

接受文件

子线程不断接受从服务端发送过来的文件,也是接受文件名和文件长度和文件字节内容,但会在指定路径新建一个文件来接受传输过来的内容。中途会更新消息列表再去接受

 new Thread() {@Overridepublic void run() {super.run();try {while (true){String filename = null;String content = null;DataInputStream dis = new DataInputStream(fSocket.getInputStream());// 从服务器传过来的东西System.out.println("客户端已经链接文件服务");//先传输过来名字和ip和提示文件到达消息mBufferedReader = new BufferedReader(new InputStreamReader(fSocket.getInputStream()));content = mBufferedReader.readLine();Log.e("content", ": "+content );Message handleMsg = new Message();handleMsg.what = 0;handleMsg.obj = content+" position at "+" /storage/emulated/0/Download/";fHandler.sendMessage(handleMsg);filename = dis.readUTF();Log.e("file name", "/storage/emulated/0/Download/"+filename );//根据服务器发送过来的UTF格式的文件名字String destFilePath ="/storage/emulated/0/Download/"+filename;File file = new File(destFilePath);file.createNewFile();// 保存到本地的文件//获取服务器传过来的文件大小Log.e("new position", " "+file.getAbsolutePath() );//显示完整路径double totleLength = dis.readLong();Log.e("length", " "+totleLength );DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));//通过dos往文件里写入内容System.out.println("开始接收:" + totleLength);int length = -1;long recvlength = -1;byte[] buff = new byte[1024];double curLength = 0;try {while((length=dis.read(buff))>0){String str = new String(buff, StandardCharsets.UTF_8);if (str.charAt(0)=='E'&&str.charAt(1)=='O'&&str.charAt(2)=='F'){break;}dos.write(buff, 0, length);Arrays.fill(buff, (byte) 0);//往文件里写入buffLog.e("写入文件的长度: ", " "+length );curLength+=length;//System.out.println("传输进度:"+(curLength/totleLength*100)+"%");System.out.println("传输进度:"+(curLength/totleLength*100)+"%");}System.out.println("传输完成");} catch (Exception ste) {System.out.println("接收文件出错");}}}catch (IOException e) {e.printStackTrace();}}}.start();

中途发送到主线程根据文件消息更新消息列表

 fHandler = new Handler() {@Overridepublic void handleMessage(Message handleMsg) {if (handleMsg.what == 0) {//接受到消息后的操作String content = handleMsg.obj.toString();Log.e("recive content", content);String[] arr = content.split("#\\$#");String ip = arr[0];String name = arr[1];String file = arr[2];Log.e("get ", ip+file + name);Msg msg;if (ip.equals(Name.IP)) {Log.e("recive from server", "it is me ");msg = new Msg(name, file, Msg.TYPE_SENT);} else {msg = new Msg(name, file, Msg.TYPE_RECEIVED);}msgList.add(msg);Log.e("TAG", "msg " + msgList.size());adapter.notifyItemInserted(msgList.size() - 1);//当有新消息时,刷新RecyclView中的显示msgRecyclerView.scrollToPosition(msgList.size() - 1);//将RecyclerView定位到最后一行inputTest.setText("");//清空输入框*/}}};

服务端

先列出各个可以监听的ip地址,然后得到运行两个子线程,分别用处理接受消息和文件并再发送给各个客户端


public class MyServer {public static ArrayList<Socket> mSocketList = new ArrayList<>() ;public static ArrayList<Socket> fSocketList = new ArrayList<>() ;public static void main(String[] args) throws SocketException{Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();while (networkInterfaces.hasMoreElements()) {NetworkInterface ni = networkInterfaces.nextElement();for (InterfaceAddress ia : ni.getInterfaceAddresses()) {InetAddress inetAddress = ia.getAddress();if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {System.out.println("ServerSocket可能监听的IP地址: " + inetAddress.getHostAddress());}}}try {//创建服务器SocketServerSocket ss = new ServerSocket(8848);ServerSocket fs = new ServerSocket(18848);while (true){//监听链接Socket s = ss.accept();Socket f = fs.accept();//打印信息System.out.println("ip:"+ s.getInetAddress().getHostAddress() +"加入聊天室");System.out.println("ip:"+ f.getInetAddress().getHostAddress()+" 客户端已经链接文件服务");//将s加入到线程池中mSocketList.add(s);fSocketList.add(f);//启动子线程new Thread(new ServerThread(s)).start();new Thread(new FileThread(f) ).start();}}catch (IOException e){e.printStackTrace();System.out.println("服务器已崩溃");e.printStackTrace();}}
}

接受消息并发送给各个客户端

接受消息,然后发给各个客户端


public class ServerThread implements Runnable {private Socket mSocket = null;private BufferedReader mBufferedReader = null;//构造方法public ServerThread(Socket s)throws IOException{mSocket = s;//输入管道到服务器mBufferedReader = new BufferedReader(new InputStreamReader(s.getInputStream(), "utf-8"));}public void run(){try {String content = null;//循环接受服务器消息,如果没有接收到,说明该客户端下线,将其从线程池中删除while ((content = mBufferedReader.readLine())!=null){System.out.println("ip:"+ mSocket.getInetAddress().getHostAddress()+":"+content);//循环向其他线程发送消息for (Iterator<Socket> it = MyServer.mSocketList.iterator();it.hasNext();) {Socket s = it.next();try {OutputStream os = s.getOutputStream();os.write((content + "\n").getBytes("utf-8"));} catch (SocketException e) {e.printStackTrace();it.remove();}}}}catch (IOException e){System.out.println("接收出错");try {mSocket.close();} catch (IOException e1) {e1.printStackTrace();}MyServer.mSocketList.remove(mSocket);System.out.println("ip:"+ mSocket.getInetAddress().getHostAddress() +"退出聊天室");}}
}

接受文件并发送给各个客户端

接受文件相关信息,在本地新建一个文件,并将接受到的字节流写入文件,然后再将文件相关信息和字节内容发送给各个客户端

public class FileThread implements Runnable{private Socket fSocket = null;private BufferedReader fBufferedReader = null;//构造方法public FileThread(Socket f)throws IOException{fSocket = f;//输入管道到服务器}@Overridepublic void run() {try {while (true) {System.out.println("new");   String filename = null;String content = null;DataInputStream dis = new DataInputStream(fSocket.getInputStream());// 从服务器传过来的东西//先传输过来名字和ip和提示文件到达消息BufferedReader mBufferedReader = new BufferedReader(new InputStreamReader(fSocket.getInputStream()));content = mBufferedReader.readLine()+"\r\n";System.out.println("content"+content);filename=dis.readUTF();//根据客户端发送过来的UTF格式的文件名字File file = new File("D:\\androidstudio\\chatroom\\server\\savefile\\"+filename);System.out.println("filename"+filename);if (!file.exists()) {try {// 新建文件boolean created = file.createNewFile();if (created) {System.out.println("成功创建文件");// 文件成功创建} else {// 文件创建失败,可能是因为权限问题或其他原因}} catch (IOException e) {e.printStackTrace();}}// 保存到本地的文件//获取服务器传过来的文件大小double totleLength = dis.readLong();System.out.println("file length "+totleLength);DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));//通过dos往文件里写入内容System.out.println("开始接收:"+totleLength);int length=-1;byte[] buff= new byte[1024];double curLength = 0;try {while((length=dis.read(buff))>0){String str = new String(buff, StandardCharsets.UTF_8);System.err.println(str);if (str.charAt(0)=='E'&&str.charAt(1)=='O'&&str.charAt(2)=='F'){break;}dos.write(buff, 0, length);Arrays.fill(buff, (byte) 0); //往文件里写入buffcurLength+=length;//System.out.println("传输进度:"+(curLength/totleLength*100)+"%");System.out.println("传输进度:"+(curLength/totleLength*100)+"%");}System.out.println("传输完成");} catch (Exception ste) {System.out.println("接收文件出错"); }for (Iterator<Socket> it = MyServer.fSocketList.iterator();it.hasNext();){  Socket f= it.next();try {DataOutputStream fout = new DataOutputStream(f.getOutputStream());DataInputStream fin = new DataInputStream(new FileInputStream(file));fout.write(content.getBytes("utf-8"));System.out.println("content: "+content);//将文件发送出去// 传送文件名字fout.writeUTF(file.getName());System.out.println("file.getName() "+file.getName());fout.flush();// 传送长度fout.writeLong(file.length());System.out.println("file.length() "+file.length());fout.flush();System.out.println("开始传送文件...(大小:" + file.getTotalSpace() + ")");// 传送文件int lengthout = -1;// 读取到的文件长度byte[] buffout = new byte[1024];curLength = 0;// 循环读取文件,直到结束while ((lengthout = fin.read(buffout)) > 0) {Thread.sleep(4);//System.out.println(" lengthout: "+lengthout );curLength+=lengthout;System.out.println("curlength / length: "+curLength+"/"+file.length());fout.write(buffout, 0, lengthout);fout.flush();}System.out.println("传送文件完成");Thread.sleep(1000);byte[] bytes = "EOF".getBytes(Charset.forName("UTF-8"));fout.write(bytes);}catch (Exception e) {System.out.println("传输意外");}}  }}catch (IOException e) {System.out.println("接收出错");try {fSocket.close();} catch (IOException e1) {e1.printStackTrace();}MyServer.fSocketList.remove(fSocket);System.out.println("ip:"+ fSocket.getInetAddress().getHostAddress() +"文件传输结束");}} 
}

http://www.ppmy.cn/news/1457794.html

相关文章

SQL语句的执行处理

一、SQL语句的执行处理模式 SQL语句的执行处理模式分为两种&#xff1a;即时SQL&#xff08;Immediate Statements&#xff09;和预处理SQL&#xff08;Prepared Statements&#xff09; 二、即时SQL 1、定义 动态的根据传入的参数拼接SQL语句并执行&#xff0c;一条语句经过M…

链舞算法谱---链表经典题剖析

前言&#xff1a;探究链表算法的奥秘&#xff0c;解锁编程新世界&#xff01; 欢迎来到我的链表算法博客&#xff0c;这将是您深入了解链表算法&#xff0c;提升编程技能的绝佳机会。链表作为数据结构的重要成员之一&#xff0c;其动态性和灵活性在实现某些功能上发挥不可替代的…

Star15.3k,开源数据可视化分析工具项目

好东西来了&#xff0c;这是一个人人可用的开源数据可视化分析工具项目&#xff0c;V 哥迫不及待的要给大家推荐这个项目&#xff0c;帆软、Tableau 等商业 BI 工具的开源替代&#xff0c;已在 Github 上被 Star了15.3k了&#xff0c;大家一起来了解一下。自己搭建起来可用&…

页面嵌套,界面套娃,除了用iframe,还有其他方式吗?

UIOTOS可以了解下&#xff0c;uiotos.net&#xff0c;通过连线来代替脚本逻辑开发&#xff0c;复杂的交互界面&#xff0c;通过页面嵌套轻松解决&#xff0c;是个很新颖的思路&#xff0c;前端零代码&#xff01; 蓝图连线尤其是独创的页面嵌套和属性继承技术&#xff0c;好家…

【k8s多集群管理平台开发实践】六、client-go实现k8s的cronjob的列表、创建cronjob、yaml配置更新

文章目录 简介 一.cronjob的列表实现1.1.controllers控制器代码1.2.models模型代码 二.界面创建cronjob2.1.controllers控制器代码2.2.models模分代码 三.读取cronjob的yaml配置并更新3.1.controllers控制器代码3.2.models模型代码 四.路由设置4.1.路由设置 五.前端代码5.1.列表…

【强训笔记】day13

NO.1 代码实现&#xff1a; #include <iostream>#include<string>using namespace std;int n,k,t; string s;int func() {int ret0;for(int i0;i<n;i){char chs[i];if(chL) ret-1;else{if(i-1>0&&i-2>0&&s[i-1]W&&s[i-2]W) retk…

武汉星起航:亚马逊:跨境电商领军平台,中国卖家全球拓展的首选

2015年&#xff0c;亚马逊全球开店业务正式进入中国&#xff0c;为中国卖家带来了全新的跨境电商机遇。如今&#xff0c;亚马逊已在全球拥有包括美国、加拿大、墨西哥、英国、法国、德国等在内的17大海外站点&#xff0c;为中国卖家提供了广阔的销售市场。武汉星起航将详细探讨…

Web Component fancy-components

css-doodle 组件库 fancy-components 组件库使用 yarn add fancy-components使用&#xff1a; import { FcBubbles } from fancy-components new FcBubbles() //要用哪个就new哪个 new 这里可能会报错eslink,eslintrc.js中处理报错 module.exports {rules: {no-new: off} …