基于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入门演示