稀土掘金 稀土掘金

Next.js 服务端渲染框架实战

基于React.js 技术栈的服务端渲染框架Next.js 实战记录

第一次在掘金上发布文章,本着学习的态度,将自己运用Next.js开发服务端渲染的项目复原总结出来,巩固知识点,也可以跟同行探讨下技术。(文章不断完善中...)

1.项目背景

公司原有项目基于PHP和jQuery混合开发的,提出重构需求。但是后端技术栈由PHP更替为Java微服务,前端技术栈也从jQuery更替为React.js。因为公司所处行业需要做线上推广,那项目重构必须得考虑对SEO优化友好了,自己平时更多的是用React.js技术栈做前端开发,于是找到了Next.js(基于React)这个服务端渲染框架。

2.基本需求

  • 搜索引擎收录: SEO友好,利于搜索引起爬取页面信息
  • 路由美化: 路由需要按照规律来展现
  • 根据不同城市显示不同数据: 需要根据具体城市(IP定位)来展示不同城市数据
  • PC端/M端: 根据设备判断渲染不同客户端的组件模板
  • SEO信息可配置: 每个首屏子页面(如首页、关于我们、公司介绍页)支持SEO信息可配置
  • 支持开发/正式环境切换: 根据命令行判断当前环境,配置API接口前缀
  • 微信授权文件部署:微信支付授权文件*.txt 文件在项目根目录部署
  • 部分Http请求代理处理:部分接口跨域处理(http-proxy-middleware)
  • 类似重定向处理:访问当前域名,拉取不同域名下的页面数据,展示在当前路由下
  • 本地数据模拟:会是尝试用mock.js / json-server / faker.js 方式 [api文档管理工具可以用yapi]
  • 项目部署方式:项目两种部署方式,基于Docker部署和Node部署(本地也会用docker调试)

3.Next.js原理

中文官网 Next.js 是一个轻量级的 React 服务端渲染应用框架。 服务端渲染的理解:其实很多人接触过服务端渲染,最传统的PHP嵌套静态html页面就是服务端渲染的一种。PHP通过模板引擎把从数据库取到的数据渲染到html种,当前端访问指定路由时,php发送给前台指定的页面,这个页面在浏览器端识别到的是.html 文件(Content-type:text/html),浏览器按照静态html文件格式解析页面渲染后展示出来,用浏览器查看源代码时就是丰富的html标签还有标签里的文本信息,例如SEO信息,文章标题/内容等。这样的页面搜索引擎就可以很容易抓取到了。Next.js 原理类似,只不过后端的语言是Node而已,在React组件中嵌入getInitialProps方法获取到的服务端动态数据,在服务端把React组件渲染成html页面,发送到前台。

4.Next.js关键点

文件系统:

Next文件系统规定,在pages文件夹下每个*.js 文件将变成一个路由,自动处理和渲染

新建 ./pages/index.js 到你的项目中, 项目运行后可以通过 localhost:3000/index 路径访问到页面。同理 ./pages/second.js 可以通过localhost:3000/second访问到

静态文件服务:

如图片,字体,js工具类

在根目录下新建文件夹叫static。代码可以通过/static/来引入相关的静态资源 不要自定义静态文件夹的名字,只能叫static ,因为只有这个名字 Next.js 才会把它当作静态资源

export default () => <img src="/static/my-image.png" alt="my image" />

数据获取:

Next.js 能实现服务端渲染的关键点就在这里了。getInitialProps函数提供获取数据的生命周期钩子

创建一个有状态、生命周期或有初始数据的 React 组件

import React from 'react'

export default class extends React.Component {
  static async getInitialProps({ req }) {
    const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
    return { userAgent } // 这里绑定userAgent数据到Props,组件里就可以用 this.props.userAgent访问到了
  }

  render() {
    const { userAgent } = this.props // ES6解构赋值 
    return (
      <div>
        Hello World {userAgent}
      </div>
    )
  }
}

==========================================

// 无状态组件定义getInitialProps *这种方式也只能用在pages目录下
const Page = ({ stars }) =>
  <div>
    Next stars: {stars}
  </div>

Page.getInitialProps = async ({ req }) => {
  const res = await fetch('https://api.github.com/repos/zeit/next.js')
  const json = await res.json()
  return { stars: json.stargazers_count }
}

