用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.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字符串了, 如果找不到, 就会返回空结果.