Koa
或者其它Node.js
服务器进行集成mkdir zhufengnextjs
cd zhufengnextjs
npm init -y
npm install react react-dom next redux react-redux --save
npm install axios express body-parser cors express-session connect-mongo mongoose koa koa-router --save
package.json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
_app.js
和 _document.js
pages
组件代码自动分割.next
目录function Home() {
return <div>Home</div>;
}
export default Home;
pages\user.js
function User() {
return <div>User</div>;
}
export default User;
npm run dev
curl http://localhost:3000/
styled-jsx
来生成独立作用域的 CSS
css
App
组件是每个页面的根组件,页面切换时 App 不会销毁,但是里面的页面组件会销毁,因此可以利用这个来设置全局属性和样式pages_app.js
import App from "next/app";
import Link from "next/link";
import _appStyle from "./_app.module.css";
import "../styles/global.css";
class LayoutApp extends App {
render() {
let { Component } = this.props;
return (
<div>
<style jsx>
{`
li {
display: inline-block;
margin-left: 10px;
line-height: 31px;
}
`}
</style>
<header>
<img src="/images/logo.png" className={_appStyle.logo} />
<ul>
<li>
<Link href="/">首页</Link>
</li>
<li>
<Link href="/user">用户管理</Link>
</li>
<li>
<Link href="/profile">个人中心</Link>
</li>
</ul>
</header>
<Component />
<footer style={{ textAlign: "center" }}>@copyright 珠峰架构</footer>
</div>
);
}
}
export default LayoutApp;
pages_app.module.css
.logo {
width: 120px;
height: 31px;
float: left;
}
styles\global.css
html,
body {
padding: 0;
margin: 0;
}
pages\index.js
function Home() {
return <div>Home</div>;
}
export default Home;
pages\user.js
import Link from "next/link";
function User() {
return (
<div>
<p>User</p>
<Link href="/">首页</Link>
</div>
);
}
export default User;
pages\profile.js
import router from "next/router";
function Profile() {
return (
<div>
<p>Profile</p>
<button onClick={() => router.back()}>返回</button>
</div>
);
}
export default Profile;
getInitialProps
获取数据pages_app.js
import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
class LayoutApp extends App {
+ static async getInitialProps({ Component,ctx }) {
+ let pageProps = {};
+ if (Component.getInitialProps)
+ pageProps = await Component.getInitialProps(ctx);
+ return { pageProps };
+ }
render() {
+ let { Component, pageProps } = this.props;
return (
<div>
<style jsx>
{
`li{
display:inline-block;
margin-left:10px;
line-height:31px;
}`
}
</style>
<header>
<img src="/images/logo.png" className={_appStyle.logo} />
<ul>
<li><Link href="/">首页</Link></li>
+ <li><Link href="/user/list" >用户管理</Link></li>
<li><Link href="/profile">个人中心</Link></li>
</ul>
</header>
+ <Component {...pageProps} />
<footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</footer>
</div>
)
}
}
export default LayoutApp;
pages\user\index.js
import Link from "next/link";
function UserLayout(props) {
return (
<div>
<div>
<ul>
<li>
<Link href="/user/list">
<a>用户列表</a>
</Link>
</li>
<li>
<Link href="/user/add">
<a>添加用户</a>
</Link>
</li>
</ul>
<div>{props.children}</div>
</div>
</div>
);
}
export default UserLayout;
pages\user\list.js
import Link from "next/link";
import UserLayout from "./";
function UseList(props) {
return (
<UserLayout>
<ul>
{props.list.map((user) => (
<li key={user.id}>
<Link href={`/user/detail/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
</UserLayout>
);
}
UseList.getInitialProps = async () => {
let list = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
];
return { list };
};
export default UseList;
pages\user\add.js
import UserLayout from "./";
import React from "react";
function UserAdd() {
let nameRef = React.useRef();
let passwordRef = React.useRef();
let handleSubmit = (event) => {
event.preventDefault();
let user = {
name: nameRef.current.value,
password: passwordRef.current.value,
};
};
return (
<UserLayout>
<form onSubmit={handleSubmit}>
用户名:
<input ref={nameRef} />
密码:
<input ref={passwordRef} />
<button type="submit">添加</button>
</form>
</UserLayout>
);
}
export default UserAdd;
pages\user\detail[id].js
import React from "react";
import UserLayout from "../";
function UserDetail(props) {
return (
<UserLayout>
<p>ID:{props.user.id}</p>
</UserLayout>
);
}
UserDetail.getInitialProps = async (ctx) => {
return { user: { id: ctx.query.id } };
};
export default UserDetail;
getInitialProps
将会把数据序列化,就像JSON.stringify
getInitialProps
返回的是一个普通 JS
对象,而不是 Date, Map 或 Set 类型getInitialProps
只会加载在服务端。只有当路由跳转(Link 组件跳转或 API 方法跳转)时,客户端才会执行getInitialProps
getInitialProps
将不能使用在子组件中。只能使用在pages
页面中服务器端
LayoutApp getInitialProps 获取LayoutApp初始属性
UseList getInitialProps 调用页面组件的getInitialProps方法
LayoutApp constructor 根据属性创建LayoutAPp的实例
LayoutApp render 调用此实例的render方法,返回react元素
UseList constructor 创建子组件
UseList render 渲染子组件
客户端
LayoutApp constructor
LayoutApp render
UseList constructor
UseList render
客户端
LayoutApp getInitialProps
UseList getInitialProps
LayoutApp render
UseList constructor
UseList render
pages_app.js
import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
class LayoutApp extends App {
+ constructor(props) {
+ super(props)
+ console.log('LayoutApp constructor');
+ }
static async getInitialProps({ Component, ctx }) {
+ console.log('LayoutApp getInitialProps');
let pageProps = {};
if (Component.getInitialProps)
pageProps = await Component.getInitialProps(ctx);
return { pageProps };
}
render() {
console.log('LayoutApp render');
let { Component, pageProps } = this.props;
return (
<div>
<style jsx>
{
`li{
display:inline-block;
margin-left:10px;
line-height:31px;
}`
}
</style>
<header>
<img src="/images/logo.png" className={_appStyle.logo} />
<ul>
<li><Link href="/">首页</Link></li>
<li><Link href="/user/list" >用户管理</Link></li>
<li><Link href="/profile">个人中心</Link></li>
</ul>
</header>
<Component {...pageProps} />
<footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</footer>
</div>
)
}
}
export default LayoutApp;
pages\user\list.js
import React from 'react';
import Link from 'next/link';
import UserLayout from './';
+import request from '../../utils/request';
class UseList extends React.Component {
+ constructor(props) {
+ super(props);
+ console.log('UseList constructor');
+ }
render() {
console.log('UseList render');
return (
<UserLayout>
<ul>
{
this.props.list.map((user) => (
<li key={user.id}>
<Link href={`/user/detail/${user.id}`}>{user.name}</Link>
</li>
))
}
</ul>
</UserLayout>
)
}
}
UseList.getInitialProps = async () => {
+ console.log('UseList getInitialProps');
let response = await request({ url: '/api/users', method: 'GET' }).then(res => res.data);
return { list: response.data };
}
export default UseList;
pages\user\add.js
import UserLayout from './';
import React from 'react';
+import request from '../../utils/request';
import router from 'next/router'
function UserAdd() {
let nameRef = React.useRef();
let passwordRef = React.useRef();
let handleSubmit = async (event) => {
event.preventDefault();
const user = { name: nameRef.current.value, password: passwordRef.current.value };
+ let response = await request.post('/api/register', user).then(res => res.data);
+ if (response.success) {
+ router.push('/user/list');
+ } else {
+ alert('添加用户失败');
+ }
}
return (
<UserLayout>
<form onSubmit={handleSubmit}>
用户名:<input ref={nameRef} />
密码:<input ref={passwordRef} />
<button type="submit">添加</button>
</form>
</UserLayout>
)
}
export default UserAdd;
pages\user\detail[id].js
import React from 'react';
import UserLayout from '../';
+import request from '../../../utils/request';
function UserDetail(props) {
return (
<UserLayout>
<p>ID:{props.user.id}</p>
<p>NAME:{props.user.name}</p>
</UserLayout>
)
}
UserDetail.getInitialProps = async (ctx) => {
+ let response = await request({ url: `/api/users/${ctx.query.id}`, method: 'GET' }).then(res => res.data);
+ return { user: response.data };
}
export default UserDetail;
utils\request.js
import axios from "axios";
axios.defaults.withCredentials = true;
const instance = axios.create({
baseURL: "http://localhost:5000",
});
export default instance;
pages\user\detail[id].js
import React from 'react';
import UserLayout from '../';
import request from '../../../utils/request';
+import dynamic from 'next/dynamic';
//import UserInfo from '@/components/UserInfo';
+const DynamicUserInfo = dynamic(() => import('@/components/UserInfo'));
function UserDetail(props) {
const [show, setShow] = React.useState(false);
return (
<UserLayout>
<p>ID:{props.user && props.user.id}</p>
+ <button onClick={() => setShow(!show)}>显示/隐藏</button>
+ {
+ show && props.user && <DynamicUserInfo user={props.user} />
+ }
+ </UserLayout>
)
}
UserDetail.getInitialProps = async (ctx) => {
let response = await request({ url: `/api/users/${ctx.query.id}`, method: 'GET' }).then(res => res.data);
return { user: response.data };
}
export default UserDetail;
UserInfo.js
import React, { useState } from "react";
function UserInfo(props) {
const [createdAt, setCreatedAt] = useState(props.user.createdAt);
async function changeFormat() {
const moment = await import("moment");
setCreatedAt(moment.default(createdAt).fromNow());
}
return (
<>
<p>NAME:{props.user.name}</p>
<p>创建时间:{createdAt}</p>
<button onClick={changeFormat}>切换为相对时间</button>
</>
);
}
export default UserInfo;
jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["/*"]
}
}
}
pages_app.js
import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
+import { Provider } from 'react-redux';
+import request from '../utils/request';
+import createStore from '../store';
+import * as types from '../store/action-types';
+function getStore(initialState) {
+ if (typeof window === 'undefined') {
+ return createStore(initialState);//如果是服务器端,每次都返回新的仓库
+ } else {
+ if (!window._REDUX_STORE_) {
+ window._REDUX_STORE_ = createStore(initialState);
+ }
+ return window._REDUX_STORE_;
+ }
+}
class LayoutApp extends App {
constructor(props) {
super(props)
+ this.store = getStore(props.initialState);
console.log('LayoutApp constructor');
}
static async getInitialProps({ Component, ctx }) {
+ console.log('LayoutApp getInitialProps');
+ let store = getStore();//1.后台创建新仓库 5.每次切换路由都会执行此方法获取老仓库
+ if (typeof window == 'undefined') {//2.后台获取用户信息
+ let options = { url: '/api/validate' };
+ if (ctx.req && ctx.req.headers.cookie) {
+ options.headers = options.headers || {};
+ options.headers.cookie = ctx.req.headers.cookie;
+ }
+ let response = await request(options).then(res => res.data);
+ if (response.success) {
+ store.dispatch({ type: types.SET_USER_INFO, payload: response.data });
+ }
+ }
+ let pageProps = {};
+ if (Component.getInitialProps)
+ pageProps = await Component.getInitialProps(ctx);
+ const props = { pageProps };
+ if (typeof window == 'undefined') {//后台获取用赋值状态
+ props.initialState = store.getState();
+ }
+ return props;
}
render() {
console.log('LayoutApp render');
let state = this.store.getState();
let { Component, pageProps } = this.props;
return (
+ <Provider store={this.store}>
<style jsx>
{
`li{
display:inline-block;
margin-left:10px;
line-height:31px;
}`
}
</style>
<header>
<img src="/images/logo.png" className={_appStyle.logo} />
<ul>
<li><Link href="/">首页</Link></li>
<li><Link href="/user/list" >用户管理</Link></li>
<li><Link href="/profile">个人中心</Link></li>
<li>
{
+ state.currentUser ? <span>{state.currentUser.name}</span> : <Link href="/login">登录</Link>
}
</li>
</ul>
</header>
<Component {...pageProps} />
<footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</footer>
+ </Provider>
)
}
}
export default LayoutApp;
store\action-types.js
export const SET_USER_INFO = 'SET_USER_INFO';//设置用户信息
store\reducer.js
import * as types from './action-types';
let initState = {
currentUser: null
}
const reducer = (state = initState, action) => {
switch (action.type) {
case types.SET_USER_INFO:
return { currentUser: action.payload }
default:
return state;
}
}
export default reducer;
store\index.js
import { createStore } from 'redux';
import reducer from './reducer';
export default function (initialState) {
return createStore(reducer, initialState);
}
pages\login.js
import React from 'react';
import request from '@/utils/request';
import router from 'next/router';
import { connect } from 'react-redux';
import * as types from '@/store/action-types';
function Login(props) {
let nameRef = React.useRef();
let passwordRef = React.useRef();
let handleSubmit = async (event) => {
event.preventDefault();
let user = { name: nameRef.current.value, password: passwordRef.current.value };
let response = await request.post('/api/login', user).then(res => res.data);
if (response.success) {
props.dispatch({ type: types.SET_USER_INFO, payload: response.data });
router.push('/');
} else {
alert('登录失败');
}
}
return (
<form onSubmit={handleSubmit}>
用户名:<input ref={nameRef} />
密码:<input ref={passwordRef} />
<button type="submit">登录</button>
</form>
)
}
export default connect(state => state)(Login);
事件 | 触发时机 |
---|---|
routeChangeStart(url) | 路由开始切换时触发 |
routeChangeComplete(url) | 完成路由切换时触发 |
routeChangeError(err, url) | 路由切换报错时触发 |
beforeHistoryChange(url) | 浏览器 history 模式开始切换时触发 |
hashChangeStart(url) | 开始切换 hash 值但是没有切换页面路由时触发 |
hashChangeComplete(url) | 完成切换 hash 值但是没有切换页面路由时触发 |
pages_app.js
import App from 'next/app';
import Link from 'next/link';
import _appStyle from './_app.module.css';
import '../styles/global.css';
import { Provider } from 'react-redux';
import request from '../utils/request';
import createStore from '../store';
import * as types from '../store/action-types';
+import router from 'next/router';
function getStore(initialState) {
if (typeof window === 'undefined') {
return createStore(initialState);//如果是服务器端,每次都返回新的仓库
} else {
if (!window._REDUX_STORE_) {
window._REDUX_STORE_ = createStore(initialState);
}
return window._REDUX_STORE_;
}
}
class LayoutApp extends App {
constructor(props) {
super(props)
+ this.state = { loading: false }
this.store = getStore(props.initialState);
console.log('LayoutApp constructor');
}
static async getInitialProps({ Component, ctx }) {
console.log('LayoutApp getInitialProps');
let store = getStore();//1.后台创建新仓库 5.每次切换路由都会执行此方法获取老仓库
if (typeof window == 'undefined') {//2.后台获取用户信息
let options = { url: '/api/validate' };
if (ctx.req && ctx.req.headers.cookie) {
options.headers = options.headers || {};
options.headers.cookie = ctx.req.headers.cookie;
}
let response = await request(options).then(res => res.data);
if (response.success) {
store.dispatch({ type: types.SET_USER_INFO, payload: response.data });
}
}
let pageProps = {};
if (Component.getInitialProps)
pageProps = await Component.getInitialProps(ctx);
const props = { pageProps };
if (typeof window == 'undefined') {//后台获取用赋值状态
props.initialState = store.getState();
}
return props;
}
+ componentDidMount() {
+ this.routeChangeStart = (url) => {
+ this.setState({ loading: true });
+ };
+ router.events.on('routeChangeStart', this.routeChangeStart);
+ this.routeChangeComplete = (url) => {
+ this.setState({ loading: false });
+ };
+ router.events.on('routeChangeComplete', this.routeChangeComplete);
+ }
+ componentWillUnmount() {
+ router.events.off('routeChangeStart', this.routeChangeStart)
+ router.events.off('routeChangeStart', this.routeChangeComplete)
+ }
render() {
console.log('LayoutApp render');
let state = this.store.getState();
let { Component, pageProps } = this.props;
return (
<Provider store={this.store}>
<style jsx>
{
`li{
display:inline-block;
margin-left:10px;
line-height:31px;
}`
}
</style>
<header>
<img src="/images/logo.png" className={_appStyle.logo} />
<ul>
<li><Link href="/">首页</Link></li>
<li><Link href="/user/list" >用户管理</Link></li>
<li><Link href="/profile">个人中心</Link></li>
<li>
{
state.currentUser ? <span>{state.currentUser.name}</span> : <Link href="/login">登录</Link>
}
</li>
</ul>
</header>
{
+ this.state.loading ? <div>切换中......</div> : <Component {...pageProps} />
}
<footer style={{ textAlign: 'center' }} >@copyright 珠峰架构</footer>
</Provider>
)
}
}
export default LayoutApp;
pages\profile.js
import router from 'next/router';
import { connect } from 'react-redux';
import request from '../utils/request';
function Profile(props) {
let { currentUser } = props;
return (
<div>
<p>当前登录用户:{currentUser.name}</p>
<button onClick={() => router.back()}>返回</button>
</div>
)
}
Profile.getInitialProps = async function (ctx) {
let options = { url: '/api/validate' };
if (ctx.req && ctx.req.headers.cookie) {
options.headers = options.headers || {};
options.headers.cookie = ctx.req.headers.cookie;
}
let response = await request(options).then(res=>res.data);
if (response.success) {
return {currentUser:response.data};
} else {
if (ctx.req) {
ctx.res.writeHead(303, { Location: '/login' })
ctx.res.end()
} else {
router.push('/login');
}
return {};
}
}
const WrappedProfile = connect(
state => state
)(Profile);
export default WrappedProfile;
pages_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
class CustomDocument extends Document {
static async getInitialProps(ctx) {
const props = await Document.getInitialProps(ctx);
return { ...props };
}
render() {
return (
<Html>
<Head>
<style>
{
`
*{
padding:0;
margin:0;
}
`
}
</style>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default CustomDocument;
pages\index.js
import Head from 'next/head'
export default function (props) {
return (
<div>
<Head>
<title>首页</title>
<meta name="description" content="这是首页" />
</Head>
<p>Home</p>
</div>
)
}
pages\user\list.js
import React from 'react';
import Link from 'next/link';
import UserLayout from './';
import request from '@/utils/request';
class UseList extends React.Component {
constructor(props) {
super(props);
console.log('UseList constructor');
}
render() {
console.log('UseList render');
return (
<UserLayout>
<ul>
{
this.props.list.map((user) => (
<li key={user.id}>
<Link href={`/user/detail/${user.id}`}>{user.name}</Link>
</li>
))
}
</ul>
</UserLayout>
)
}
}
-UseList.getInitialProps = async () => {
- console.log('UseList getInitialProps');
- let response = await request({ url: '/api/users', method: 'GET' }).then(res => res.data);
- return { list: response.data };
-}
//每个请求都会调用
+export async function getServerSideProps() {
+ const res = await request.get('http://localhost:5000/api/users')
+ return {
+ props: {
+ list: res.data.data
+ },
+ }
+}
export default UseList;
pages\user\list.js
import React from 'react';
import Link from 'next/link';
import UserLayout from './';
import request from '@/utils/request';
class UseList extends React.Component {
constructor(props) {
super(props);
console.log('UseList constructor');
}
render() {
console.log('UseList render');
return (
<UserLayout>
<ul>
{
this.props.list.map((user) => (
<li key={user.id}>
<Link href={`/user/detail/${user.id}`}>{user.name}</Link>
</li>
))
}
</ul>
</UserLayout>
)
}
}
/* UseList.getInitialProps = async () => {
console.log('UseList getInitialProps');
let response = await request({ url: '/api/users', method: 'GET' }).then(res => res.data);
return { list: response.data };
} */
//每个请求都会调用
export async function getServerSideProps() {
const res = await request.get('http://localhost:5000/api/users')
return {
props: {
list: res.data.data
},
}
}
// 这个函数在编译阶段被调用
export async function getStaticProps() {
const res = await request.get('http://localhost:5000/api/users');
return {
props: {
list: res.data.data
},
}
}
export default UseList;
pages\user\detail[id].js
import React from 'react';
import UserLayout from '../';
import request from '@/utils/request';
import dynamic from 'next/dynamic';
//import UserInfo from '@/components/UserInfo';
const DynamicUserInfo = dynamic(() => import('@/components/UserInfo'));
function UserDetail(props) {
const [show, setShow] = React.useState(false);
return (
<UserLayout>
<p>ID:{props.user && props.user.id}</p>
<button onClick={() => setShow(!show)}>显示/隐藏</button>
{
show && props.user && <DynamicUserInfo user={props.user} />
}
</UserLayout>
)
}
UserDetail.getInitialProps = async (ctx) => {
let response = await request.get(`/api/users/${ctx.query.id}`)
return { user: response.data.data };
}
export async function getStaticPaths() {
const res = await request.get('http://localhost:5000/api/users');
const users = res.data;
const paths = users.map(user => `/user/detail/${user.id}`);
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
console.log('params', params, new Date().toTimeString());
const res = await request.get(`http://localhost:5000/api/users/${params.id}`);
return {
props: {
user: res.data
}
}
}
export default UserDetail;
npm run build
npm run start
npm run build
start.js
const next = require('next');
const app = next({ dev:false });
const handler = app.getRequestHandler();
app.prepare().then(() => {
let express = require("express");
let bodyParser = require("body-parser");
let {UserModel} = require('./model');
let session = require("express-session");
let config = require('./config');
let MongoStore = require('connect-mongo')(session);
let app = express();
//.......
app.get('*', async (req, res) => {
await handler(req, res);
})
app.listen(5000, () => {
console.log('服务器在5000端口启动!');
});
});
const express = require('express');
const cors = require('cors');
const session = require('express-session');
const app = express();
app.use(
cors({
origin: ['http://localhost:3000'],
credentials: true,
allowedHeaders: "Content-Type,Authorization",
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS"
})
);
app.use(session({
saveUninitialized: true,
resave: true,
secret: 'zhufeng'
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const users = [];
app.get('/api/users', (req, res) => {
res.json({
success: true,
data: users
});
});
app.post('/api/register', (req, res) => {
const user = req.body;
user.id = Date.now() + "";
user.createdAt = new Date().toISOString();
users.push(user);
res.json({
success: true,
data: user
})
});
app.get('/api/users/:id', (req, res) => {
const id = req.params.id;
const user = users.find(user => user.id === id);
res.json({
success: true,
data: user
})
});
app.post('/api/login', (req, res) => {
const user = req.body;
req.session.user = user;
res.json({
success: true,
data: user
})
});
app.get('/api/logout', (req, res) => {
req.session.user = null;
res.json({
success: true,
data: null
})
});
app.get('/api/validate', (req, res) => {
const user = req.session.user;
if (user) {
res.json({
success: true,
data: user
})
} else {
res.json({
success: false,
error: `用户未登录`
})
}
});
app.listen(5000, () => console.log('api server started on port 5000'));