export default Page

上面代码通过异步方法 getInitialProps 获取数据,绑定在props。服务渲染时,getInitialProps将会把数据序列化,就像JSON.stringify。页面初始化加载时,getInitialProps只会加载在服务端。只有当路由跳转(Link组件跳转或 API 方法跳转)时,客户端才会执行getInitialProps

划重点:getInitialProps将不能使用在子组件中。只能使用在pages页面中 子组件可以通过pages文件夹下的页面获取数据,然后Props传值到子组件

getInitialProps入参对象的属性如下

  • pathname - URL 的 path 部分
  • query - URL 的 query 部分,并被解析成对象
  • asPath - 显示在浏览器中的实际路径(包含查询部分),为String类型
  • req - HTTP 请求对象 (只有服务器端有)
  • res - HTTP 返回对象 (只有服务器端有)
  • jsonPageRes - 获取数据响应对象 (只有客户端有)
  • err - 渲染过程中的任何错误

用 组件实现客户端的路由切换

如果需要注入pathname, query 或 asPath到你组件中,你可以使用withRouter高阶组件

// pages/index.js
import Link from 'next/link'

export default () =>
  <div>
    Click{' '}
    <Link href="/about">
      <a>here</a>
    </Link>{' '}
    to read more
  </div>
  
// 高阶组件
import { withRouter } from 'next/router'

