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

【转】https://blog.csdn.net/hanhf/article/details/80194294

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


  今天我们来讲一讲node服务器相关的知识。


  一、node服务器相关命令


  平时我们经常会用到npm run dev命令来运行项目,今天就让我们来聊聊npm命令。


  npm run命令会在项目的package.json文件中寻找scripts区域,例如:


[javascript]  view plain  copy
  1. "scripts": {  
  2.   "dev""cross-env NODE_ENV=development supervisor --harmony index.js",  
  3.   "local""cross-env NODE_ENV=local supervisor --harmony index.js",  
  4.   "test""echo \"Error: no test specified\" && exit 1",  
  5.   "start""cross-env NODE_ENV=production pm2 start index.js --node-args='--harmony' --name 'node-elm'",  
  6.   "stop""cross-env NODE_ENV=production pm2 stop index.js --name 'node-elm'",  
  7.   "restart""cross-env NODE_ENV=production pm2 restart index.js --node-args='--harmony' --name 'node-elm'"  
  8. },  

  这时,你可以输入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中注册路由的示例:

[javascript]  view plain  copy
  1. var server = express();  
  2.   
  3. server.get('/user'function(req, res, next) {  
  4.   res.send('You are visiting user');  
  5. });  


  上例中,将“/user”这个URL与一个函数绑定在一起,访问这个URL,即执行该函数。

  除了get方法,还有post、put、delete等常用HTTP方法,对应于不同的请求类型。get方法的第一个参数是一个URL,第二个参数是一个回调函数。回调函数中可以接收到三个参数:req(HTTP请求)、res(HTTP响应)、next(下一个路由)。


  还有一个all方法,可以响应所有的HTTP请求(get、post、put、delete、......),只要URL匹配。


  路由可以构成一个链,即一个URL可以连续执行多个回调函数,例如:


[javascript]  view plain  copy
  1. var cb0 = function (req, res, next) {  
  2.   console.log('CB0');  
  3.   next(); // 执行下一个回调函数  
  4. }  
  5.   
  6. var cb1 = function (req, res, next) {  
  7.   console.log('CB1');  
  8.   next(); // 执行下一个回调函数  
  9. }  
  10.   
  11. var cb2 = function (req, res) {  
  12.   res.send('Hello from C!'); // 执行完了  
  13. }  
  14.   
  15. server.get('/user', [cb0, cb1, cb2]);  


  2. 中间件


  Express是一个自身功能极简,完全是由路由和中间件构成的一个Web开发框架。从本质上来说,一个Express应用就是在调用各种中间件。
  中间件(Middleware)是一个函数,它可以访问请求对象(req)、响应对象(res)、下一个中间件(next)。

  如果当前中间件没有终结请求-响应循环,则必须调用next()方法将控制权交给下一个中间件,否则请求就会挂起。

  可以看到,中间件和路由的使用方式几乎一模一样。


  Express中注册中间件的示例:


[javascript]  view plain  copy
  1. var server = express();  
  2.   
  3. // 响应GET请求的中间件  
  4. server.get('/user/:id'function (req, res, next) {  
  5.   res.send('USER');  
  6. });  
  7.   
  8. // 响应所有请求的中间件。use方法可以响应所有类型的HTTP请求  
  9. server.use('/user/:id'function (req, res, next) {  
  10.   console.log('Request Type:', req.method);  
  11.   next();  
  12. });  
  13.   
  14. // 没有指定URL的中间件。每个请求都会执行该中间件  
  15. server.use(function (req, res, next) {  
  16.   console.log('Time:', Date.now());  
  17.   next();  
  18. });  


  三、后端项目中的路由分析


  让我们从后端项目的入口看起,index.js:


