node.js koa框架实战—商城案例

519 阅读5分钟

不使用数据库

传统方式动态换数据

const Koa = require('koa');
const staticCache = require('koa-static-cache');
const Router = require('@koa/router');
const fs = require('fs');

const app = new Koa();

app.use(staticCache('./public', {
    prefix: '/public',
    dynamic: true,
    gzip: true
}))

const router = new Router();

app.use(router.routes());

// 商品首页
router.get('/', async ctx => {
    let data = {
        title: '欢迎来到开课吧'   
    };
    let templateContent = fs.readFileSync('./template/index.html').toString();

    // 对模板一些可变的地方进行替换,生成一个最终的页面,返还给客户端
    templateContent = templateContent.replace(/\{\{(\w+)\}\}/g, ($0, $1) => {
        // console.log('$1', $1);
        return data[$1];
    })

    ctx.body = templateContent;
})

app.listen(8888, ctx => {
    console.log('服务器启动成功:http://localhost:8888');
})

使用nunjucks模板引擎换数据

const Koa = require('koa');
const staticCache = require('koa-static-cache');
const Router = require('@koa/router');
const nunjucks = require('nunjucks')

nunjucks.configure('template', {
    noCache: true// 开发环境下,设置noCache为true,有利于测试效果,开发完毕后关闭节约性能
    watch: true // 当模板变化时重新加载 
});

const app = new Koa();

app.use(staticCache('./public', {
    prefix: '/public',
    dynamic: true,
    gzip: true
}))

const router = new Router();

app.use(router.routes());

// 商品首页
router.get('/', async ctx => {

    let data = {
        title: '欢迎来到开课吧',
    }

    // let templateContent = fs.readFileSync('./template/index.html').toString();

    // 对模板一些可变的地方进行替换,生成一个最终的页面,返还给客户端
    // templateContent = templateContent.replace(/\{\{(\w+)\}\}/g, ($0, $1) => {
    //     // console.log('$1', $1);
    //     return data[$1];
    // })

    let templateContent = nunjucks.render('index.html', data)

    ctx.body = templateContent;
})

app.listen(8888, ctx => {
    console.log('服务器启动成功:http://localhost:8888');
})

动态路由

const Koa = require('koa');
const KoaStaticCache = require('koa-static-cache');
const KoaRouter = require('@koa/router');
const Nunjucks = require('nunjucks');

const categories = require('./data/categories.json');
const items = require('./data/items.json');

const app = new Koa();

Nunjucks.configure('./template', {
    // 开发环境下,设置 noCache 为 true,有利于测试看效果
    noCache: true,
    watch: true
});

// http://localhost:8888/public/css/css.css
app.use(KoaStaticCache('./public', {
    prefix: '/public',
    dynamic: true,
    gzip: true
}));

const router = new KoaRouter();


// 大部分的业务都放在下面处理了

// 商品首页
router.get('/', async ctx => {
    // console.log('categories', categories);
    ctx.body = Nunjucks.render('index.html', {
        categories,
        items
    });
});

// 商品详情
// :id 表示是一个动态路由,:id 是可变的,具体根据请求来决定
// 如果一个请求是以 /item 开始 然后,后面跟着 / 任意内容,就能满足该路由的规则
// :id 是一个占位符 变量,名称可以自己定,但是在中间件里面用的话需要使用这个名称
// (\\d+) 是对可变数据的 约束
router.get('/item/:id(\\d+)', async ctx => {
    // ctx.params => 对象,是 router 中间件根据当前url解析后的数据,里面存储了路由中动态部分当前实际内容
    // console.log('ctx', ctx.params);
    let id = Number(ctx.params.id);

    let item = items.find( d => d.id === id );
    ctx.body = Nunjucks.render('item.html', {
        categories,
        item
    });
});

app.use(router.routes());

app.listen(8888, () => {
    console.log(`服务启动成功:http://localhost:8888`);
});
// item.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>
    <link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>
    
    <div id="app">
        <header id="header">
            <a href="/" id="logo"></a>
    
            <nav id="nav">
                <!-- <a href="">- {{title}} -</a> -->

                {% for category in categories %}
                <a href="">{{category.name}}</a>
                {% endfor %}
            </nav>
    
            <div id="user">
                <a href="">登录</a>
                <a href="">注册</a>
            </div>
        </header>
    
        <div id="main">   
            <h1>{{item.name}}</h1>
            <hr>
            <img src="/public/items/images/{{item.cover}}" alt="{{item.name}}">
        </div>
    </div>

