美文网首页基础前端
以 NodeJs 和 MongoDB 为基础结合 Vue 制作登

以 NodeJs 和 MongoDB 为基础结合 Vue 制作登

作者: CondorHero | 来源:发表于2019-09-25 18:54 被阅读0次
    一、制作后台登录接口
    接口和数据库

    后台接口功能展示:

    接口地址 请求方式 接口作用
    /login post 表单登录
    login?username=xiaoming&password=123123 get 表单get请求登录
    /list get 学生表单数据
    /logout get 退出登录

    后台连接的数据库里面有两个测试的用户名和密码:

    姓名 密码
    xiaoming 123123
    xiaohong 123456

    数据全部存放在 MongoDB 数据库里面,第一步数据库开机:

    D:\mongodb>mongod --dbpath D:/mongodb/database
    

    学生表格数据使用的是 mockjs 模拟出来的(包括图片),生成的 100 条,然后我们可以导入数据库:

    mongoimport -d erp -c students db.json
    

    导入成功,查看我们的数据库:

    > show dbs
    admin   0.000GB
    cache   0.000GB
    config  0.000GB
    erp     0.000GB
    local   0.000GB
    > use erp
    switched to db erp
    > show collections
    students
    users
    > db.users.find()
    { "_id" : ObjectId("5d22eb1b347914c5993f1d37"), "username" : "xiaoming", "passwo
    rd" : "123123", "nickname" : "小明", "avatar" : "", "mobile" : "", "role" : "man
    ager" }
    { "_id" : ObjectId("5d22eb1b347914c5993f1d36"), "username" : "xiaohong", "passwo
    rd" : "123456", "nickname" : "小红", "avatar" : "", "mobile" : "", "role" : "wor
    ker" }
    

    使用的数据库表格:

    用到的数据库名 数据表(文档)
    erp users和students
    NodeJs 后台演示代码
    • session 和 token 的制作
    // 应用中间件进行配置
    // 使用 session 中间件
    app.use(session({
        secret: 'keyboard cat', // 可以随便写,是字符串就行,作为服务端生成session的签名
        resave: false,           //强制保存session,即使session没有变化,默认为true(影响性能),建议设置false
        saveUninitialized: true, // 强制未初始化的session存储,默认建议true,主要针对req.session未设置初始值,其他地方的请求是否出现 session_id
        name:"session_id",        //保存在本地cookie的一个名字,默认connect.sid可以不设置
        cookie: {
            maxAge: 1000 * 10, // 设置 session 的有效时间,单位毫秒,第一次访问即开始计时
            secure: false,     // 网址只能由 https 发送
        },
        rolling:true,//每次请求强行设置cookie,并重置cookie的过期时间,也就是每次访问都会重新计时。
    }));
    

    token 使用 crypto 加密技术

    var token = crypto.createHash("sha256").update(fields.username + fields.username).digest('hex');
    

    post 登录

    // //登陆
    app.post("/login", (req, res) => {
        //处理post请求
        var form = new formidable.IncomingForm();
        form.parse(req, function(err, fields, files) {
            console.log(fields.username,fields.password)
            //判定是否正确登陆
            User.find({
                "username": fields.username,
                "password": fields.password
            }).exec((err, docs) => {
    
                if (docs.length) {
                    //登陆成功
                    req.session.login = true;
                    req.session.username = fields.username;
    
                    var token = crypto.createHash("sha256").update(fields.username + fields.username).digest('hex');
                    res.json({
                        "result": 1,
                        "token": token
                    });
                } else {
                    res.json({
                        "result": -1
                    });
                }
            });
        });
    });
    
    • 退出登录
    // 退出登录
    app.get('/logout', function(req, res) {
        // 第一种cookie过期时间为0
        req.session.cookie.maxAge = 0;
        res.json({
            "result": -1
        });
    });
    
    • 表格分页对应的核心后台代码
    // 登录成功返回的数据的数据
    app.get('/list', (req, res) => {
        if (!req.session.login) {
            res.status(401).send("请登录");
            return;
        }
        var {limit,skip} = url.parse(req.url,true).query;
        Student.find().skip(Number(skip)).limit(Number(limit)).exec((err,docs)=>{
             if (docs.length) {
                 res.json(docs);
             }
        });
    });
    

    app.js 后台对应的源码见最后:

    二、使用 iview 搭建一个基本的项目框架

    后台我没有配置跨域,所以我们自己使用 webpack.config.js 配置代理跨域。这样就能请求后天的数据了。webpack.config.js 配置代理跨域:

    // 代理跨域
    devServer: {
        // 此处可以自定义访问端口号
        port : 8080,
        proxy: {
            '/api': {
                target: 'http://192.168.2.167:250',
                pathRewrite: {'^/api' : ''}
            }
        }
    }
    

    初步 iview 搭配的代码


    初步 iview 搭配的代码

    项目目录结构:

    
    

    路由文件 router.js:

    // 引入组件
    import Index from "../components/Index.vue";
    import Login from "../components/Login.vue";
    import Students from "../components/Students.vue";
    import Teacher from "../components/Teacher.vue";
    // 
    export default [{
        // 首页重定向
        path: "/",
        redirect:"/index"
    },{
        // 首页
        path: "/index",
        component: Index,
        meta:{
            chinese:"Index"
        }
    }, {
        // 注册页
        path: "/Login",
        component: Login,
        meta:{
            chinese:"Login"
        }
    }, {
        // 学生页
        path: "/students",
        component: Students,
        meta:{
            chinese:"Students"
        }
    }, {
        // 老师页
        path: "/teacher",
        component: Teacher,
        meta:{
            chinese:"Teacher"
        }
    }, ];
    

    登录注册 login.vue

    <template>
        <div>
            <h1>我是登录页面</h1>
            <Form ref="formCustom" :model="formCustom" :rules="ruleCustom" :label-width="80">
                <Row>
                    <Col span="6">
                        <FormItem label="用户名" prop="username">
                            <Input type="text" v-model="formCustom.username"></Input>
                        </FormItem>
                    </Col>
                </Row>
                <Row>
                    <Col span="6">
                        <FormItem label="密码" prop="password">
                            <Input type="password" v-model="formCustom.password"></Input>
                        </FormItem>
                    </Col>
                </Row>
                <Row>
                    <Col span="6">
                        <FormItem label="确认密码" prop="passwordCheck">
                            <Input type="password" v-model="formCustom.passwordCheck"></Input>
                        </FormItem>
                    </Col>
                </Row>
                <FormItem>
                    <Button type="primary" @click="handleSubmit('formCustom')">Submit</Button>
                    <Button @click="handleReset('formCustom')" style="margin-left: 8px">Reset</Button>
                </FormItem>
            </Form>
        </div>
    </template>
    <script>
        import axios from "axios";
        export default {
            data () {
                const validatePass = (rule, value, callback) => {
                    if (value === '') {
                        callback(new Error('Please enter your password'));
                    } else {
                        callback();
                    }
                };
                 const username = (rule, value, callback) => {
                    if (value === '') {
                        callback(new Error('Please enter your username'));
                    } else {
                        callback();
                    }
                };
                const validatePassCheck = (rule, value, callback) => {
                    if (value === '') {
                        callback(new Error('Please enter your password again'));
                    } else if (value !== this.formCustom.password) {
                        callback(new Error('The two input passwords do not match!'));
                    } else {
                        callback();
                    }
                };
                
                return {
                    formCustom: {
                        password: '',
                        passwordCheck: '',
                        username: '',
                    },
                    ruleCustom: {
                        password: [
                            { validator: validatePass, trigger: 'blur' }
                        ],
                        passwordCheck: [
                            { validator: validatePassCheck, trigger: 'blur' }
                        ],
                        username: [
                            { validator: username, trigger: 'blur' }
                        ]
                    }
                }
            },
            methods: {
                handleSubmit (name) {
                    this.$refs[name].validate((valid) => {
                        console.log(this,this.password)
                        if (valid) {
                           // axios({
                           //      method: 'post',
                           //      url: '/api/login',
                           //      data: {"username":this.formCustom.username,"password":this.formCustom.password},
                           //  }).then(data=>{
                           //      if(data.data.result === 1){
                              //    this.$Message.success('Success!');
                              //   }
                           //  });
                           axios.post("/api/login",{"username":this.formCustom.username,"password":this.formCustom.password}).then(data=>{
                                if(data.data.result === 1){
                                    this.$Message.success('Success!');
                                }
                            });
    
    
    
                        } else {
                            this.$Message.error('Fail!');
                        }
                    })
                },
                handleReset (name) {
                    this.$refs[name].resetFields();
                }
            }
        }
    </script>
    

    App.vue 主文件:

    <template>
        <div class="layout">
            <Layout>
                <Header>
                    <Menu mode="horizontal" theme="dark" active-name="1">
                        <div class="layout-logo"></div>
                        <div class="layout-nav1">
                            <MenuItem name="1">
                                <Icon type="ios-analytics"></Icon>
                                <router-link to="/index" :class="{
                                    'cur':true,
                                    'default':$router.currentRoute.meta.chinese == 'Index'
                                }">首页</router-link>
                            </MenuItem>
                            <MenuItem name="2">
                                <Icon type="ios-navigate"></Icon>
                                <router-link to="/students" :class="{
                                    'cur':true,
                                    'default':$router.currentRoute.meta.chinese == 'Students'
                                }">学生信息</router-link>
                            </MenuItem>
                            <MenuItem name="3">
                                <Icon type="ios-keypad"></Icon>
                                <router-link to="/teacher" :class="{
                                    'cur':true,
                                    'default':$router.currentRoute.meta.chinese == 'Teacher'
                                }">教师信息</router-link>
                            </MenuItem>
                            <MenuItem name="4">
                                <Icon type="ios-analytics"></Icon>
                                <span @click="layout" :class="{
                                    'cur':true
                                }">退出登录</span>
                            </MenuItem>
                            <MenuItem name="5">
                                <Icon type="ios-analytics"></Icon>
                                <router-link to="/login" :class="{
                                    'cur':true,
                                    'default':$router.currentRoute.meta.chinese == 'Login'
                                }">登录</router-link>
                            </MenuItem>
                        </div>
                    </Menu>
                </Header>
                <Content :style="{padding: '0 50px'}">
                    <Breadcrumb :style="{margin: '20px 0'}">
                        <BreadcrumbItem>Home</BreadcrumbItem>
                        <BreadcrumbItem>Components</BreadcrumbItem>
                        <BreadcrumbItem>Layout</BreadcrumbItem>
                    </Breadcrumb>
                    <Card>
                        <div style="min-height: 200px;">
                            <router-view></router-view>
                        </div>
                    </Card>
                </Content>
                <Footer class="layout-footer-center">2011-2016 &copy; TalkingData</Footer>
            </Layout>
        </div>
    </template>
    
    <script>
        import axios from "axios";
        export default {
            methods:{
                async layout(){
                    // this.$router.push("index");
                    const {result} = await axios.get("api/logout").then(data=>data.data);
                    if(result===-1){
                        this.$Message.success({
                            content: "成功退出登录!",
                            duration: 10
                        });
                    }
                    this.checkGroup = false;
                }
            }
        }
    </script>
    
    <style scoped>
    .cur{
        text-decoration: none;
        color:rgba(255,255,255,.7);
    }
    .default{
        color:orange;
    }
    .layout-nav1{
        width: 600px;
        margin: 0 auto;
        margin-right: 20px;
    }
    </style>
    
    

    学生表格 student.vue

    <template>
        <div>
            <div class="box">
                <Table
                stripe
                border
                :columns="columns"
                :data="checkGroup"
                />
                <Page 
                :total="total" 
                :current="page" 
                @on-change="changePage"
                />
            </div>
        </div>
    </template>
    
    <script>
    import axios from "axios";
    export default {
            props:["cleardata"],
            data(){
                return {
                    total:100,
                    page:1,
                    skip:10,
                    checkGroup:[],
                    columns : [
                        {title:"序号",key:"id",render: (h, params) => {
                            // 附赠的render可以替换前面的内容
                            // 列内容全部在params的row里面
                            return h('div', [
                                h('i', params.row.id)
                            ]);
                        } },
                        {title:"头像",key:"avatar" ,render(h,params){
                            return h('div', {style:{width:'50px',height:'50px',backgroundSize:"contain",backgroundImage:`url(${params.row.avatar})`,margin:"5px auto"}},"")
                        }},
                        // 表格内容默认左对齐,align:"center"居中
                        {title:"姓名",key:"name" },
                        // sortable: true开启字段排序
                        {title:"年龄",key:"age",sortable: true },
                        { title:"共青团员",key:"member"},
                        {title:"省份",key:"province" },
                        {title:"性别",key:"sex" },
                        {title:"教育情况",key:"education" },
                        {title:"民族",key:"national"},
                    ],
                }
            },
            // 组件创建前进行数据初始化,发送Ajax
            async beforeCreate(){
                const students = await axios.get("api/list?limit=10&skip=10").then(data=>data.data);
                this.checkGroup = students;
            },
            methods:{
                async changePage(page){
                    this.skip = page;
                const students = await axios.get(`api/list?limit=10&skip=${this.skip * 10}`).then(data=>data.data);
                console.log(students);
                this.checkGroup = students;
                }
            }
        }
    </script>
    
    <style scoped>
    .box{
        width: 1000px;
        margin:50px auto;
        text-align:center;
    }
    .ivu-page{
        margin-top:20px;
    }
    </style>
    <style scoped>
    .layout{
        border: 1px solid #d7dde4;
        background: #f5f7f9;
        position: relative;
        border-radius: 4px;
        overflow: hidden;
    }
    .layout-logo{
        width: 100px;
        height: 30px;
        background: #5b6270;
        border-radius: 3px;
        float: left;
        position: relative;
        top: 15px;
        left: 20px;
    }
    .layout-nav{
        width: 420px;
        margin: 0 auto;
        margin-right: 20px;
    }
    .layout-footer-center{
        text-align: center;
    }
    </style>
    
    三、登录注册、权限功能(重要)

    接下来就是使用 Vue 进行登录注册,和权限验证。业务重要的部分。在上面我们已经配置了两个测试账号。输入测试账号会发生什么那?

    • 服务器下发 session
    • 同时请求接口会返回一个 token

    然后我们登录的逻辑一共要处理的四个事情:

    事情 作用
    实现登录页面 登录功能
    路由在每次跳转页面都要用/userinfo接口验证用户是否登录 强制跳转到登录页
    权限访问 如果用户没有权限访问它,级别不够,强制跳转到登录页面。如果他没有这个角色,根本不能让他看见效果
    如果Ajax发现了status 401Unauthorized 跳转登录
    • 实现登录页面:就是一个post请求完成。详见 Login 组件。


      登录
    • 路由跳转发送 Ajax 检查用户登录。
      更改 app.js 增加接口验证。
    // 切换路由的时候,发送Ajax来验证用户使用正确登录和发送token
    app.get("/userinfo" , (req,res)=>{
    
        if(!req.session.login){
            res.status(401);
            res.json({"result": -1,"info":"你没有登录"});
            return;
        }
    
    
        //算token
        var suantoken = crypto.createHash("sha256").update(req.session.username + req.session.username).digest('hex');
    
        if(suantoken != url.parse(req.url,true).query.token){
            res.json({"result": -1,"info":"token不正确"});
            return ;
        }
    
    
        User.find({
            "username" : req.session.username
        } , (err , docs)=>{
            res.json({
                username:docs[0].username,
                nickname :docs[0].nickname,
                avatar : docs[0].avatar,
                mobile : docs[0].mobile,
                sex : docs[0].sex,
                role : docs[0].role
            })
    
        });
    });
    

    我们输入

    http://127.0.0.1:8080/api/login?username=xiaoming&password=123123
    

    登录成功后复制返回的 token

    http://127.0.0.1:8080/api/userinfo?token=d855f63edcbc1db6499f76761889f8080333f89bb1d9fbfaccd859ac329497a4
    

    如果 token 查询正确返回这个用户的信息。

    {"username":"xiaoming","nickname":"小明","avatar":"","mobile":"","role":"manager"}
    

    路由守卫解决路由跳转发送 Ajax 请求的要求。不用想肯定是全局守卫中的beforeEach() 我的在 main.js文件里:

    // 定义路由守卫
    router.beforeEach((to, from, next) => {
        if (to.path === '/login') {
            next();
        } else {
            // 发出Ajax请求,验证用户是否登录
            axios.get("/api/userinfo?token=d855f63edcbc1db6499f76761889f8080333f89bb1d9fbfaccd859ac329497a4").then(data=>{
                if (!data.data.result) {
                    next();
                } else {
                    alert(3)
                    next("login");
                }
            }).catch(()=>{
                next("login")
            });
        }
    });
    

    token 此处可以从 localStorage 里面读取。不演示了。你在这定义一个全局变量绑定到 Vue 的原型上也是可以的。
    如果路由是这样设置的:

    {
        // 注册页
        name:"login",
        path: "/login",
        component: Login,
        meta:{
            chinese:"Login"
        }
    }
    

    路由守卫那里拿到路径就可以改为:

    // 定义路由守卫
    router.beforeEach((to, from, next) => {
        if (to.name === 'login') {
            next();
        } else {
            // 发出Ajax请求,验证用户是否登录
            axios.get("/api/userinfo?token=d855f63edcbc1db6499f76761889f8080333f89bb1d9fbfaccd859ac329497a4").then(data=>{
                if (!data.data.result) {
                    next();
                } else {
                    next({
                        'name': 'login'
                    });
                }
            }).catch(()=>{
                next({
                    'name': 'login'
                });
            });
        }
    });
    

    这就是命名路由的跳转方法,一般命名路由跳转和路径跳转差不多相同,甚至你可以使用 meta,只要能辨识路由就行。

    • 如果 Ajax 发现了status 401Unauthorized(未授权的情况)。
      很明显是我们的 Ajax 发送错误,只需在 请求错误的时候进行路由跳转:可直接使用
    window.location.hash = "#/login"
    
    // 组件创建前进行数据初始化,发送Ajax
    async beforeCreate(){
        const students = await axios.get("api/list?limit=10&skip=10").then(data=>data.data).catch(()=>{window.location.hash = "#/login"});
        this.checkGroup = students;
    }
    

    还可以使用返回的 401 状态码跳转:

    axios.get('/user/12345')
      .catch(function (error) {
        if (error.response) {
          // 请求已发出,但服务器响应的状态码不在 2xx 范围内
          console.log(error.response.data);
          console.log(error.response.status);
          console.log(error.response.headers);
        } else {
          // Something happened in setting up the request that triggered an Error
          console.log('Error', error.message);
        }
        console.log(error.config);
      });
    
    • 权限访问
    { "_id" : ObjectId("5d22eb1b347914c5993f1d37"), "username" : "xiaoming", "passwo
    rd" : "123123", "nickname" : "小明", "avatar" : "", "mobile" : "", "role" : "man
    ager" }
    { "_id" : ObjectId("5d22eb1b347914c5993f1d36"), "username" : "xiaohong", "passwo
    rd" : "123456", "nickname" : "小红", "avatar" : "", "mobile" : "", "role" : "wor
    ker" }
    

    权限访问:一个是经理"role" : "manager" 一个是工作者 "role" : "worker" 在路由守卫的地方就可以吧状态存储到 Vuex 的变量里面。后面就可以直接进行权限判断,甚至是组件判断。

    相关文章

      网友评论

        本文标题:以 NodeJs 和 MongoDB 为基础结合 Vue 制作登

        本文链接:https://www.haomeiwen.com/subject/phutuctx.html