[javascript]  view plain  copy
  1. import express from 'express';  
  2. import db from './mongodb/db.js';  
  3. import config from 'config-lite';  
  4. import router from './routes/index.js';  
  5. import cookieParser from 'cookie-parser';  
  6. import session from 'express-session';  
  7. import connectMongo from 'connect-mongo';  
  8. import winston from 'winston';  
  9. import expressWinston from 'express-winston';  
  10. import path from 'path';  
  11. import history from 'connect-history-api-fallback';  
  12. import chalk from 'chalk';  
  13.   
  14. // 创建Express服务器  
  15. const server = express();  
  16.   
  17. // 注册全局路由  
  18. // “Acces-Control-Allow-”系列的HTTP头,用于设定访问控制  
  19. // 向response中写入HTTP头  
  20. server.all('*', (req, res, next) => {  
  21.     res.header("Access-Control-Allow-Origin", req.headers.Origin || req.headers.origin || 'https://cangdu.org');  
  22.     res.header("Access-Control-Allow-Headers""Content-Type, Authorization, X-Requested-With");  
  23.     res.header("Access-Control-Allow-Methods""PUT,POST,GET,DELETE,OPTIONS");  
  24.     res.header("Access-Control-Allow-Credentials"true);  
  25.     res.header("X-Powered-By"'3.2.1');  
  26.     next();  
  27. });  
  28.   
  29. // 注册中间件,如cookie解析、session处理  
  30. server.use(cookieParser());  
  31. server.use(session(...));  
  32.   
  33. // 注册路由,路由表写在/routes/index.js中  
  34. router(app);  
  35.   
  36. // 注册中间件,历史记录处理  
  37. server.use(history());  
  38. // 注册路由,express.static可以将指定目录下的所有文件变成静态路由  
  39. server.use(express.static('./public'));  
  40.   
  41. // 启动服务器  
  42. server.listen(config.port, () => {  
  43.     console.log(chalk.green(`成功监听端口:${config.port}`));  
  44. });  


  routes/index.js:


[javascript]  view plain  copy
  1. 'use strict';  
  2.   
  3. import v1 from './v1'  
  4. import v2 from './v2'  
  5. import v3 from './v3'  
  6. import v4 from './v4'  
  7. import ugc from './ugc'  
  8. import bos from './bos'  
  9. import eus from './eus'  
  10. import admin from './admin'  
  11. import statis from './statis'  
  12. import member from './member'  
  13. import shopping from './shopping'  
  14. import promotion from './promotion'  
  15.   
  16. export default server => {  
  17.     server.use('/v1', v1);  
  18.     server.use('/v2', v2);  
  19.     server.use('/v3', v3);  
  20.     server.use('/v4', v4);  
  21.     server.use('/ugc', ugc);  
  22.     server.use('/bos', bos);  
  23.     server.use('/eus', eus);  
  24.     server.use('/admin', admin);  
  25.     server.use('/member', member);  
  26.     server.use('/statis', statis);  
  27.     server.use('/shopping', shopping);  
  28.     server.use('/promotion', promotion);  
  29. }  


  这里可能不够严谨,应该把所有接口放在统一的路径下,如:/api/v1、/api/v2等。


  进一步观察routes/v1.js:


[javascript]  view plain  copy
  1. const router = express.Router();  
  2.   
  3. router.get('/cities', CityHandle.getCity);  
  4. router.get('/cities/:id', CityHandle.getCityById);  
  5. ......  


  我们可以看到,后端项目的接口分成多个模块(例如:/v1、/v2、......),每个模块又分别指定了多个路由。

  这里用到的express.Router是一个模块化的路由,它将一组路由构成一个模块,便于路由的模块化管理。


  四、前端项目中的路由分析


  前端项目也从它的入口开始看起,先看package.json文件:


[javascript]  view plain  copy
  1. "scripts": {  
  2.   "dev""cross-env NODE_ENV=online node build/dev-server.js",  
  3.   "local""cross-env NODE_ENV=local node build/dev-server.js",  
  4.   "build""node build/build.js"  
  5. },  


  可以看到前端项目是直接用node命令执行build目录下的dev-server.js文件。

  为什么前端项目没有像后端项目那样用supervisor或pm2启动Express呢?

  因为前端项目比较复杂,各种技术混杂,前期需要很多处理。例如ES6、TypeScript需要转译成ES5,样式表语言less、sass需要转译成CSS,JS文件需要组合和压缩。这些工作都需要在服务器启动之前完成。

  webpack可以看做是一个模块打包机,它分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言,并将其打包为合适的格式以供浏览器使用。

  webpack还有很多插件,可以实现更多的功能。


  我们来看一下前端程序的执行流程。


  首先执行dev-server.js:


