Node 03 网络请求与express后端

Node 03 网络请求与express后端

用Node肯定是要搞Web的, 视频到这里要写一个weather app, 但是我看了视频中提供的Dark Sky API的官网已经通知了, 被苹果收购, 所以不再接受新注册的API账号, 原来的API服务将在2020年年底结束.... 那就得看看视频里的Key能不能用了, 反正先跟着来, 不行就换

用Node肯定是要搞Web的, 视频到这里要写一个weather app, 但是我看了视频中提供的Dark Sky API的官网已经通知了, 被苹果收购, 所以不再接受新注册的API账号, 原来的API服务将在2020年年底结束.... 那就得看看视频里的Key能不能用了, 反正先跟着来, 不行就换一个免费的天气API来做

  1. Node.js 发起HTTP请求

  2. express搭建后端

  3. express中使用模板渲染

  4. express 添加404页面

  5. 页面版的查询

  6. API版本的查询

Node.js 发起HTTP请求

这里其实是要写一个新的APP了, 也就是天气app. 最开始要解决的就是哪里去查询API. HTTP请求包有很多种, 视频里使用的是request包. 我自己安装一看, 结果发现request包已经挂了.... 看来我自己用的话, 只能像之前看JS的教学的时候一样来使用axios了, 反正也是一直用过来的. 安装之:

npm install axios

安装的是 19.2版本. 然后我去注册了一个 https://home.openweathermap.org/的API. 最简单的查询API文档里列出了如下查询方法:

  1. api.openweathermap.org/data/2.5/weather?q={city name}&appid={your api key}, 按名称查询

  2. api.openweathermap.org/data/2.5/weather?id={city id}&appid={your api key}, 按城市id查询

  3. api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={your api key},按经纬度查询

我尝试了一下在浏览器里查询Shanghai, 得到如下结果:

{
  coord: { lon: 121.46, lat: 31.22 }, //经纬度
  weather: [ { id: 800, main: 'Clear', description: 'clear sky', icon: '01d' } ], //天气情况的id, 参数, 描述和图标id
  base: 'stations',
  main: {
    temp: 307.6, //气温, 默认是开尔文
    feels_like: 313.35, //体感温度
    temp_min: 306.15, //最低温度
    temp_max: 310.37, //最高温度
    pressure: 1002,  //气压hPa
    humidity: 70     //湿度
  },
  visibility: 10000,  //可见度, 以米计量
  wind: { speed: 4, deg: 230 }, //风向, 速度和角度
  clouds: { all: 7 }, //云百分比
  dt: 1597028871, //UTC时间戳
  sys: {
    type: 1,
    id: 9659,
    country: 'CN',      //国家代码
    sunrise: 1597007802, //日升时间
    sunset: 1597056147   //日落时间
  },
  timezone: 28800,  //时区与UTC时间差异
  id: 1796236,      //城市id
  name: 'Shanghai', //城市名称
  cod: 200
}

然后就可以编写针对上边三种查询的函数来尝试一下结果:

const axios = require("axios");

const key = 'XXX'
const url = 'http://api.openweathermap.org/data/2.5/weather'

const byId = async (cityId) => {
    return await axios.get(url, {
        params: {
            id: cityId,
            appid: key
        }
    })
};

const byName = async (cityName) => {
    return await axios.get(url, {
        params: {
            q: cityName,
            appid: key
        }
    });
};

const byLatAndLon = async (lat, lon) => {
    return await axios.get(url, {
        params: {
            lat: lat,
            lon: lon,
            appid: key
        }
    });
};

module.exports = {
    byId:byId,
    byLatAndLon: byLatAndLon,
    byName: byName
}

这样在主程序里, 就可以如下来使用:

const result = weather.byLatAndLon(31.22,  121.46);

result.then(resolve => console.log(resolve.data)).catch((error) => console.log("出错"));

这样就可以查询到上海的天气情况了, 有了这个基础铺垫以后, 后边就可以来编写这个weather app的其他部分, 也就是一个web服务器了.

express搭建后端

后端说实在的当然是Java啦, 不过动态语言像Python和Node.js, 搭建后端的速度快一些, 而Java重在大型工程. 如果现在让我写处理Excel的程序, 那还只有快速用Python后端才行. Node.js里这次使用express来搭建后端, 像其文档里说的一样, 是一个Fast, unopinionated, minimalist web framework for node. 老样子先安装, 既然是个框架, 那少不了对外暴露API的设置, 写处理请求的函数了.

npm i express

express的基础用法很简单, 先创建一个对象, 然后对于暴露的api编写函数即可, 如下:

const weather = require("./weather");
const express = require("express");

const app = express();
const baseURL = '/'
app.get(baseURL, (request, response) => {
    console.log(request);
    response.send("Hello world!");
});
app.listen(8000);