</body>
</html>

分页功能完整代码

const Koa = require('koa');
const staticCache = require('koa-static-cache');
const Router = require('@koa/router');
const nunjucks = require('nunjucks')

const categories = require('./data/categories.json');
const items = require('./data/items.json');

nunjucks.configure('template', {
    noCache: true,
    watch: true
});

const app = new Koa();

app.use(staticCache('./public', {
    prefix: '/public',
    dynamic: true,
    gzip: true
}))

const router = new Router();

app.use(router.routes());

// 商品首页
router.get('/', async ctx => {

    // 对数据进行分页展示
    // ctx.query 自动解析 当前 url 中 queryString 的部分,也就是url中 ? 后面的内容
    let page = Number(ctx.query.page) || 1;
    
    let prepage = 8;
    let start = (page - 1) * prepage;
    let end = start + prepage;
    let pages = Math.ceil(items.length / 8);

    let data = items.filter((item, index) => index >= start && index < end);

    ctx.body = nunjucks.render('index.html', {
        categories,
        data,
        page,
        pages
    });
})

// 商品详情
router.get('/item/:id(\\d+)', async ctx => {

    let id = Number(ctx.params.id);

    let item = items.find(d => d.id === id);

    console.log(item);

    ctx.body = nunjucks.render('item.html', {
        categories,
        item
    });

})

app.listen(8888, ctx => {
    console.log('服务器启动成功:http://localhost:8888');
})
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="./public/css/css.css" />

</head>

<body>

    <div id="app">
        <header id="header">
            <a href="/" id="logo"></a>

            <nav id="nav">
                {% for category in categories %}
                <a href="">{{category.name}}</a>
                {% endfor %}
            </nav>

            <div id="user">
                <a href="">登录</a>
                <a href="">注册</a>
            </div>
        </header>

        <div id="main">

            <ul class="items-list">


                {% for item in data %}
                <li class="panel">
                    <a href="/item/{{item.id}}">
                        <img src="/public/items/images/{{item.cover}}" alt="{{item.name}}" class="cover">
                    </a>
                    <div class="name">{{item.name}}</div>
                    <div class="price">¥{{(item.price/100).toFixed(2)}}</div>
                </li>
                {% endfor %}


            </ul>

            <div class="pagination-container">

                <div class="pagination">
                    <a href="/?page={{page-1}}" class="prev">上一页</a>

                    {% for i in range(1, pages+1) -%}
                    {% if i == page %}
                    <a href="/?page={{i}}" class="current">{{i}}</a>
                    {% else %}
                    <a href="/?page={{i}}">{{i}}</a>
                    {% endif %}
                    {%- endfor %}

                    <a href="/?page={{page+1}}" class="next">下一页</a>
                </div>

            </div>

        </div>
    </div>

</body>

</html>

使用数据库

新增注册、登录、添加功能

js完整代码

const Koa = require('koa');
const KoaStaticCache = require('koa-static-cache');
const KoaRouter = require('@koa/router');
const Nunjucks = require('nunjucks');
const mysql = require('mysql2');
const KoaBody = require('koa-body');

const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: '12345678',
    database: 'kkb_12'
});

const app = new Koa();

Nunjucks.configure('./template', {
    noCache: true,
    watch: true
});

app.use(KoaStaticCache('./public', {
    prefix: '/public',
    dynamic: true,
    gzip: true
}));

const router = new KoaRouter();


// 商品首页
router.get('/', getCategories, async ctx => {

    let [{count}] = await query(
        'select count(*) as count from `items`',
    );
    // console.log('count', count);

    let page = Number(ctx.query.page) || 1;
    let prepage = 4;
    let start = (page - 1) * prepage;
    // let end = start + prepage;
    let pages = Math.ceil(count / prepage);

    let data = await query(
        'select * from `items` limit ? offset ?',
        [
            prepage,
            start
        ]
    );

    ctx.body = Nunjucks.render('index.html', {
        categories: ctx.state.categories,
        data,
        page,
        pages
    });
});