[javascript]  view plain  copy
  1. // 读项目配置文件/config/index.js  
  2. var config = require('../config')  
  3. if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)  
  4. var path = require('path')  
  5. var express = require('express')  
  6. var webpack = require('webpack')  
  7. var opn = require('opn')  
  8. var proxyMiddleware = require('http-proxy-middleware')  
  9. // 读webpack配置文件  
  10. var webpackConfig = require('./webpack.dev.conf')  
  11.   
  12. var port = process.env.PORT || config.dev.port  
  13.   
  14. // 创建Express服务器  
  15. var server = express()  
  16. // 创建webpack  
  17. var compiler = webpack(webpackConfig)  
  18.   
  19. // 创建webpack开发中间件:webpack-dev-middleware  
  20. var devMiddleware = require('webpack-dev-middleware')(compiler, {  
  21.     publicPath: webpackConfig.output.publicPath,  
  22.     stats: {  
  23.         colors: true,  
  24.         chunks: false  
  25.     }  
  26. })  
  27.   
  28. // 创建webpack热更新中间件:webpack-hot-middleware  
  29. var hotMiddleware = require('webpack-hot-middleware')(compiler)  
  30. compiler.plugin('compilation'function(compilation) {  
  31.     compilation.plugin('html-webpack-plugin-after-emit'function(data, cb) {  
  32.         hotMiddleware.publish({  
  33.             action: 'reload'  
  34.         })  
  35.         cb()  
  36.     })  
  37. })  
  38.   
  39. // 设置HTTP代理中间件:http-proxy-middleware  
  40. var context = config.dev.context  
  41. switch(process.env.NODE_ENV){  
  42.     case 'local'var proxypath = 'http://localhost:8001'break;  
  43.     case 'online'var proxypath = 'http://elm.cangdu.org'break;  
  44.     default:  var proxypath = config.dev.proxypath;   
  45. }  
  46. var options = {  
  47.     target: proxypath,  
  48.     changeOrigin: true,  
  49. }  
  50. if (context.length) {  
  51.     server.use(proxyMiddleware(context, options))  
  52. }  
  53.   
  54. server.use(require('connect-history-api-fallback')())  
  55.   
  56. server.use(devMiddleware)  
  57.   
  58. server.use(hotMiddleware)  
  59.   
  60. // 注册静态路由  
  61. var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)  
  62. server.use(staticPath, express.static('./static'))  
  63.   
  64. // 启动服务器  
  65. module.exports = server.listen(port, function(err) {  
  66.     if (err) {  
  67.         console.log(err)  
  68.         return  
  69.     }  
  70.     var uri = 'http://localhost:' + port  
  71.     console.log('Listening at ' + uri + '\n')  
  72.   
  73.     if (process.env.NODE_ENV !== 'testing') {  
  74.         opn(uri)  
  75.     }  
  76. })  


  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的相关代码:


[javascript]  view plain  copy
  1. var context = config.dev.context  
  2. switch(process.env.NODE_ENV) {  
  3.     case 'local'var proxypath = 'http://localhost:8001'break;  
  4.     case 'online'var proxypath = 'http://elm.cangdu.org'break;  
  5.     defaultvar proxypath = config.dev.proxypath;  
  6. }  
  7. var options = {  
  8.     target: proxypath, // 目标URL  
  9.     changeOrigin: true// 是否将原主机头改为目标URL  
  10. }  
  11. // 创建HTTP代理中间件,有两个参数:  
  12. // context:被代理的URL  
  13. // options:选项  
  14. if (context.length) {  
  15.     server.use(proxyMiddleware(context, options))  
  16. }  


  上例的context参数使用到了config.dev.context,这个配置对象定义在/config/index.js中:


[javascript]  view plain  copy
  1. context: [  
  2.     '/shopping',  
  3.     '/ugc',  
  4.     '/v1',  
  5.     '/v2',  
  6.     '/v3',  
  7.     '/v4',  
  8.     '/bos',  
  9.     '/member',  
  10.     '/promotion',  
  11.     '/eus',  
  12.     '/payapi',  
  13.     '/img',  
  14. ],  


  一旦使用了HTTP代理中间件,在前端项目中就可以直接访问“/shopping”,中间件会把这个请求转发给目标URL,并处理好跨域问题。


  五、前端路由和后端路由


  可以看到,在饿了么这样的单页应用中,后端只负责给前端喂数据,或将前端来的数据保存到数据库中,数据往来使用JSON格式。后端发布了相应的访问API,只要你输入正确的URL,就会得到后端的响应。

  我们把后端项目的路由称为后端路由,实质上是一组访问API。后端的实现其实Java语言也做得很好,SpringMVC就是干这个的。只不过node使用JavaScript语言,前端开发人员比较熟悉。现在JavaScript语言等于抢了一块Java语言的蛋糕。

  至于前端部分,因为前端项目经常是单页应用,总共就一个index.html页面,所以页面跳转根本不存在。但确实有必要在不同功能页上切换,一般用一个URL代表一个组件,所以也存在路由问题。前端路由通常由开发框架管理,例如Vue框架就有vue-router模块。


猜你喜欢

转载自blog.csdn.net/weixin_41858951/article/details/80654507