高仿“饿了么”Vue项目(三)

时间:2022-12-28 11:58:08

高仿“饿了么”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模块。