高仿“饿了么”Vue项目(三)
今天我们来讲一讲node服务器相关的知识。
一、node服务器相关命令
平时我们经常会用到npm run dev命令来运行项目,今天就让我们来聊聊npm命令。
npm run命令会在项目的package.json文件中寻找scripts区域,例如:
"scripts": { "dev": "cross-env NODE_ENV=development supervisor --harmony index.js", "local": "cross-env NODE_ENV=local supervisor --harmony index.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "cross-env NODE_ENV=production pm2 start index.js --node-args='--harmony' --name 'node-elm'", "stop": "cross-env NODE_ENV=production pm2 stop index.js --name 'node-elm'", "restart": "cross-env NODE_ENV=production pm2 restart index.js --node-args='--harmony' --name 'node-elm'" },
这时,你可以输入npm run dev或任何scripts中的条目,也可以简写为npm dev,即省去run。
还有另一种方式也可以运行一个JS文件,即:node index.js。
让我们来仔细观察上述配置:
1. cross-env
Node.js有一个环境变量NODE_ENV,用于设置不同用途的node应用,如development(开发)、production(生产),因为开发和生产环境在运行项目时肯定会有一些差别。
如果在命令中指定环境变量的值,例如NODE_DEV=production,这时一些Windows系统会报错:“'NODE_ENV' 不是内部或外部命令,也不是可运行的程序”,cross-env这个小插件解决这个问题,让我们可以跨平台使用环境变量。
2. supervisor和pm2
在上面的脚本中,开发环境dev使用了“supervisor index.js”,生产环境start使用了“pm2 index.js”,那么这两者有何不同呢?
(1) supervisor
在node中,服务端的JS代码只有在node第一次引用,才会重新加载;如果node已经加载了某个文件,即使我们对它进行了修改, node也不会重新加载这个文件。那么,在开发过程中,要如何才能在修改某个文件后,直接刷新网页就能看到效果呢?
这时,可以使用supervisor这个插件运行你的JS文件。
(2) pm2
pm2是一个进程管理工具,它可以管理你的node进程,支持性能监控、进程守护、负载均衡等功能。pm2通常应用于生产环境。
3. --harmoney选项
NodeJS使用V8引擎,而V8引擎对ES6中的东西有部分支持,所以在NodeJS中可以使用一些ES6中的东西。但是由于很多东西只是草案而已,也许正式版会删除,所以还没有直接引入。而是把它们放在了和谐(harmony)模式下,在node的运行参数中加入--harmony标志才能启用。
二、express服务器
Express是一个基于Node.js平台的极简、灵活的Web应用开发框架,它提供一系列强大的特性,帮助你创建各种Web和移动设备应用。Express不对Node.js已有的特性进行二次抽象,只是在它之上扩展了Web应用所需的基本功能。
Node.js的原理我就不说了,总之,它的高并发访问性能绝对优秀。Express只在Node.js之上加了一层封装,它很适合作为服务器,可以是Web服务器(相当于Apache),也可以是应用服务器(相当于Tomcat)。
Express中有两个重要概念,我们需要好好了解一下。
1. 路由
作为一个服务器,Express采用路由的方式分发用户请求。例如你访问“/user”,就会调用某个JS函数,访问“/product”,就会调用另一个JS函数,即将一个URL与一个函数绑定在一起。
Express中注册路由的示例:
var server = express(); server.get('/user', function(req, res, next) { res.send('You are visiting user'); });
上例中,将“/user”这个URL与一个函数绑定在一起,访问这个URL,即执行该函数。
除了get方法,还有post、put、delete等常用HTTP方法,对应于不同的请求类型。get方法的第一个参数是一个URL,第二个参数是一个回调函数。回调函数中可以接收到三个参数:req(HTTP请求)、res(HTTP响应)、next(下一个路由)。
还有一个all方法,可以响应所有的HTTP请求(get、post、put、delete、......),只要URL匹配。
路由可以构成一个链,即一个URL可以连续执行多个回调函数,例如:
var cb0 = function (req, res, next) { console.log('CB0'); next(); // 执行下一个回调函数 } var cb1 = function (req, res, next) { console.log('CB1'); next(); // 执行下一个回调函数 } var cb2 = function (req, res) { res.send('Hello from C!'); // 执行完了 } server.get('/user', [cb0, cb1, cb2]);
2. 中间件
Express是一个自身功能极简,完全是由路由和中间件构成的一个Web开发框架。从本质上来说,一个Express应用就是在调用各种中间件。
中间件(Middleware)是一个函数,它可以访问请求对象(req)、响应对象(res)、下一个中间件(next)。
如果当前中间件没有终结请求-响应循环,则必须调用next()方法将控制权交给下一个中间件,否则请求就会挂起。
可以看到,中间件和路由的使用方式几乎一模一样。
Express中注册中间件的示例:
var server = express(); // 响应GET请求的中间件 server.get('/user/:id', function (req, res, next) { res.send('USER'); }); // 响应所有请求的中间件。use方法可以响应所有类型的HTTP请求 server.use('/user/:id', function (req, res, next) { console.log('Request Type:', req.method); next(); }); // 没有指定URL的中间件。每个请求都会执行该中间件 server.use(function (req, res, next) { console.log('Time:', Date.now()); next(); });
三、后端项目中的路由分析
让我们从后端项目的入口看起,index.js:
import express from 'express'; import db from './mongodb/db.js'; import config from 'config-lite'; import router from './routes/index.js'; import cookieParser from 'cookie-parser'; import session from 'express-session'; import connectMongo from 'connect-mongo'; import winston from 'winston'; import expressWinston from 'express-winston'; import path from 'path'; import history from 'connect-history-api-fallback'; import chalk from 'chalk'; // 创建Express服务器 const server = express(); // 注册全局路由 // “Acces-Control-Allow-”系列的HTTP头,用于设定访问控制 // 向response中写入HTTP头 server.all('*', (req, res, next) => { res.header("Access-Control-Allow-Origin", req.headers.Origin || req.headers.origin || 'https://cangdu.org'); res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); res.header("Access-Control-Allow-Credentials", true); res.header("X-Powered-By", '3.2.1'); next(); }); // 注册中间件,如cookie解析、session处理 server.use(cookieParser()); server.use(session(...)); // 注册路由,路由表写在/routes/index.js中 router(app); // 注册中间件,历史记录处理 server.use(history()); // 注册路由,express.static可以将指定目录下的所有文件变成静态路由 server.use(express.static('./public')); // 启动服务器 server.listen(config.port, () => { console.log(chalk.green(`成功监听端口:${config.port}`)); });
routes/index.js:
'use strict'; import v1 from './v1' import v2 from './v2' import v3 from './v3' import v4 from './v4' import ugc from './ugc' import bos from './bos' import eus from './eus' import admin from './admin' import statis from './statis' import member from './member' import shopping from './shopping' import promotion from './promotion' export default server => { server.use('/v1', v1); server.use('/v2', v2); server.use('/v3', v3); server.use('/v4', v4); server.use('/ugc', ugc); server.use('/bos', bos); server.use('/eus', eus); server.use('/admin', admin); server.use('/member', member); server.use('/statis', statis); server.use('/shopping', shopping); server.use('/promotion', promotion); }
这里可能不够严谨,应该把所有接口放在统一的路径下,如:/api/v1、/api/v2等。
进一步观察routes/v1.js:
const router = express.Router(); router.get('/cities', CityHandle.getCity); router.get('/cities/:id', CityHandle.getCityById); ......
我们可以看到,后端项目的接口分成多个模块(例如:/v1、/v2、......),每个模块又分别指定了多个路由。
这里用到的express.Router是一个模块化的路由,它将一组路由构成一个模块,便于路由的模块化管理。
四、前端项目中的路由分析
前端项目也从它的入口开始看起,先看package.json文件:
"scripts": { "dev": "cross-env NODE_ENV=online node build/dev-server.js", "local": "cross-env NODE_ENV=local node build/dev-server.js", "build": "node build/build.js" },
可以看到前端项目是直接用node命令执行build目录下的dev-server.js文件。
为什么前端项目没有像后端项目那样用supervisor或pm2启动Express呢?
因为前端项目比较复杂,各种技术混杂,前期需要很多处理。例如ES6、TypeScript需要转译成ES5,样式表语言less、sass需要转译成CSS,JS文件需要组合和压缩。这些工作都需要在服务器启动之前完成。
webpack可以看做是一个模块打包机,它分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言,并将其打包为合适的格式以供浏览器使用。
webpack还有很多插件,可以实现更多的功能。
我们来看一下前端程序的执行流程。
首先执行dev-server.js:
// 读项目配置文件/config/index.js var config = require('../config') if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) var path = require('path') var express = require('express') var webpack = require('webpack') var opn = require('opn') var proxyMiddleware = require('http-proxy-middleware') // 读webpack配置文件 var webpackConfig = require('./webpack.dev.conf') var port = process.env.PORT || config.dev.port // 创建Express服务器 var server = express() // 创建webpack var compiler = webpack(webpackConfig) // 创建webpack开发中间件:webpack-dev-middleware var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: webpackConfig.output.publicPath, stats: { colors: true, chunks: false } }) // 创建webpack热更新中间件:webpack-hot-middleware var hotMiddleware = require('webpack-hot-middleware')(compiler) compiler.plugin('compilation', function(compilation) { compilation.plugin('html-webpack-plugin-after-emit', function(data, cb) { hotMiddleware.publish({ action: 'reload' }) cb() }) }) // 设置HTTP代理中间件:http-proxy-middleware var context = config.dev.context switch(process.env.NODE_ENV){ case 'local': var proxypath = 'http://localhost:8001'; break; case 'online': var proxypath = 'http://elm.cangdu.org'; break; default: var proxypath = config.dev.proxypath; } var options = { target: proxypath, changeOrigin: true, } if (context.length) { server.use(proxyMiddleware(context, options)) } server.use(require('connect-history-api-fallback')()) server.use(devMiddleware) server.use(hotMiddleware) // 注册静态路由 var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) server.use(staticPath, express.static('./static')) // 启动服务器 module.exports = server.listen(port, function(err) { if (err) { console.log(err) return } var uri = 'http://localhost:' + port console.log('Listening at ' + uri + '\n') if (process.env.NODE_ENV !== 'testing') { opn(uri) } })
1. webpack-dev-middleware,开发中间件
webpack-dev-middleware的作用是监听资源的变更,自动打包。
2. webpack-hot-middleware,热更新中间件
webpack-hot-middleware一般和webpack-dev-middleware配合使用,实现页面的热更新。
3. http-proxy-middleware,HTTP代理中间件
为什么需要HTTP代理中间件?假设有如下情形:Web服务器运行于8000端口,应用服务器运行于8001端口,这时前端项目如果要调用后端项目的API,取JSON数据,要这样访问:http://127.0.0.1:8081/api/user,由于两个项目没有运行在同一个端口上,就存在跨域问题。
跨域是指从一个域名的网页去请求另一个域名的资源。比如从www.baidu.com页面去请求www.google.com的资源。跨域的严格一点的定义是:只要协议、域名、端口有任何一个不同,就被当作是跨域。
为什么浏览器要限制跨域访问呢?原因就是安全问题:如果一个网页可以随意地访问另外一个网站的资源,那么就有可能在客户完全不知情的情况下出现安全问题。
既然有安全问题,那为什么又要跨域呢? 因为有时公司内部有多个不同的子域,比如一个是location.company.com,而应用是放在app.company.com,这时想从app.company.com去访问location.company.com的资源就属于跨域。
http-proxy-middleware这个中间件的作用就是解决跨域访问的问题。
使用http-proxy-middleware的相关代码:
var context = config.dev.context switch(process.env.NODE_ENV) { case 'local': var proxypath = 'http://localhost:8001'; break; case 'online': var proxypath = 'http://elm.cangdu.org'; break; default: var proxypath = config.dev.proxypath; } var options = { target: proxypath, // 目标URL changeOrigin: true, // 是否将原主机头改为目标URL } // 创建HTTP代理中间件,有两个参数: // context:被代理的URL // options:选项 if (context.length) { server.use(proxyMiddleware(context, options)) }
上例的context参数使用到了config.dev.context,这个配置对象定义在/config/index.js中:
context: [ '/shopping', '/ugc', '/v1', '/v2', '/v3', '/v4', '/bos', '/member', '/promotion', '/eus', '/payapi', '/img', ],
一旦使用了HTTP代理中间件,在前端项目中就可以直接访问“/shopping”,中间件会把这个请求转发给目标URL,并处理好跨域问题。
五、前端路由和后端路由
可以看到,在饿了么这样的单页应用中,后端只负责给前端喂数据,或将前端来的数据保存到数据库中,数据往来使用JSON格式。后端发布了相应的访问API,只要你输入正确的URL,就会得到后端的响应。
我们把后端项目的路由称为后端路由,实质上是一组访问API。后端的实现其实Java语言也做得很好,SpringMVC就是干这个的。只不过node使用JavaScript语言,前端开发人员比较熟悉。现在JavaScript语言等于抢了一块Java语言的蛋糕。
至于前端部分,因为前端项目经常是单页应用,总共就一个index.html页面,所以页面跳转根本不存在。但确实有必要在不同功能页上切换,一般用一个URL代表一个组件,所以也存在路由问题。前端路由通常由开发框架管理,例如Vue框架就有vue-router模块。