const ActiveLink = ({ children, router, href }) => {
  const style = {
    marginRight: 10,
    color: router.pathname === href? 'red' : 'black'
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default withRouter(ActiveLink)

5.项目目录

从这一步开始就是实际创建项目写代码的过程了,由于是公司项目,这里全部用模拟数据,但是上文提到的项目需求都会从零开始一项项实现。

安装

1.首先新建目录 ssr 在ssr目录下执行
cnpm install --save next react react-dom // 需要设置 npm镜像

2.执行完命令后目录下出现文件夹node_module 和文件package.json 
ssr
    -node_modules
    -package.json

package.json 文件内容如下
{
  "dependencies": {
    "next": "^8.1.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

3.添加脚本到package.json文件. 我们可以在这里自定义npm脚本命令
{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "^8.1.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

4.在ssr目录下新建文件夹 pages | static | components ... ,然后在pages下新建文件 index.js,文件内容如下

export default () => <div>Welcome to next.js!</div>

最终新目录结构如下 [暂时没提到的文件和目录后续会讲到]
ssr
    -node_modules
    -package.json
    -components
    -static
        -imgs
            -logo.png
        -fonts
            -example.ttf
        -utils
            -index.js
    -pages
        -index.js
        -about.js
    -.gitignore
    -README.md
    -next.config.js
    -server.js

5.运行 npm run dev 命令并打开 http://localhost:3000
  执行npm run start 之前需要先执行 npm run build 不然会报错

用浏览器调试工具打开查看源代码,可以看到 根容器_next 下有div元素渲染进去了,数据很多时就会有丰富的利于搜索引擎爬取html代码。

这里跟SPA单页面应用对比更好理解,SPA应用只有一个挂载组件的root根容器。容器里面不会看到其他丰富的html代码

6.项目需求实现

项目是为了利于SEO做的服务端渲染,说到SEO,需要设置html文档里的head头部信息。这里有三个非常关键的信息,kywords | description | title 分别表示当前网页的关键字,描述,网页标题。搜索引擎会根据这几个标签里的内容爬取网页的关键信息,然后用户在搜索的时候根据这些关键字匹配程度做搜索结果页面展现。(当然展现算法远远不止参考这些信息,页面标签的语意化,关键字密度,外链,内链,访问量,用户停留时间...)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="keywords" content="Winyh | Next.js | React.js | Node.js | ...">
    <meta name="description" content="这是一个跟next.js服务端相关的页面">
    <title>基于React.js 技术栈的服务端渲染框架Next.js 实战记录</title>
</head>
<body>
    
</body>
</html>

需求一:SEO信息可配置

这个实现了,搜索引擎搜录也算是简单实现了。要实现搜索引擎友好其实有上述很多方面的可以优化。

  • 设置一个内置组件来装载到页面中,将文件命名字为HeadSeo.js
// components/Common/HeadSeo.js 文件里代码如下
import Head from 'next/head'

export default () =>
    <Head>
        <meta charSet="UTF-8"> // 注意这里的charSet大写,不然React jsx语法 会报错
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="keywords" content="Winyh | Next.js | React.js | Node.js | ...">
        <meta name="description" content="这是一个跟next.js服务端相关的页面">
        <title>基于React.js 技术栈的服务端渲染框架Next.js 实战记录</title>
    </Head>
    
    
// pages/index.js 文件里代码如下
import Layout from "../components/Layouts/PcLayout"

export default () => 
    <Layout>
        <div>Welcome to next.js!</div>
    </Layout>

相应目录结构为
ssr
    -node_modules
    -package.json
    -components
        -Common   // 公共组件
            -HeadSeo.js
        -Layouts  // 布局文件
            -PcLayout.js
            -MLayout.js
    -static // 静态资源
        -imgs
            -logo.png
        -fonts
            -example.ttf
        -utils
            -index.js
    -pages 
        -index.js
        -about.js
    -.gitignore
    -README.md
    -next.config.js // 配置文件
    -server.js // 服务端脚本

打开localhost:3000 可以看到相关 head 头部seo信息已经渲染出来了。如果需要在服务端动态渲染数据,可以在pages目录下的文件请求后台数据,通过Props传值的方式渲染到HeadSeo文件中,这里暂时值说下方法,后续写实际代码实现。

需求二:路由美化

通过自定义服务端路由实现路由自定义美化功能。例如在武汉(wuhan)站点时,访问首页需要路由是这样的

城市 首页 关于我们
武汉 /wuhan/index /wuhan/about
上海 /shanghai/index /shanghai/about
南京 /nanjing/index /nanjing/about

创建服务端脚本文件 server.js,服务端用Express做服务器

// 安装 express 服务端代理工具也一起安装了 http-proxy-middleware
cnpm i express http-proxy-middleware --save
const express = require('express')
const next = require('next')
const server = express()

const port = parseInt(process.env.PORT, 10) || 3000 // 设置监听端口
const dev = process.env.NODE_ENV !== 'production' // 判断当前开发环境
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
  .then(() => {

    server.get('/:city', (req, res) => {
        const actualPage = '/index';
        const queryParams = { city: req.params.city}; // 通过 req 请求对象访问到路径上传过来的参数
        console.log(req.params)
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/index', (req, res) => {
        const actualPage = '/index';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/about', (req, res) => {
        const actualPage = '/about';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/posts/:id', (req, res) => {
      return app.render(req, res, '/posts', { id: req.params.id })
    })

    server.get('*', (req, res) => {
      return handle(req, res)
    })

    server.listen(port, (err) => {
      if (err) throw err
      console.log(`> Ready on http://localhost:${port}`)
    })
})

修改package.json 文件的脚本如下:然后运行命令 npm run ssrdev 打开3000端口,至此可以通过美化后的路由访问到页面了 localhost:3000/wuhan/index
localhost:3000/wuhan/about

{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "ssrdev": "node server.js", // 可以通过nodemon 来代替node,这样server.js 文件修改后不需要重新运行脚本
    "ssrstart": "npm run build && NODE_ENV=production node server.js", // 需要先执行 npm run build
    "export": "npm run build && next export"
  },
  "dependencies": {
    "express": "^4.17.0",
    "http-proxy-middleware": "^0.19.1",
    "next": "^8.1.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

需求三:根据不同城市显示不同数据

根据用户地理位置展示对应城市站点首页,获取不同城市的数据。这里开始就数据模拟和服务端数据获取了。 本次项目实践会尝试两种数据模拟的方式

  • json-server

  • mock.js (这种方式更简单,后续加上)

首先安装开源的 json-server具体使用方式参照github

cnpm install -g json-server

在ssr目录下新建mock文件下,然后在mock下新建 data.json,文件数据如下

{
    "index":{
        "city":"wuhan",
        "id":1,
        "theme":"默认站点"
    },
    "posts": [
      { "id": 1, "title": "json-server", "author": "typicode" }
    ],
    "comments": [
      { "id": 1, "body": "some comment", "postId": 1 }
    ],
    "profile": { "name": "typicode" },
    "seo":{
        "title":"基于React.js 技术栈的服务端渲染框架Next.js 实战记录",
        "keywords":"Winyh, Next.js, React.js, Node.js",
        "description":"Next.js服务端渲染数据请求模拟页面测试"
    }
}

在当前目录新建路由规则文件 routes.json 为模拟api添加/api/前缀。文件类型如下

{
    "/api/*": "/$1"
}

修改package.json 文件,添加数据模拟命令行脚本

{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "ssrdev": "nodemon server.js",
    "ssrstart": "npm run build && NODE_ENV=production nodemon server.js",
    "export": "npm run build && next export",
  + "mock": "cd ./mock && json-server --watch data.json --routes routes.json --port 4000"
  },
  "dependencies": {
    "express": "^4.17.0",
    "http-proxy-middleware": "^0.19.1",
    "next": "^8.1.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

运行命令 npm run mock 启动模拟数据服务器即可访问数据

localhost:4000/api/seo

先安装ajax请求工具

cnpm install isomorphic-unfetch --save

更新pages/index.js文件内容为

import React, { Component } from 'react';
import Layout from "../components/Layouts/PcLayout"
import 'isomorphic-unfetch'

class index extends Component {
    constructor(props) {
		super(props);
		this.state = {
			city:"武汉"
		};
    }

    static async getInitialProps({ req }) {
        const res = await fetch('http://localhost:4000/api/seo')
        const seo = await res.json()
        return { seo }
    }

    componentDidMount(){
        console.log(this.props)
    }
    
    render(){
        const { seo } = this.props;
        return (
            <Layout seo={seo}>
                <div>Welcome to next.js!</div>
                <div>{seo.title}</div>
            </Layout>
        )
    }
}
export default index

/Layouts/Pclayout.js 文件内容修改为

import HeadSeo from '../Common/HeadSeo'

export default ({ children, seo }) => (
  <div id="pc-container">
    <HeadSeo seo={ seo }></HeadSeo>
    { children }
  </div>
)

/components/Common/HeadSeo.js 文件内容修改为

import Head from 'next/head'

export default ({seo}) =>
    <Head>
        <meta charSet="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="keywords" content={seo.keywords} />
        <meta name="description" content={seo.description} />
        <title>{seo.title}</title>
    </Head>

至此页面上就可以看到打印的数据和展示的数据了

下一步根据用户地理位置确定显示的页面城市,解决方案步骤如下【暂时只说方法,稍后完善代码】

  • 先请求百度开放api根据ip定位获取到城市名称和唯一码
  • 通过唯一码作为参数请求唯一码对应城市数据
  • 路由美化后返回对应城市页面数据到前台展示

需求四:PC端/M端渲染不同页面

基本原理:根据请求头user-agnet 判断终端,然后渲染不同的组件 在static文件夹下新建 js文件夹,在 js文件夹下新建 util.js工具类模块,代码如下

// 根据 user-agent 请求头判断是否移动端
const util = {

    isMobile: (req) => {
        const deviceAgent = req.headers["user-agent"];
        return /Android|webOS|iPhone|iPod|BlackBerry/i.test(deviceAgent)
    },

};

module.exports  = util

在pages文件夹下新建mindex.js文件,作为移动端渲染的首页

import React, { Component } from 'react';
import Layout from "../components/Layouts/MLayout"
import 'isomorphic-unfetch'

class index extends Component {
    constructor(props) {
		super(props);
		this.state = {
			city:"武汉"
		};
    }

    static async getInitialProps({ req }) {
        const res = await fetch('http://localhost:4000/api/seo')
        const seo = await res.json()
        return { seo }
    }

    componentDidMount(){
        console.log(this.props)
    }
    
    render(){
        const { seo } = this.props;
        return (
            <Layout seo={seo}>
                <div>Welcome to next.js!</div>
                <div>移动端页面</div>
            </Layout>
        )
    }
}
export default index
    

修改server.js文件内容如下

const express = require('express')
const next = require('next')
const server = express()

const util = require("./static/js/util");

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
  .then(() => {

    server.get('/:city', (req, res) => {
        const actualPage = util.isMobile(req) ? '/mindex' : '/index'; // 这里是关键
        const queryParams = { city: req.params.city};
        console.log(req.params.city, actualPage)
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/index', (req, res) => {
        const actualPage = '/index';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/about', (req, res) => {
        const actualPage = '/about';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/posts/:id', (req, res) => {
      return app.render(req, res, '/posts', { id: req.params.id })
    })

    server.get('*', (req, res) => {
      return handle(req, res)
    })

    server.listen(port, (err) => {
      if (err) throw err
      console.log(`> Ready on http://localhost:${port}`)
    })
})

用浏览器打开调试面板的移动端模式就可以自动渲染移动端页面了

需求五:SEO信息可配置

其实基本已经实现了,前台通过不同的页面路由页面参数请求后端子页面首屏初始化数据接口,请求在服务端渲染时getInitialProps方法里完成。 例如:/wuhan/index 可以根据 index 作为参数获取后台配置给index页面的seo信息 /wuhan/posts 可以根据 posts 作为参数获取后台配置给posts页面的seo信息

需求六:支持开发/正式环境切换

服务端server.js可通过如下方法实现

const dev = process.env.NODE_ENV !== 'production';

客户端可以通过配置文件next.config.js实现

/*
* @Author: winyh
* @Date:   2018-11-01 17:17:10
 * @Last Modified by: winyh
 * @Last Modified time: 2018-12-14 11:01:35
*/
const withPlugins = require('next-compose-plugins')
const path = require("path");
const sass = require('@zeit/next-sass')

const isDev = process.env.NODE_ENV  !== 'production'

console.log({
    isDev
})

// api主机 
const host = isDev ? 'http://localhost:4000':'http://localhost:4001'

const {
    PHASE_PRODUCTION_BUILD,
    PHASE_PRODUCTION_SERVER,
    PHASE_DEVELOPMENT_SERVER,
    PHASE_EXPORT,
} = require('next/constants');

const nextConfiguration = {
    //useFileSystemPublicRoutes: false, 
    //distDir: 'build',
    testConfig:"www",
    webpack: (config, options) => {

        config.module.rules.push({
            test: /\.(jpe?g|png|svg|gif|ico|webp)$/,
            use: [
              {
                loader: "url-loader",
                options: {
                  limit: 20000,
                  publicPath: `https://www.winyh.com/`,
                  outputPath: `/winyh/static/images/`,
                  name: "[name].[ext]"
                }
              }
            ]
        })
    
        return config;
    },

    serverRuntimeConfig: { // Will only be available on the server side
        mySecret: 'secret'
    },
    publicRuntimeConfig: { // Will be available on both server and client
        mySecret: 'client',
        host: host,
        akSecert:'GYxVZ027Mo0yFUahvF3XvZHZzAYog9Zo' // 百度地图ak 密钥
    }
}

module.exports = withPlugins([
    
    [sass, {
        cssModules: false,
        cssLoaderOptions: {
          localIdentName: '[path]___[local]___[hash:base64:5]',
        },
        [PHASE_PRODUCTION_BUILD]: {
          cssLoaderOptions: {
            localIdentName: '[hash:base64:8]',
          },
        },
    }]

], nextConfiguration)

pages/index.js通过配置文件修改api主机地址码,代码如下(fetch请求后面会封装成公用方法)

import React, { Component } from 'react';
import Layout from "../components/Layouts/PcLayout"
import 'isomorphic-unfetch'
import getConfig from 'next/config' // next自带的配置方法
const { publicRuntimeConfig } = getConfig() // 取到配置参数

class index extends Component {
    constructor(props) {
		super(props);
		this.state = {
			city:"武汉"
		};
    }

    static async getInitialProps({ req }) {
        const res = await fetch(publicRuntimeConfig.host + '/api/seo') // 从配置文件里获取
        const seo = await res.json()
        return { seo }
    }

    componentDidMount(){
        console.log(this.props)
    }
    
    render(){
        const { seo } = this.props;
        return (
            <Layout seo={seo}>
                <div>Welcome to next.js!</div>
                <div>{seo.title}</div>
            </Layout>
        )
    }
}
export default index

需求七:微信授权文件部署

在网页端做微信支付或者授权时需要通过微信服务器的安全校验,微信服务器下发一个密钥文件*.txt,一般放在项目根目录,需要支持访问,例如:localhost:3000/MP_verify_HjspU6daVebgWsvauH.txt

  • 将根目录设置为可以访问 server.use(express.static(__dirname)),这个太不安全了,根目录所有文件都暴露了

  • 在server.js文件里加上处理.txt文件的方法

server.get('*', (req, res) => {
const express = require('express')
const next = require('next')
const server = express()

const util = require("./static/js/util");

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
  .then(() => {

    server.get('/:city', (req, res) => {
        const txt = req.url; // 获取请求路径
        // 这里需要做一个请求拦截判断
        if(txt.indexOf(".txt") > 0){
          res.sendFile(__dirname + `/${txt}`);
        }
        const actualPage = util.isMobile(req) ? '/mindex' : '/index';
        const queryParams = { city: req.params.city};
        console.log(req.params.city, actualPage)
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/index', (req, res) => {
        const actualPage = '/index';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/about', (req, res) => {
        const actualPage = '/about';
        const queryParams = { city: req.params.city};
        app.render(req, res, actualPage, queryParams);
    });

    server.get('/:city/posts/:id', (req, res) => {
      return app.render(req, res, '/posts', { id: req.params.id })
    })

    // server.get('*', (req, res) => {
    //   return handle(req, res)
    // })

    server.get('*', (req, res) => {
      const txt = req.url; // 获取请求路径
      if(txt.indexOf(".txt") > 0){
        res.sendFile(__dirname + `/${txt}`);
      } else {
        return handle(req, res)
      }
    })

    server.listen(port, (err) => {
      if (err) throw err
      console.log(`> Ready on http://localhost:${port}`)
    })
})

在根目录创建一个文件MP_verify_HjspU6daVebgWsvauH.txt测试下,浏览器访问结果

需求八:部分Http请求代理处理

在server.js 文件里添加如下代码,当访问/proxy/*路由时自动匹配代理到http://api.test-proxy.com

const proxyApi = "http://api.test-proxy.com"
server.use('/proxy/*', proxy({ 
  target: proxyApi, 
  changeOrigin: true 
}));

需求九:类重定向处理

当访问localhost:3000/winyh路由时需要显示 ww.redirect.com/about?type_… 页面上的内容。 先安装工具 cnpm i urllib --save

// 修改server.js 文件代码

server.get('/winyh', async (req, res) => {
  const agent  = req.header("User-Agent");
  const result = await urllib.request(
    'http://ww.redirect.com/about?type_id=3', 
    {   
      method: 'GET',
      headers: {
        'User-Agent': agent
      },
    })
    res.header("Content-Type", "text/html;charset=utf-8");
    
    res.send(result.data);// 需要获取result.data 不然显示到前台的数据时二进制 45 59 55 
})

需求十:本地数据模拟

上述文章有提到,已实现

需求十一:项目部署方式

主要是编写Dockerfile文件,本地VsCode可以启动容器调试,后续演示

FROM mhart/alpine-node

WORKDIR /app
COPY . .

RUN yarn install
RUN yarn build

EXPOSE 80

CMD ["node", "server.js"]

最后总结:

  • 还有很多细节可以完善,希望能帮到大家,也希望评论交流,相互学习。
  • 后面会把数据请求改成Graphql.js方式。我的这篇文章里 GraphQL.js 与服务端交互的新方式写了一个GraphQL入门演示

玻璃钢生产厂家定西玻璃钢设备外壳威海玻璃钢外壳制作许昌玻璃钢坐凳厂家十堰玻璃钢座椅厂家湘潭玻璃钢机械外壳厂湖北玻璃钢人物雕塑批发武汉商场美陈制作佛山玻璃钢天花吊顶定做山东玻璃钢机械外壳厂邯郸玻璃钢摆件制作宁德玻璃钢天花吊顶鹤壁玻璃钢花坛定制遵义玻璃钢树池坐凳多少钱德州玻璃钢景观雕塑价格邯郸玻璃钢动物雕塑定做海东玻璃钢天花吊顶厂家鞍山玻璃钢花瓶湖州玻璃钢人物雕塑厂家直销铜仁玻璃钢天花吊顶批发白山玻璃钢花盆生产厂家榆林玻璃钢树池坐凳厂家直销云南玻璃钢设备外壳制作保定玻璃钢花瓶价格邢台玻璃钢茶几定做邵阳玻璃钢摆件加工南昌玻璃钢装饰造型价格百色玻璃钢浮雕制造廊坊玻璃钢种植池批发芜湖玻璃钢浮雕多少钱阳江玻璃钢沙发公司香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化