• 从node+koa后端模板引擎渲染到vue+node+koa+ajax请求



    熟悉 VueReact 开发的同学对于 ajax 请求数据前端渲染应该是不陌生的,这里先从 node+koa 后端模板引擎渲染 .html 说起,可以更好的理解如何从后端渲染过渡到现在的主流 ajax 请求+前端框架的开发形式的转变,更能理解 ajax 请求的好处到底在哪,毕竟 ajax 是后期发展起来的,它并不是一开始就有的,既然发展起来并得到了如此广泛的应用,一定是因为它解决了重要的难题。

    node+koa+koa-swig

    初始化项目

    npm init -y
    

    服务器

    // app.js
    const Koa = require('koa') // 包装过的http
    const KoaStaticCache = require('koa-static-cache') // 静态资源中间件
    const Router = require('koa-router')
    const co = require('co')
    const Render = require('koa-swig') // 模板引擎
    const path = require('path')
    const bodyParser = require('koa-bodyparser') // 处理post请求
    
    const app = new Koa() // 创建服务器
    
    /* 处理静态资源:任何请求,首先通过 KoaStaticCache 中间件处理看是否是静态资源请求 */
    app.use( KoaStaticCache(__dirname + '/static', {
      prefix: '/public' // 如果当前请求 url 是以 /public 开始的,则作为静态资源请求,映射到我服务器上的 /static 路径中的文件
    }) )
    
    /* 处理请求正文中的数据(处理post请求参数)*/
    app.use(bodyParser())
    
    /* 服务器重启会重置数据,所以可以写成一个本地文件,对文件进行增删改查(类似数据库的功能了) */
    let datas = {
      maxId: 3,
      appName: 'TodoList',
      skin: 'index.css',
      tasks: [
        {id: 1, title: '测试任务1', done: true},
        {id: 2, title: '学习koa', done: false},
        {id: 3, title: '学习sql', done: false},
      ]
    }
    
    /* 设置模板引擎 */
    app.context.render = co.wrap(Render({
      root: path.join(__dirname, 'views'),
      autoescape: true, // 数据是否编码(html)
      cache: false,
      // cache: 'memory', // 内存中缓存模板,下次访问就直接从内存中读取模板
      ext: 'html'
    }))
    
    /* router.routes() 也相当于一个中间件,请求 url 会经过我们定义的路由进行过滤 */
    const router = new Router()
    
    /* 首页,模板引擎中设置了文件内容为views文件夹,所以 / 会去找 views/index.html文件 */
    router.get('/', async ctx => {
      ctx.body =  await ctx.render('index.html', {datas})
    })
    
    /* 添加 */
    router.get('/add', async ctx => {
      ctx.body =  await ctx.render('add.html', {datas})
    })
    router.post('/posttask', async ctx => {
      let cur_title = ctx.request.body.title || ''
      if(cur_title) {
        datas.tasks.unshift({
          id: ++datas.maxId,
          title: cur_title,
          done: false
        })
        ctx.body = await ctx.render('message', {
          msg: '添加成功',
          href: '/'
        })
      } else {
        ctx.body = await ctx.render('message', {
          msg: '请输入任务标题',
          href: 'javascript:history.back()'
        })
      }
    })
    
    /* 改变 */
    router.get('/change/:id', ctx => {
      let cur_id = ctx.params.id
      datas.tasks.forEach(task => {
        if(task.id * 1 === cur_id * 1) {
          task.done = !task.done
        }
      })
      ctx.response.redirect('/')
    })
    
    /* 删除 */
    router.get('/remove/:id', async ctx => {
      let cur_id = ctx.params.id
      datas.tasks = datas.tasks.filter(task => task.id * 1 !== cur_id * 1)
      ctx.body = await ctx.render('message', {
        msg: '删除成功',
        href: '/'
      })
    })
    
    app.use(router.routes())
    
    app.listen(80, () => {
      console.log('启动成功...运行在http://localhost:80')
    })
    

    后端模板内容

    
    DOCTYPE html>
    <html lang="en">
    <head>
      <link rel="stylesheet" href="/public/{{datas.skin}}">
    head>
    <body>
      <h1>{{datas.appName}}h1>
      <a href="/add">添加新任务a>
      <hr />
      <ul>
        {% for task in datas.tasks %}
          {% if task.done %}
          <li class="done">
            <input type="checkbox" checked onclick="change({{task.id}})" />
            [{{task.id}}] - {{task.title}}
            <a href="/remove/{{task.id}}">删除a>
          li>
          {% else %}
          <li>
            <input type="checkbox" onclick="change({{task.id}})" />
            [{{task.id}}] - {{task.title}}
            <a href="/remove/{{task.id}}">删除a>
          li>
          {% endif %}
        {% endfor %}
      ul>
    
      <script>
        function change(id) {
          window.location.href = '/change/' + id
        }
      script>
    body>
    html>
    

    添加任务的模板就是原生表单

    
    <form action="/posttask" method="POST">
      <input type="text" name="title" />
      <button>添加button>
    form>
    

    在这里插入图片描述
    模板引擎中引入了样式文件,通过服务端的配置,会去 static 路径下去查找

    // app.js
    app.use( KoaStaticCache(__dirname + '/static', {
      prefix: '/public' // 如果当前请求 url 是以 /public 开始的,则作为静态资源请求,映射到我服务器上的 /static 路径中的文件
    }) )
    

    启动项目

    开发 node 程序时,调试过程中每修改一次代码都需要重新启动服务才生效,这是因为 NodeJS 只有在第一次引用到某部分时才会去解析脚本文件,以后都会直接访问内存,避免重复载入和解析,这种设计有利于提高性能,但是却并不利于开发调试,为了每次修改后都能实时生效,安装一个辅助依赖 supervisor 会监视代码的改动重启NodeJS服务。

    npm i supervisor
    
    // package.json
    "scripts": {
      "supervisor": ".\\node_modules\\.bin\\supervisor app"
    },
    
    npm run supervisor
    

    启动成功后,访问 http://localhost/

    项目预览

    请添加图片描述

    总结

    以上就是通过后端模板引擎来渲染页面的方式,个人认为明显的缺陷有:

    • 繁琐的原生html和js语法,以及复杂的引擎模板语法
    • 改变任务状态这种操作,改变了数据以后需要重定向到首页,重新渲染整个页面(更希望只改变数据改变的部分内容即可)

    node+koa+vue+ajax

    下面尝试使用 Vue + ajax 的形式改写以上功能

    改写app.js

    // app.js
    const Koa = require('koa')
    const KoaStaticCache = require('koa-static-cache')
    const Router = require('koa-router')
    const bodyParser = require('koa-bodyparser')
    const fs = require('fs')
    
    const app = new Koa()
    
    // 这里的数据改用本地读取的方式来mock
    let datas = JSON.parse(fs.readFileSync('./data/data.json'))
    
    /* 静态资源托管 */
    app.use( KoaStaticCache(__dirname + '/static', {
      prefix: '/public',
      gzip: true
    }) )
    
    /* 处理body解析 */
    app.use(bodyParser())
    
    /* 路由 */
    const router = new Router()
    router.get('/', async ctx => {
      ctx.body = 'hello, this is a test!'
    })
    router.get('/todos', async ctx => {
      ctx.body = {
        code: 0,
        result: datas.todos
      }
    })
    router.post('/add', async ctx => {
      let title = ctx.request.body.title || ''
      if(!title) {
        ctx.body = {
          code: 1,
          result: '请传入任务标题'
        }
      } else {
        let newTask = {
          id: ++datas._id,
          title,
          done: false
        }
        datas.todos.unshift(newTask)
        ctx.body = {
          code: 0,
          result: newTask
        }
        fs.writeFileSync('./data/data.json', JSON.stringify(datas))
      }
    })
    router.post('/toggle', async ctx => {
      let id = ctx.request.body.id * 1 || 0
      if(!id) {
        ctx.body = {
          code: 1,
          result: '请传入id'
        }
      } else {
        let todo = datas.todos.find(todo => todo.id * 1 === id)
        todo.done = !todo.done
        ctx.body = {
          code: 0,
          result: '修改成功'
        }
        fs.writeFileSync('./data/data.json', JSON.stringify(datas))
      }
    })
    router.post('/remove', async ctx => {
      let id = ctx.request.body.id * 1 || 0
      if(!id) {
        ctx.body = {
          code: 1,
          result: '请传入id'
        }
      } else {
        datas.todos = datas.todos.filter(todo => todo.id * 1 !== id)
        ctx.body = {
          code: 0,
          result: '删除成功'
        }
        fs.writeFileSync('./data/data.json', JSON.stringify(datas))
      }
    })
    
    app.use(router.routes())
    
    app.listen(80, () => {
      console.log('启动成功...')
    })
    

    index.html

    
    DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Documenttitle>
      <link rel="stylesheet" href="/public/css/index.css">
      <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14">script>
    head>
    <body>
      <div id="app">
        <h1>TotoListh1>
        <div>
          <input type="text" v-model="newTask" />
          <button @click="add">提交button>
        div>
        <hr />
        <ul>
          <li v-for="todo in todos" :key="todo.id">
            <input type="checkbox" :checked="todo.done" @click.prevent="toggle(todo.id)" />
            <span>{{todo.title}}span>
            <button @click="remove(todo.id)">删除button>
          li>
        ul>
      div>
    
      <script>
        new Vue({
          el: '#app',
          data: {
            newTask: '',
            todos: []
          },
          created() {
            fetch('/todos').then(r => {
              return r.json()
            }).then(res => {
              let {code, result} = res
              if(code * 1 === 0) {
                this.todos = result
              }
            })
          },
          methods: {
            remove(id) {
              fetch('/remove', {
                method: 'post',
                headers: {
                  'Content-Type': 'application/json;charset=utf-8'
                },
                body: JSON.stringify({id})
              }).then(r => {
                return r.json()
              }).then(res => {
                let {code, result} = res
                if(code * 1 === 0) {
                  this.todos = this.todos.filter(todo => todo.id * 1 !== id * 1)
                } else {
                  alert(result)
                }
              })
            },
            add() {
              fetch('/add', {
                method: 'post',
                headers: {
                  'Content-Type': 'application/json;charset=utf-8'
                },
                body: JSON.stringify({title: this.newTask})
              }).then(r => {
                return r.json()
              }).then(res => {
                let {code, result} = res
                if(code * 1 === 0) {
                  this.todos.unshift(result)
                  this.newTask = ''
                } else {
                  alert(result)
                }
              })
            },
            toggle(id) {
              fetch('/toggle', {
                method: 'post',
                headers: {
                  'Content-Type': 'application/json;charset=utf-8'
                },
                body: JSON.stringify({id})
              }).then(r => {
                return r.json()
              }).then(res => {
                let {code, result} = res
                if(code * 1 === 0) {
                  let todo = this.todos.find(todo => todo.id * 1 === id * 1)
                  todo.done = !todo.done
                  alert(result)
                } else {
                  alert(result)
                }
              })
            }
          }
        })
      script>
    body>
    html>
    

    在这里插入图片描述是不是很熟悉了?Vue+ajax就这样应用了

    启动项目

    跟上面的不同,这里的index.html不再是需要后端渲染的模板引擎了,可以直接丢到服务器上运行的文件

    node app.js
    

    启动成功后,访问 http://localhost/public/index.html,静态资源托管还是没变,访问 /public 会映射到 /static 目录下。

    项目预览

    请添加图片描述

    总结

    路径是没变的,对数据的操作都是通过 ajax 请求局部改变数据部分内容的,不需要对整个页面重刷新。

    附录

    以上内容代码可以去这里自行clone。

  • 相关阅读:
    网络爬虫指南
    MySQL详细学习教程(建议收藏)
    Docker 网络简单了解
    KTL 一个支持C++14编辑公式的K线技术工具平台 - 第五版,支持sqlite3,全新sqlite3zz语法超简单使用sqlite3; 添加方差等统计函数。
    【基础教程】基于Matlab画花式箱体图
    灵活用工系统开发优势在哪里?
    Springboot全局异常和自定义异常
    【MySQL】锁
    Laravel文档阅读笔记-Adding a Markdown editor to Laravel
    java之ArrayList和Vector源码分析
  • 原文地址:https://blog.csdn.net/weixin_43443341/article/details/126952827