利用vue.js2X写前端搜索页面,express写后端API接口展现搜索数据
这里用的数据库是mongodb
第1先做一个数据库类
db.js
const {MongoClient}=require('mongodb');class ClientDB{#urlPath='mongodb://ychj:123456@localhost:27017/?authSource=employees';constructor(dbName,collectionName){this.dbName=dbName;this.collName=collectionName;this.client=new MongoClient(this.#urlPath);this.connection=null;}async connect(){if(!this.connection){this.connection=await this.client.connect();console.log('Connected to MongoDB.');const db=this.client.db(this.dbName);this.collection=db.collection(this.collName);}}async disconnect(){if(this.connection){await this.client.close();this.connection=null;console.log('Connection closed');}}async insertOne(obj){await this.connect();try{let result=await this.collection.insertOne(obj);if(result.acknowledged){return result.insertedId;}}catch(err){console.log('Insert Error:',err);throw err;}}async deleteOne(obj){await this.connect();try{let result=await this.collection.deleteOne(obj);if(result.deletedCount===1){return true;}else{return false;}}catch(err){console.error('Delete Error:',err);throw err;}}/*** 统计查询对象有多少条结果* 这里只要大于0则返回true* @param {object} obj //查询对象 * @returns */async countDocuments(obj){await this.connect();try{let result=await this.collection.countDocuments(obj);console.log(result);if(result>0){return true;}else{return false;}}catch(err){console.error("Count documents Error:",err);throw err;}}/*** 查找多结果* @param {Object} obj //查询对象* @returns */async find(obj){await this.connect();try{let result=await this.collection.find(obj).toArray();if(result.length>=1){return result;}else{return false;}}catch(err){console.error("find documents Error:",err);throw new Error(err);}}/*** * @param {Object} upCondition 更新的条件对象* @param {Object} updateData 要更新的数据对象*/async updateOne(upCondition,updateData){await this.connect();try{let result=await this.collection.updateOne(upCondition,updateData);if(result.acknowledged && result.modifiedCount===1){return true;}else{return false;}}catch(err){console.error(err);throw new Error(err);}}
}
module.exports=ClientDB;
第2写一个根据搜索词,从数据库里面找出相关词,并生成一个临时的json文件,用数据流的方式将所有搜索到的json文件写入临时文件内
注意:数据库里面的文档都是进行了分词,用了jieba这个Nodejs库进行的分词,搜索出结果后要把文档内的空格全删除掉
searchJieba.js
const fs=require("fs");
const ClientDB=require('../db');
const db=new ClientDB("employees","employees");async function searchDocuments(words)
{console.log(words);try{const docs=await db.find({$text:{"$search":words}});if(docs===false){return;}//创建随机的临时文件名const outputFile=getRandomFileName();//对大文件的数据流,为什么要用数据流,因为搜索出来的结果如果非常大,如上千条,我们不能存储在内存中,而是存在一个临时的随机文件中//避免占用或撑爆我们的内存,所以直接写入临时文件当中,然后显示 的时候再读取const writeableStream=fs.createWriteStream(outputFile);//将读取到的搜索结果存储为json文件格式,用一个数组将其包含当中writeableStream.write("[");let isFile=true;docs.forEach((doc)=>{if(!isFile){writeableStream.write(",\n");}const formattitle=doc.title.replace(/\s+/g,'');const formatcontent=doc.content.split('\n').map(paragraph=>paragraph.replace(/\s+/g,'')).join("\n");const id=doc.id;//将各属性id,title,content组合成对象形式,然后再转化为json格式写入临时的json文件中const result={id:id,title:formattitle,content:formatcontent};writeableStream.write(JSON.stringify(result));isFile=false;});writeableStream.write("]");writeableStream.end();//返回临时文件名字return outputFile;}finally{await db.disconnect();}
}//生成临时的随机json文件
function getRandomFileName(){return `output_${Math.random().toString(36).substring(2,9)}.json`
}exports.searchDocuments=searchDocuments;
第3做接口文件,方便前端调用
const express=require("express");
const app=express();
const {searchDocuments}=require("./searchJieba");
const fs=require("fs");
const readline=require("readline");
const cors=require("cors");
const path=require("path");app.use(cors({origin:"http://localhost:5173"
}))
app.use(express.json());app.post("/api/search",async (req,res)=>{let {words}=req.body;console.log(words);try{const outputFile=await searchDocuments(words);console.log("文件名:",outputFile);if(outputFile===undefined){const msg={stat:404,message:"未查询到相关结果"};const msgJson=JSON.stringify(msg);res.send(msgJson);}else{const fileStream=fs.createReadStream(path.join(__dirname,outputFile));const rl=readline.createInterface({input:fileStream,crlfDelay:Infinity});//按行读取所有的json文件内容let jsonData='';rl.on('line',line=>{jsonData+=line;});rl.on('close',()=>{try{console.log(jsonData);res.send(jsonData);//延时删除临时文件,避免文件数据还未传输完,这边已经删除了临时文件if(fs.existsSync(outputFile)){new Promise((resolve,reject)=>{setTimeout(()=>{fs.unlink(outputFile,(err)=>{if(err)reject(err);else resolve(`文件${outputFile}已经删除`);});},5000);//延时5秒}).then(console.log).catch(console.error);}}catch(err){console.error('发送json数据给前端出错:',err);res.status(400).end('向前端发送json数据出错');}})}}catch(err){console.error('查询搜索词出错:',err);res.status(400).end('查询出错');}
});app.listen(1337,"localhost",()=>{console.log('服务端口已经开启监听');
});
第4步是用vue写前端文件
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>测试用vite显示搜索前端的VUE页面,然后调用后端API搜索出来的数据</title><script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.13/vue.js"></script></head><body><div id="app"><my-search></my-search></div><script>const showChild={props:["responseData"],template:`<div><ul><li v-for="item in responseData" :key="item.id"><div>ID: {{item.id}}</div> <div>标题:{{item.title}}</div><div>内容:{{item.content}}</div> </li> </ul></div>`,};const Search={template:`<div><form @submit.prevent="handleSubmit"><div><label for="searchPage">搜索</label><input type="text" id="searchPage" v-model="searchData"><button :disabled="isLoading">{{isLoading ? "搜索中..." : "搜索"}}</button> </div></form><div v-if="!isValied" style="color:red;font-size:14px;">{{searchWordsErr}}</div> <div v-if="!isSearchResult">{{searchWordsNull}} </div> <my-child :responseData="responseData" v-if="isSearchResult"></my-child></div>`,data(){return{searchData:'',isLoading:false,searchWordsErr:'',responseData:'',searchWordsNull:'',isSearchResult:true}},computed:{isValied(){return this.searchData.length >= 2;}},methods:{handleSubmit(){this.valiedSearchWords(this.searchData);if(!this.isValied)return;this.isLoading=true;let searchWords={words:this.searchData};console.log(searchWords);try{fetch("http://localhost:1337/api/search",{method:"post",body:JSON.stringify(searchWords),headers:{"Content-Type":"application/json"}}).then(response=>{if(response.ok){return response.json();}}).then(data=>{if(data.stat===404){console.log(data.message);this.isSearchResult=false;this.searchWordsNull=data.message;return;}this.isSearchResult=true;this.responseData=data;}).catch(error=>{console.log('搜索出错:',error);}).finally(()=>{this.isLoading=false;})}catch(err){console.error("获取后端数据出错:",err);}},valiedSearchWords(words){if(words.length<2){this.searchWordsErr="搜索必须大于2个字符";}else{this.searchWordsErr='';}}},watch:{searchData(newVal){this.valiedSearchWords(newVal);}},components:{'my-child':showChild} };new Vue({el:"#app",components:{"mySearch":Search}});</script></body>
</html>
这样就是一个用vue写的搜索页面,后端是一个express调用接口
顺便也把插入文档的代码也贴出来,数据库类和上面的db.js 是一样的
第1步是写插入数据库的文件代码
insertJieba.js
const {Jieba}=require("@node-rs/jieba")
const {dict}=require("@node-rs/jieba/dict");
const ClientDB=require("../db");
const db=new ClientDB("employees","employees");async function insertDocument(titledata,contentdata)
{ try{const jieba=Jieba.withDict(dict);const titleWords=jieba.cut(titledata).join(' ');const contentWords=contentdata.split("\n").map(paragraph=>jieba.cut(paragraph).join(" "));const randomId=RandomId(1,10000);const result=await db.insertOne({id:randomId,title:titleWords,content:contentWords.join("\n")});if(result){return true;}else{return false;}}catch(error){console.error(error);throw new Error(error);}
}/*** 用洗牌方法随机生成一个不重复的数字ID* @param {num} min * @param {num} max * @returns */
function RandomId(min, max) {// 创建一个从 min 到 max 的数组const numbers = Array.from({ length: max - min + 1 }, (_, i) => i + min);// 使用 Fisher-Yates 洗牌算法打乱数组for (let i = numbers.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));[numbers[i], numbers[j]] = [numbers[j], numbers[i]];}// 返回第一个元素作为IDreturn numbers[0];
}exports.insertDocument=insertDocument;
第2步是用express写接口文件,接口文件是接收前端数据,然后传给后端的insertJieba.js写入数据库的中间件
150.js
const express=require("express");
const cors=require("cors");
const app=express();
const {insertDocument}=require('./insertJieba');app.use(cors({origin:"http://localhost:5173"
}))
app.use(express.json());app.post("/api/participle",async (req,res)=>{let {title,content}=req.body;try{let result=await insertDocument(title,content);let resData={};if(result){resData={msg:'数据插入成功',status:true};}else{resData={msg:"数据插入失败",status:false};}res.send(JSON.stringify(resData));}catch(error){console.error('Insert Documents Error:',error);res.status(404).end('插入分词发生错误');}
});app.listen(1337,"localhost",()=>{console.log("服务端口已经开启监听");
});
第3步是前端文件,即vue写的插入title和content
150.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>测试用vite显示vue前端页面,然后调用后端API数据</title><script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.13/vue.js"></script></head><body><div id="app"><my-content></my-content></div><script>const Content={template:`<div><form @submit.prevent="handleSubmit"><div><label for="addTitle">标题</label><input type="text" name="title" id="addTitle" v-model="title" @input="valiedTitle" required><span v-show="!titleErr.stat" :style="titleStyle">{{titleErr.msg}}</span></div> <div><label for="addContent">内容</label><textarea id="addContent" rows="10" cols="45" v-model="content" @input="valiedContent" placeholder="请输入内容" ></textarea><span v-show="!contentErr.stat" :style="contentStyle">{{contentErr.msg}}</span></div><div><button id="btn" :disabled="isLoading">{{isLoading ? "提交中..." : "提交"}}</button></div> </form><div id="output">{{responseData}}</div> </div>`,data(){return{title:'',content:'',isLoading:false,responseData:'',titleErr:{stat:true,msg:''},contentErr:{stat:true,msg:''},titleStyle:{color:"red",fontSize:"14px",marignLeft:"10px"},contentStyle:{color:"red",fontSize:"14px",marignLeft:"10px"}}}, methods:{handleSubmit(){this.valiedTitle(this.title);this.valiedContent(this.content);if(!this.titleErr.stat || !this.contentErr.stat){return;}this.isLoading=true;let obj={title:this.title,content:this.content};fetch("http://localhost:1337/api/participle",{method:"post",body:JSON.stringify(obj),headers:{"Content-Type":"application/json"}}).then(response=>response.json()).then(data=>{const {status,msg}=data;if(status){this.responseData=msg;this.title='';this.content='';}else{this.responseData=msg;}}).catch(error=>{console.error(error);this.responseData="提交失败:"+error;}).finally(()=>{this.isLoading=false;})},valiedTitle(newVal){if(newVal.length<5){this.titleErr.msg="标题至少4个字符";this.titleErr.stat=false;}else{this.titleErr.msg='';this.titleErr.stat=true;}},valiedContent(newVal){if(newVal.length<5){this.contentErr.msg='内容至少4个字符';this.contentErr.stat=false;}else{this.contentErr.msg='';this.contentErr.stat=true;}}},watch:{title(newVal){this.valiedTitle(newVal);},content(newVal){this.valiedContent(newVal);}}};new Vue({el:"#app",components:{"myContent":Content}})</script></body>
</html>
运行方式:在vite中运行前端文件,html后缀的文件,在node中运行express写的文件