写了两年web应用的我们一看就知道是怎么回事了, 接下来就是看如何返回JSON. 其实也很简单, 就是在send中传入一个对象,就会自动将其转换成JSON:

app.get(baseURL+"json", (request, response) => {
    console.log(request);
    response.send({
        job: "cannot find",
        timetofind: "3month"
    });
});

这样访问/json的时候, 就看到显示出了JSON字符串. 然后是返回静态文件, 也就是HTML JS CSS这种东西, 其实也简单, express使用一个.static()函数, 其中传入静态资源所在的目录 在项目下新建staticfiles目录, 然后在其中新建一个index.html文件, 然后编写如下代码:

const weather = require("./weather");
const path = require("path");

const express = require("express");
const app = express();
const filePath = path.join(__dirname,'/staticfiles')

app.use(express.static(filePath));

const baseURL = '/'

app.get(baseURL, (request, response) => {
    console.log(request);
    response.send("<h1 style='text-align: center'>Hello world!</h1>");
});

app.get(baseURL+"json", (request, response) => {
    console.log(request);
    response.send({
        job: "cannot find",
        timetofind: "3month"
    });
});

app.listen(8000);

再此启动服务器之后, 虽然下边写了对应路径的访问, 但是默认就会显示index.html的内容, 说明会覆盖根路径, 不过/json路径依然有效. 对于单页面应用来说, 返回一个HTML也足够了, 所以可以把访问根目录的get方法删除了. 存在多个页面的情况下, 可以直接用html文件路径访问, 比如http://localhost:8000/about.html 之后是载入js和css文件, 在staticfiles目录下创建css, 然后弄一个样式文件style.css:

h1 {
    color: orangered;
    font-size: 60px;
}

这里关键是index.html中的路径设置, 要如何设置才能找到css文件呢, 其实已经知道根目录对应的实际目录, 在header标签中写上就行了:

<link rel="stylesheet" href="/css/style.css">

这样就生效了, 知道了这个方式之后, 所有的静态文件都是一样. 由于Node.js主要开发的就是前后端分离, 所以一般不会重型的去渲染. 然后在staticfiles中创建一个新的也叫app.js的JS文件, 将其也引入到页面来, 就可以来编写Web应用了. 学过Web开发就是方便, 再看Web应用驾轻就熟, 还缺最后一个, 就是使用模板来渲染.

express中使用模板渲染

express自己是没有模板解析引擎和渲染库, 必须要搭配其他库使用. 常用的两个库是handlebarshbs, 其中hbs和express搭配很方便, 可以做为模板渲染引擎. 使用 npm i hbs 安装之后, 按照文档, 加上一行:

app.set('view engine', 'hbs');

这一行的意思是调用res.render方法的时候会使用hbs文件, 如果将HTML作为模板, 则需要改成:

app.set('view engine', 'html');

使用hbs之后, 在weather-app下边(不是静态文件夹下边), 需要创建一个固定的views文件夹, 其中用来存放模板. 然后删除静态文件index.html, 之后访问根目录就需要使用res.render函数来渲染了:

app.get('', (req, res) => {
    res.render('index');
});

这个'index'需要与view中的'index.hbs'去掉后缀名的名称一致, 以方便找到模板. 可见Web应用都差不多,这个与Spring MVC也非常相似. 有了模板, 然后就是渲染其中的内容了, render函数还接受第二个参数就是一个对象, 将模板中变量替换成实际的值:

app.get('', (req, res) => {
    res.render('index',{
        title: 'Weather App',
        name: 'Minkopig'
    });
});

修改模板如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>express 测试首页</title>
    <link rel="stylesheet" href="/css/style.css">
    <script src="/js/app.js"></script>
</head>
<body>
<h1 style="text-align: center">Express 模板文件 来自于 index.hbs</h1>
<p>This is {{title}}</p>
<p>Author: {{name}}</p>
</body>
</html>

就可以渲染出来结果了. 自定义视图目录也是可以的, 只要使用:

const viewsPath = path.join(__dirname,'/templates')
app.set('views', viewsPath);

这样设置之后, 记得将原来的views目录改成templates目录即可. 模板还可以使用partials来渲染一小段页面, 类似于Django中一小段, 了解即可.

express 添加404页面

这个其实就是路径匹配的顺序. express在内部写每个路由的时候, 是有顺序的, 就类似Django中path的匹配顺序, 目前顺序如下:

app.get('', (req, res) => {
    res.render('index',{
        title: 'Weather App',
        name: 'Minkopig'
    });
});

app.get('/json', (req, res) => {
    res.send({
        title: 'Weather App',
        name: 'Minkopig'
    });
});

这表明先访问的根路径, 会渲染模板, /json路径则返回一个JSON字符串. 所以就和很多Web框架一下, 在最后加上一个接住所有匹配URL的路径就可以了:

app.get('*', (req, res) => {
    res.render('page404');
});

page404.hbs如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>express 404</title>
    <link rel="stylesheet" href="/css/style.css">
    <script src="/js/app.js"></script>
