用Node肯定是要搞Web的, 视频到这里要写一个weather app, 但是我看了视频中提供的Dark Sky API的官网已经通知了, 被苹果收购, 所以不再接受新注册的API账号, 原来的API服务将在2020年年底结束.... 那就得看看视频里的Key能不能用了, 反正先跟着来, 不行就换一个免费的天气API来做
Node.js 发起HTTP请求
这里其实是要写一个新的APP了, 也就是天气app. 最开始要解决的就是哪里去查询API. HTTP请求包有很多种, 视频里使用的是request包. 我自己安装一看, 结果发现request包已经挂了.... 看来我自己用的话, 只能像之前看JS的教学的时候一样来使用axios了, 反正也是一直用过来的. 安装之:
npm install axios
安装的是 19.2版本. 然后我去注册了一个 https://home.openweathermap.org/的API. 最简单的查询API文档里列出了如下查询方法:
api.openweathermap.org/data/2.5/weather?q={city name}&appid={your api key}
, 按名称查询api.openweathermap.org/data/2.5/weather?id={city id}&appid={your api key}
, 按城市id查询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自己是没有模板解析引擎和渲染库, 必须要搭配其他库使用. 常用的两个库是handlebars和 hbs, 其中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字符串了, 如果找不到, 就会返回空结果.