router.get('/item/:id(\\d+)', getCategories, async ctx => {
    // ctx.params => 对象,是 router 中间件根据当前url解析后的数据,里面存储了路由中动态部分当前实际内容
    // console.log('ctx', ctx.params);
    let id = Number(ctx.params.id);

    let [item] = await query(
        'select * from `items` where `id`=?',
        [id]
    );
    // console.log('item', item);

    ctx.body = Nunjucks.render('item.html', {
        categories: ctx.state.categories,
        item
    });
});

// 渲染表单页面
router.get('/additem', getCategories, async ctx => {
    // let categories = await query(
    //     'select * from `categories`'
    // );

    ctx.body = Nunjucks.render('additem.html', {
        categories: ctx.state.categories
    });
});

// 处理提交数据请求
let addItemKoaBodyOptions = {
    // 能解析multipart格式提交过来的普通文本型数据,二进制数据不在这里设置
    multipart: true,
    // 设置处理二进制流数据,formidable使用了第三方库:https://github.com/node-formidable/formidable,通过这个库可以实现对文件的解析和保存处理
    formidable: {
        // 解析后的文件数据存储的路径
        uploadDir: './public/items/images',
        // 是否保存上传后的文件扩展名
        keepExtensions: true
    }
};
router.post('/additem', KoaBody(addItemKoaBodyOptions), async ctx => {

    // console.log('headers', ctx.headers);
    
    // let {category_id: categoryId, name, price, cover} = ctx.request.body;

    let {category_id: categoryId, name, price} = ctx.request.body;

    // multipart 提交的数据,普通文本数据存储在 ctx.request.body 中
    // multipart 提交的二进制数据,是存储在 ctx.request.files 中
    let coverObject = ctx.request.files.cover;    // cover 里面存储的每一个文件的 File 对象

    // 我们数据库中存储的是上传后的文件名称,而不是原始的文件名
    let lastIndexPoint = coverObject.path.lastIndexOf('/');
    
    let cover = coverObject.path.substring(lastIndexPoint + 1);

    // console.log(categoryId, name, price, cover);
    
    let rs = await query(
        "insert into `items` (`category_id`, `name`, `price`, `cover`) values (?, ?, ?, ?)",
        [
            categoryId,
            name,
            price,
            cover
        ]
    );

    ctx.body = '添加成功';
});

// 注册页面
router.get('/register', getCategories, async ctx => {
    
    
    ctx.body = Nunjucks.render('register.html', {
        categories: ctx.state.categories
    });
});

router.post('/register', getCategories, KoaBody(), async ctx => {
    let {username, password, repassword}  = ctx.request.body;

    if (!username || !password) {
        return ctx.body = '用户名和密码必须填写';
    }

    if (password !== repassword) {
        return ctx.body = '两次输入密码不一致';
        // ctx.body = Nunjucks.render('message.html', {
        //     categories: ctx.state.categories,
        //     message: '两次输入密码不一致'
        // });
    }

    // 去数据库中查询当前注册用户名是否已经存在了
    let user = await query(
        "select * from `users` where `username`=?",
        [username]
    );

    if (user.length) {
        return ctx.body = '该用户名已经被注册了';
    }

    let rs = await query(
        "insert into `users` (`username`, `password`) values (?, ?)",
        [
            username,
            password
        ]
    )
    
    ctx.body = '注册成功';
});

// 登录页面
router.get('/login', getCategories, async ctx => {
    
    
    ctx.body = Nunjucks.render('login.html', {
        categories: ctx.state.categories
    });
});

router.post('/login', KoaBody(), async ctx => {
    let {username, password}  = ctx.request.body;

    if (!username || !password) {
        return ctx.body = '用户名和密码必须填写';
    }

    // 去数据库中查询当前注册用户名是否已经存在了
    let [user] = await query(
        "select * from `users` where `username`=?",
        [username]
    );

    if (!user) {
        return ctx.body = '该用户不存在';
    }

    if (user.password !== password) {
        return ctx.body = '密码错误';
    }

    
    ctx.body = '登录成功';
})


app.use(router.routes());

app.listen(8888, () => {
    console.log(`服务启动成功:http://localhost:8888`);
});


async function getCategories(ctx, next) {
    ctx.state.categories = await query(
        'select * from `categories`'
    );
    await next();
}


