利用vue.js2X写前端搜索页面,express写后端API接口展现搜索数据

这里用的数据库是mongodb

第1先做一个数据库类

db.js

javascript 复制代码
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

javascript 复制代码
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做接口文件,方便前端调用

javascript 复制代码
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写前端文件

javascript 复制代码
<!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

javascript 复制代码
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]];
  }

  // 返回第一个元素作为ID
  return numbers[0];
}

exports.insertDocument=insertDocument;

第2步是用express写接口文件,接口文件是接收前端数据,然后传给后端的insertJieba.js写入数据库的中间件

150.js

javascript 复制代码
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

javascript 复制代码
<!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写的文件