高仿“饿了么”Vue项目(三)
高仿“饿了么”Vue项目(三) 今天我们来讲一讲node服务器相关的知识。 一、node服务器相关命令 平时我们经常会用到npm run dev命令来运行项目,今天就让我们来聊聊npm命令。 npm run命令会在项目的package.json文件中寻找scripts区域,例如:"scripts": {"dev": "cross-env N
高仿“饿了么”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模块。
更多推荐
所有评论(0)