function query(sql, data) {
    return new Promise( (resolve, reject) => {
        connection.query(sql, data, function(err, ...data) {
            if (err) {
                reject(err);
            } else {
                resolve(...data);
            }
        })
    } );
}

添加功能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>
    <link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>

    <!-- 
        实际上表单的提交就是一个上传数据的过程(每一次请求就是数据的上传)
        服务器的每一次响应实际上就是一个下载过程

        但是根据上传的数据的类型不同,上传的时候需要进行一些特定的设置(规范),为了能够更好的识别当前上传数据,以方便后端进行不同的识别和处理,所以上传的时候除了可以携带上传的必要数据以外,还可以通过 请求头 content-type 类设置(告诉服务器)当前我携带过去的数据是什么类型的:mime

        在表单中有一个enctype属性,可以设置当前通过表单来发送请求的时候,提交过去的数据的content-type的类型

        application/x-www-form-urlencoded:默认,数据是一个 urlencoded
                    category_id=1&name=asdsa&price=123123&cover=1b0c619e070aed76.jpg
        text/plain:纯文本
        multipart/form-data:formData格式,二进制,如果提交的数据中包含了不能被application/x-www-form-urlencoded或者text/plain处理的特殊格式(二进制文件)
     -->
    
    {% include "nav.html" %}

    <div id="main">
        
       <div id="login" class="panel">
           <h2>添加新商品</h2>
           <form action="/additem" method="post" enctype="multipart/form-data">
                <div class="form-item">
                    <label>
                        <span class="txt">分类:</span>
                        <select name="category_id">
                            {% for category in categories %}
                                <option value="{{category.id}}">{{category.name}}</option>
                            {% endfor %}
                        </select>
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt">名称:</span>
                        <input type="text" name="name" class="form-input">
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt">价格:</span>
                        <input type="text" name="price" class="form-input">
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt">封面:</span>
                        <!-- <input type="text" name="cover" class="form-input"> -->
                        <div class="form-button primary" id="uploaderBtn">
                            请选择一张封面图片进行上传
                        </div>
                    </label>
                    <input multiple type="file" name="cover" id="cover" style="display: none;" />
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt"></span>
                        <button class="form-button success">添加</button>
                    </label>
                </div>
           </form>
       </div>    

    </div>

</body>
</html>

<script>
    let uploaderBtn = document.querySelector('#uploaderBtn');
    let coverElement = document.querySelector('#cover');

    uploaderBtn.onclick = function() {
        coverElement.click();
    }

    coverElement.onchange = function() {
        // value 存储的是字符串,是这个文件的文件名(包含路径的)
        // value 不是文件本身的二进制数据,而是文件名称
        // console.log(this.value);
        // uploaderBtn.innerHTML = this.value;

        // files 存储才是具体的文件二进制数据对象
        // console.log(this.files);
        uploaderBtn.innerHTML = this.files[0].name;
    }
</script>

注册功能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>
    <link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>
    
    {% include "nav.html" %}

    <div id="main">
        
       <div id="register" class="panel">
           <h2>注册</h2>
           <form action="" method="post">
                <div class="form-item">
                    <label>
                        <span class="txt">姓名:</span>
                        <input type="text" name="username" class="form-input">
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt">密码:</span>
                        <input type="password" name="password" class="form-input">
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt">重复密码:</span>
                        <input type="password" name="repassword" class="form-input">
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt"></span>
                        <button class="form-button primary">登录</button>
                        <button class="form-button">注册</button>
                    </label>
                </div>
           </form>
       </div>    

    </div>

</body>
</html>

登录功能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>
    <link rel="stylesheet" href="/public/css/css.css" />
</head>
<body>
    
    {% include "nav.html" %}

    <div id="main">
        
       <div id="login" class="panel">
           <h2>登录</h2>
           <form action="" method="post">
                <div class="form-item">
                    <label>
                        <span class="txt">姓名:</span>
                        <input type="text" name="username" class="form-input">
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt">密码:</span>
                        <input type="password" name="password" class="form-input">
                    </label>
                </div>
                <div class="form-item">
                    <label>
                        <span class="txt"></span>
                        <button class="form-button primary">登录</button>
                        <button class="form-button">注册</button>
                    </label>
                </div>
           </form>
       </div>    

    </div>

</body>
</html>