</head>
<body>
<h1 style="text-align: center">找不到页面</h1>
<p>This is {{title}}</p>
<p>Author: {{name}}</p>
<p><a href="/">返回首页</a></p>
</body>
</html>

这样就添加了一个错误页面, 只要匹配不了的URL都会到这个页面来. 这里涉及到express的路由匹配规则, 有兴趣就可以看文档, 不过确实用这个起一个后端还是挺快的. express还真是一个短小精悍的后端, 只要再挂上数据库就差不多了.

页面版的查询

这里要写实际业务了, 实际业务可能会分为两块, 一块是渲染页面的, 也就是用户在页面上输入城市名称, 然后显示该城市的天气信息, 其他也是一样. 还需要提供一个JSON接口, 可以像天气API一样, 使用URL参数来获取结果, 然后返回一个JSON字符串. 其实对于我也都驾轻就熟了, 现在就可以来开始编写了. 页面版的查询搞一个最简单的就可以, 也就是一个input框用于输入城市的名称, 然后点击一个按钮, 就可以在页面上显示出来查询的结果. 这次就可以把那些练习代码全部都删除掉, 正式来编写, 首页返回index.hbs:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>查询天气</title>
    <link rel="stylesheet" href="/css/style.css">
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.20.0-0/axios.js"></script>

</head>
<body>
<h1 style="text-align: center">查询天气</h1>
<div class="container">
    <input type="text" id="city">
    <button>查询</button>
</div>

<script src="/js/app.js"></script>

</body>
</html>

然后要编写app.js, 注意app.js虽然在项目中, 但是实际上由浏览器导入, 所以无法使用像node这样的功能, 必须老实引入额外的axios文件在index.hbs中. 然后就要编写业务代码了:

const input = document.querySelector('input');
const button = document.querySelector('button');
const key = '4bea17900b1c68b05c79eb7b7af7dd10'
const url = 'http://api.openweathermap.org/data/2.5/weather'
const resultArea = document.getElementById("result");
input.value = 'shanghai';

async function handleClick() {
    button.disabled = true;
    await axios.get(url, {
        params: {
            q: input.value,
            appid: key
        }
    }).then(resolve => {
        console.log(resolve.data);
        renderResult(resolve.data);
        button.disabled = false;
    }).catch(() => {
        resultArea.innerHTML = "Cannot find " + input.value;
        button.disabled = false;
    });
}

function renderResult(result) {
    let temp = (parseFloat(result.main.temp) - 273.15).toFixed(2);
    resultArea.innerHTML = `
    <p>City: ${result.sys.country} ${result.name}</p>
    <p>Temperature: ${temp}</p>
    <p>Weather: ${result.weather[0].main} ${result.weather[0].description}</p>
    `
}

button.addEventListener('click', handleClick);

这个业务代码还是挺简单的, 发送异步请求同时阻挡住反复请求, 之后根据返回结果, 渲染结果区域或者显示找不到. 样式稍微修饰了一下:

h1 {
    color: grey;
    font-size: 48px;
}

.container {
    max-width: 960px;
    margin: 2em auto;
}

input {
    font-size: 1.2em;
    color: orangered;
    font-family: Tahoma, Geneva, sans-serif;
}

button {
    background-color: dodgerblue;
    font-size: 1.2em;
    color: white;
    padding: 5px 10px;
    border: none;
    border-radius: 5px;
    box-shadow: 0 0 4px white;
}

label {
    font-size: 1.2em;
    padding-left: 5px;
    padding-right: 5px;
}

API版本的查询

这个因为是直接通过express提供服务, 所以不是写在页面引入的app.js中, 而是写在启动node的app.js中. 这里我们仅仅就使用一个city参数, 然后是get请求, 来获取一下结果, 新的app.js如下:

const axios = require("axios");
const url = 'http://api.openweathermap.org/data/2.5/weather'
const path = require("path");
const key = '4bea17900b1c68b05c79eb7b7af7dd10'
const express = require("express");
const app = express();
const filePath = path.join(__dirname, '/staticfiles')
const viewsPath = path.join(__dirname, '/templates')

app.set('view engine', 'hbs');
app.set('views', viewsPath);
app.use(express.static(filePath));

app.get('', (req, res) => {
    res.render('index', {
        title: 'Weather App',
        name: 'Minkopig'
    });
});

app.get('/api', (req, res) => {
    if (req.query.city) {
        axios.get(url, {
            params: {
                q: req.query.city,
                appid: key
            }
        }).then(resolve => res.send(resolve.data)).catch(() => res.send({}));
    } else {
        res.send({});
    }
});


app.listen(8000);

这样只需要以http://localhost:8000/api?city=shanghai这样的查询就可以得到JSON字符串了, 如果找不到, 就会返回空结果.

LICENSED UNDER CC BY-NC-SA 4.0
Comment