简介:该教程兼容pc+移动端,兼容原理:同时封装了pc框架antd、和移动端框架antdMobile,根据不同客户端类型渲染不同的组件,同一个组件需要根据客户端类型同时封装pc和移动端,如果觉得开发麻烦,可忽略兼容部分教程,根据需要分别构建pc、和移动端
- antd官网:https://ant.design/components/overview-cn/
- antd-mobile官网:https://mobile.ant.design/zh
- next.js: https://www.nextjs.cn/
- react:https://react.zcopy.site/
- redux:https://react-redux.js.org/api/hooks#custom-context
一、介绍
Next.js,这是一个 React 的同构应用开发框架。
- 直观的、 基于页面 的路由系统(并支持 动态路由)
- 预渲染。支持在页面级的 静态生成 (SSG) 和 服务器端渲染 (SSR)
- 自动代码拆分,提升页面加载速度
- 具有经过优化的预取功能的 客户端路由
- 内置 CSS 和 Sass 的支持,并支持任何 CSS-in-JS 库
- 开发环境支持 快速刷新
- 利用 Serverless Functions 及 API 路由 构建 API 功能
- 完全可扩展
二、构建项目
yarn create next-app “文件名” --typescript
yarn dev
三、调整项目
- 文件目录
- _app.tsx
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return
}
- index.tsx
import {NextPage} from "next";
const Home: NextPage = (props) => {
return dsada
};
export default Home
三、静态资源assets
- 创建assets>css、assets>font、assets>img
- 安装依赖
yarn add sass
- 集成字体图标,下载阿里icon库,解压后把压缩包里的文件复制到assets>font
- css文件下分别创建globals.scss、iframe.scss、normalize.scss、variable.scss
//globals.scss 全局样式文件
body{
font-size: $font_size!important;
}
//iframe.scss 公共样式导入
@import "./globals";
@import "./normalize";
@import "../font/iconfont.css";
//normalize.scss 同一浏览器样式,下载后放入该文件中
http://nicolasgallagher.com/about-normalize-css/
https://github.com/necolas/normalize.css
//variable.scss 全局变量文件
$primary-color: red;
/**
* 字体大小
*/
$font_size: 14px;//基础字体大小
$sm_font_size: 12px;//小号字体
$bg_font_size: 16px;//大号字体
$xl_font_size: 20px;//超大号字体
/**
* icon 大小
*/
$icon_size: $font_size;//默认字体
$bg_icon_size: $bg_font_size;//大号字体
$sm_icon_size: $sm_font_size;//小号字体
$xl_icon_size: $xl_font_size;//超大号字体
/**
* button 颜色、大小
*/
$btn_primary: #1677ff;
$btn_danger: #ff4d4f;
/**
* h1-h5标签字体大小
*/
$h1_font_size: 38px;//h1字体大小
$h2_font_size: 30px;//h2字体大小
$h3_font_size: 24px;//h3字体大小
$h4_font_size: $xl_font_size;//h4字体大小
$h5_font_size: $bg_font_size;//h5字体大小
- 配置引入路径tsconfig.json
"paths": {
...
"@css/": [
"./src/assets/css/"
],
"@img/": [
"./src/assets/img/"
],
...
}
- 引入全局样式,修改_app.tsx
import type { AppProps } from "next/app";
import "@css/iframe.scss";
export default function App({ Component, pageProps }: AppProps) {
return
}
- 引入全局sass变量,next.config.js
const path = require("path");
/** @type {import('next').NextConfig} */
const nextConfig = {
...
sassOptions:{
includePaths: [path.join(__dirname, "./src/assets/css")],
prependData: "@import 'variable.scss';"
},
...
}
module.exports = nextConfig
- 配置antd-mobile主题,https://mobile.ant.design/zh/guide/theming,新建 css>antdMobileTheme.scss
:root:root {
--adm-color-primary: #ff4d4f;
}
- 配置antd主题
- 新建pages>antTheme.module.scss
/* antd 主题配置
* 详细配置可参考 https://ant.design/docs/react/customize-theme-cn*/
:export {
colorPrimary: $primary-color;
fontSize: $font_size;
fontSizeHeading1: $h1_font_size;
fontSizeHeading2:$h2_font_size;
fontSizeHeading3:$h3_font_size;
fontSizeHeading4:$h4_font_size;
fontSizeHeading5:$h5_font_size;
fontSizeLG:$bg_font_size;
fontSizeSM:$sm_font_size;
fontSizeXL:$xl_font_size;
fontSizeIcon:$sm_icon_size;
}
- 修改_app.tsx
import type { AppProps } from "next/app";
import {ConfigProvider} from "antd";
import them from "./antTheme.module.scss";
import "@css/iframe.scss";
export default function App({ Component, pageProps }: AppProps) {
return
}
- 集成postcss
- 安装依赖postcss-px-to-viewport-8-plugin
yarn add postcss-px-to-viewport-8-plugin --dev
- 根目录新建postcss.config.js
//postcss.config.js
module.exports = {
plugins: {
"postcss-px-to-viewport-8-plugin": {
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度
viewportHeight: 912, // 视窗的高度,对应的是我们设计稿的高度,可以不设置
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
propList: ['*'],
selectorBlackList: [/^.pc/],
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 允许在媒体查询中转换`px`,
exclude: [/pc.module/,/antTheme.module.scss/,/braft-editor/], //设置忽略文件,用正则做目录名匹配
}
},
};
参数 | 说明 | 类型 | 默认值 |
unitToConvert | 需要转换的单位,默认为 px | string | px |
viewportWidth | 设计稿的视口宽度,如传入函数,函数的参数为当前处理的文件路径,函数返回 undefind 跳过转换 | number | Function | 320 |
unitPrecision | 单位转换后保留的精度 | number | 5 |
propList | 能转化为 vw 的属性列表 | string[] | ['*'] |
viewportUnit | 希望使用的视口单位 | string | vw |
fontViewportUnit | 字体使用的视口单位 | string | vw |
selectorBlackList | 需要忽略的 CSS 选择器,不会转为视口单位,使用原有的 px 等单位 | string[] | [] |
minPixelValue | 设置最小的转换数值,如果为 1 的话,只有大于 1 的值会被转换 | number | 1 |
mediaQuery | 媒体查询里的单位是否需要转换单位 | boolean | false |
replace | 是否直接更换属性值,而不添加备用属性 | boolean | true |
landscape | 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape) | boolean | false |
landscapeUnit | 横屏时使用的单位 | string | vw |
landscapeWidth | 横屏时使用的视口宽度,,如传入函数,函数的参数为当前处理的文件路径,函数返回 undefind 跳过转换 | number | 568 |
exclude | 忽略某些文件夹下的文件或特定文件,例如 node_modules 下的文件,如果值是一个正则表达式,那么匹配这个正则的文件会被忽略,如果传入的值是一个数组,那么数组里的值必须为正则 | Regexp | undefined |
include | 需要转换的文件,例如只转换 'src/mobile' 下的文件 ( include: /\/src\/mobile\// ),如果值是一个正则表达式,将包含匹配的文件,否则将排除该文件, 如果传入的值是一个数组,那么数组里的值必须为正则 | Regexp | undefined |
四、集成redux
- 安装依赖
yarn add redux react-redux redux-saga
yarn add @types/react-redux @types/redux-saga next-redux-wrapper redux-devtools-extension --dev
- 创建redux>reducers、redux>sagas文件夹
- 配置引入路径tsconfig.json
"paths": {
...
"@reducers/*": [
"./src/redux/store/reducers/*"
],
"@sagas/*": [
"./src/redux/store/sagas/*"
],
"@store/*": [
"./src/redux/store/*"
],
...
}
- 创建第一个store,redux>reducers>mobileStore.tsx
/**
* @description 该store,判断是否是移动端
* */
/**
* @description 定义相关接口或者枚举
* */
export enum MobileStoreActionEnum {
INIT="mobileStoreInit",
CHANGE="mobileStoreChange"
}
export type MobileStoreStateType = boolean;
interface MobileStoreActionInterface{
type: MobileStoreActionEnum,
payload:MobileStoreStateType
}
/**
* @description store逻辑
* */
const mobileInitState:MobileStoreStateType = false;
const mobileStore = (state:MobileStoreStateType =mobileInitState, action: MobileStoreActionInterface):MobileStoreStateType => {
switch (action.type) {
case MobileStoreActionEnum.INIT:
return state
case MobileStoreActionEnum.CHANGE:
return action.payload
default:
return state
}
}
export default mobileStore;
- 创建第一个sagaStore,redux>reducers>demoStore.tsx,异步store
/**
* @description 定义相关接口或者枚举
* */
export enum DemoStoreActionEnum{
WATCH='watchDemoStore',
CHANGE='demoStoreChange'
}
interface DemoStoreStateInterface {
num:number
}
export interface DemoStoreActionInterface {
type: DemoStoreActionEnum
payload: DemoStoreStateInterface
}
/**
* @description store逻辑
* */
const initState:DemoStoreStateInterface = {
num: 100
}
const demoStore = (state:DemoStoreStateInterface = initState, action: DemoStoreActionInterface):DemoStoreStateInterface => {
switch (action.type) {
case DemoStoreActionEnum.CHANGE:
return action.payload
default:
return state
}
};
export default demoStore;
- 依次创建redux>sagas>demo.tsx、redux>sagas>mainSaga.tsx
- saga的应用场景是复杂异步,如长时事务LLT(long live.transcation)等业务场景。
- 方便测试,可以使用takeEvery打印logger。
- 提供takeLatest/takeEvery/throttle方法,可以便利的实现对事件的仅关注最近事件、关注每一次、事件限频
- 提供cancel/delay方法,可以便利的取消、延迟异步请求
- 提供race(effects),[…effects]方法来支持竞态和并行场景
- 提供channel机制支持外部事
import { call, put, takeEvery, takeLatest,take,all,race,throttle,delay,fork,cacel,cancelled} from 'redux-saga/effects';
takeEvery:被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听。
take:与takeEver相反,与 action 只会监听一次,使用一次就销毁
takeLatest:每次 action 被匹配,当有action正在匹配,会放弃正在匹配的action,执行最新的
call: saga通过 Generator函数实现,在yield函数后执行effect,其中call是用于执行某些异步操作的。
put: 和上面的call一样,中间件提供put 来把action丢到中间件中去dispatch,好处同样是便于测试
all: 同步执行多个任务使需要用到 yield all([call(fetch, '/users'),call(fetch, '/repos')])
race: 和promise中的race一个概念,执行多个任务,受到响应后则继续执行 yield race({posts: call(fetchApi, '/posts'),timeout: call(delay, 1000)})
fork:fork和take不同,take会和call一样阻塞代码的执行,知道结果返回,fork则不会,它会将任务启动并且不阻塞代码的执行,fork会返回一个task,可以用cacel(task)来取消任务
cacel:来取消任务
cancelled:如果当前任务,被cacel取消,则返回true
throttle:节流
//redux>sagas>demo.tsx
import {call, put, takeEvery} from "@redux-saga/core/effects";
import {DemoStoreActionEnum, DemoStoreActionInterface} from "@reducers/demoStore";
// 延时器
const delay = (ms:number) => new Promise(resolve => setTimeout(resolve, ms));
function* asyncDemoSaga(action:DemoStoreActionInterface):Generator {
yield call(delay,2000);
yield put({ type: DemoStoreActionEnum.CHANGE,payload:action.payload})
}
function* watchDemoSaga():Generator {
yield takeEvery(DemoStoreActionEnum.WATCH, asyncDemoSaga)
}
export default watchDemoSaga;
//redux>sagas>mainSaga.tsx
import {all} from "redux-saga/effects"
import watchDemoSaga from "@sagas/demo";
// saga中间件 主saga,用于区别是否需要saga来处理异步操作,如果没有异步,则放行
function* mainSaga() {
yield all([
// 监听 saga 中有 userWatchSaga 操作,所以会拦截这个 action
watchDemoSaga(),
])
}
// 主saga要对外暴露出去
export default mainSaga;
- 修改_app.tsx
import type { AppProps } from "next/app";
import {ConfigProvider} from "antd";
import them from "./antTheme.module.scss";
import "@css/iframe.scss";
import {useEffect} from "react";
import {useDispatch} from "react-redux";
import { MobileStoreActionEnum} from "@reducers/mobileStore";
import wrapper from "@/redux";
import {Dispatch} from "redux";
const App = ({ Component, pageProps }: AppProps) => {
const dispatch:Dispatch = useDispatch();
useEffect(():void => {
//判断是哪个客户端(pc,mobile),主要用来兼容样式
if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) {
dispatch({
type: MobileStoreActionEnum.CHANGE,
payload: true
});
//增加全局class,用于设置全局样式
document.getElementsByTagName('html')[0].className = 'mobile';
}else{
//增加全局class,用于设置全局样式
document.getElementsByTagName('html')[0].className = 'pc';
}
},[])
return
}
export default wrapper.withRedux(App)
- 创建redux>index.tsx
import { createWrapper, MakeStore } from "next-redux-wrapper";
import { applyMiddleware, createStore, Store} from "redux";
import createSagaMiddleware, {SagaMiddleware} from "redux-saga";
import { composeWithDevTools } from "redux-devtools-extension/developmentOnly";
import rootReducer from "@store/index";
import mainSaga from "@sagas/mainSaga"; //异步初始化store
const makeStore: MakeStore = () => {
const sagaMiddleware:SagaMiddleware = createSagaMiddleware()
const store:Store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)))
sagaMiddleware.run(mainSaga)
return store
}
export default createWrapper(makeStore)
- 修改.eslintrc.json
{
"extends": "next/core-web-vitals",
"rules": {
"react/display-name": "off"
}
}
- 创建components>antd>button
- 创建pc端button组件button>pc.tsx、button>pc.module.scss
//button>pc.tsx
/**
* @description pc端Button组件
* */
/**********第三方插件、组件引用**********/
import React from "react";
import {Button as PcButton, ButtonProps} from "antd";
import {SizeType} from "antd/es/config-provider/SizeContext";
import {ButtonType} from "antd/es/button";
/**********当前目录文件*********/
import styles from "./pc.module.scss";
export interface PcButtonInterface {
type?: ButtonType,
size?: SizeType,
onClick?: ButtonProps['onClick'],
children?: React.ReactNode
}
const Button = React.memo((props:PcButtonInterface)=>{
return
{ props.children }
});
export default Button;
- 创建移动端button组件button>mobile.tsx、button>mobile.module.scss
//button>mobile.tsx
/**
* @description 移动端Button组件
* */
/**********第三方插件、组件引用**********/
import React from "react";
import {Button as MobileButton, ButtonProps} from "antd-mobile";
/**********当前目录文件*********/
import styles from "./mobile.module.scss";
export interface MobileButtonInterface {
type?: ButtonProps['color'],
size?: ButtonProps['size'],
onClick?:ButtonProps['onClick'],
children?: React.ReactNode;
}
const Button = React.memo((props:MobileButtonInterface)=>{
return
{ props.children }
});
export default Button;
- 创建兼容pc+移动组件button>index.tsx、button>index.scss
//button>index.tsx
/**
* @description 同时兼容pc、移动的Button组件
* */
import React, {useState} from "react";
import PcButton, {PcButtonInterface} from "./pc";
import MobileButton, {MobileButtonInterface} from "./mobile";
import {useSelector, useStore} from "react-redux";
import {Store} from "redux";
interface ClientButtonInterface {
type?: PcButtonInterface['type'] & MobileButtonInterface['type'],
size?: PcButtonInterface['size'] & MobileButtonInterface['size'],
onClick?: PcButtonInterface['onClick'] & MobileButtonInterface['onClick'],
children?: React.ReactNode
}
const Button = React.memo((props: ClientButtonInterface) => {
const store:Store = useStore();
const storeState = store.getState() as any;
const [mobile,setMobile]= useState(storeState.mobileStore)
useSelector((state:any):void => {
if(mobile!=state?.mobileStore){
setMobile(state?.mobileStore);
}
});
return <>
{mobile ? : }
>
});
export default Button;
//button>index.scss
.button{
font-size: 14px;
height: 32px;
padding: 4px 15px;
border-radius: 6px;
}
- 修改button>mobile.module.scss、button>pc.module.scss
@import "./index";
- 使用
import Button from "@/components/antd/button";
- 持续储存
- 安装依赖,https://www.npmjs.com/package/redux-persist
yarn add redux-persist
- 修改redux>index.tsx
import { createWrapper, MakeStore } from "next-redux-wrapper";
import { applyMiddleware, createStore, Store} from "redux";
import createSagaMiddleware, {SagaMiddleware} from "redux-saga";
import { composeWithDevTools } from "redux-devtools-extension/developmentOnly";
import {persistStore, persistReducer} from "redux-persist";
import storage from "redux-persist/lib/storage";
import rootReducer from "@store/index";
import mainSaga from "@sagas/mainSaga"; //异步初始化store
//持久化储存配置
const persistConfig = {
key: 'root', //在localStorge中生成key为root的值
storage,
blacklist:['demoSaga'] //设置某个reducer数据不持久化
}
const makeStore: MakeStore = () => {
const sagaMiddleware:SagaMiddleware = createSagaMiddleware();
const rootPersistReducer = persistReducer(persistConfig, rootReducer)
const store:Store = createStore(rootPersistReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)))
sagaMiddleware.run(mainSaga);
persistStore(store);
return store
}
export default createWrapper(makeStore)
五、页面配置
- 设置页面标题:_app.tsx
import '@/assets/css/globals.scss';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { ConfigProvider } from 'antd';
import them from '@/pages/app.module.scss';
export default function App({ Component, pageProps }: AppProps) {
return <>
页面标题
>
}
- 设置页面框架代码:_document.tsx,只会在初始化预渲染,设置的内容只会在build后生效
import {Html, Head, Main, NextScript} from 'next/document'
export default function Document() {
return (
)
}
- 自定义404页面,pages下新建404.tsx页面
export default function Custom_404(){
return <>404页面>
}
六、图片引用
- 方法一:原生img,使用' '可能会导致LCP变慢和带宽增加,build时会有此警告
import idCard from '@img/idCard.png';
- 方法二:使用 next/image;简单的图片引用建议用原生img
//建议用div包括起来,不单独使用,单独使用会自动生成很多自带的样式;Image会自适应div大小
import idCard from '@img/idCard.png';
import Image from 'next/image';
- next/image 自带优化的api适用于SSR,SSG中无法使用 ,可以改动 next.config.js 配置解决
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images:{
unoptimized:true
}
}
module.exports = nextConfig
- 安装sharp包(高性能图片处理器),Next.js将自动使用它的图像优化
yarn add sharp
七、动态路由
- 创建pages>demo文件夹
- 创建demo>index.tsx、demo>index.scss、demo>mobile.module.scss、demo>pc.module.scss
//demo>index.tsx
import Image from "next/image";
import idCard from "@img/idCard.png";
import useStyle from "@hook/styleHook";
import mobileStyle from "./mobile.module.scss";
import pcStyle from "./pc.module.scss";
const Demo = () => {
const styles = useStyle(pcStyle,mobileStyle);
return
;
};
export default Demo
//demo>index.scss
.P_demo{
img{
width: 100px;
height: 100px;
}
}
//demo>mobile.module.scss、demo>pc.module.scss
@import "./index";
- 修改page>index.tsx
import {NextRouter, useRouter} from "next/router";
const router:NextRouter = useRouter();
- 自定义路由,改动 next.config.js 配置
const nextConfig = {
...
//自定义路由,通常不需要自定义路由,适用于SS
exportPathMap: async ()=>{
return {
'/':{
page:'/index'
}
}
},
...
}
module.exports = nextConfig
- 动态路由
- 修改demo>index.tsx为demo>[id].tsx
import Image from "next/image";
import idCard from "@img/idCard.png";
import useStyle from "@hook/styleHook";
import mobileStyle from "./mobile.module.scss";
import pcStyle from "./pc.module.scss";
import {NextRouter, useRouter} from "next/router";
const Demo = () => {
const styles = useStyle(pcStyle,mobileStyle);
const router:NextRouter = useRouter();
console.log(router.query)
return
;
};
export default Demo
- 修改pages>index.tsx
- 路由嵌套
- [id].tsx:paths 里面参数只有一个id ,/demo/1
- [...routers].tsx :paths 里面可以包含多个路径,以数组形式发挥参数,/demo/1/3
- [id]/[comment].tsx:paths 里面可以包含多个路径,以json形式返回,/demo/1/3
八、接口api
NEXT.js存在CSR/SSR/SSG 三种请求方式,最多存在两种:1、CSR+SSR;2、CSR+SSG
CSR请求:常规前端项目中请求方式,由客户端浏览器端发送请求,拿到数据后再渲染道页面
SSR请求:由服务端发起请求(NEXT.js中的node.js),拿到数据后,组装HTML,再把HTML返回到客户端浏览器
SSG请求:与SSR请求类似,由服务端发起请求(NEXT.js中的node.js),拿到数据后,组装HTML,然后静态化输出。由于是完全静态化输出,当数据变化时,必须重新静态化才能更新页面
- 安装axios
yarn add axios
- 封装axios
/**
* @description axios公共请求封装
* */
import axios, {AxiosResponse, InternalAxiosRequestConfig} from "axios";
/**
* @description 定义相关接口或者枚举
* */
//请求枚举
export enum MethodEnum {
Get='GET',
Post='POST'
}
//返回结果
export interface ResponseResultInterface {
Header:{},
Body: Body
}
//请求参数
export interface RequestInterface {
url:string,
params?:params,
method?:MethodEnum
}
/**
* 封装axios
* */
// 添加请求拦截器
axios.interceptors.request.use( (config:InternalAxiosRequestConfig)=>{
return config;
}, (error)=>{
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use( (response:AxiosResponse) => {
return response;
}, (error) => {
return Promise.reject(error);
});
/**
* @method useAxiosRequest axios请求封装
* @param requestPar { RequestInterface } 请求url
* @return Promise
* */
const baseRequest= async (requestPar:RequestInterface):Promise => {
const requestResult:AxiosResponse = await axios({
method: requestPar.method || MethodEnum.Post,
url: requestPar.url,
data: requestPar.params
});
return requestResult.data as responseData;
};
export default baseRequest;
- 配置引入路径,修改 tsconfig.json
"paths": {
...
"@common/*": [
"./src/common/*"
],
"@api/*": [
"./src/pages/api/*"
],
...
}
- 创建api服务,pages>api>demoApi.tsx
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import {ResponseResultInterface} from "@common/baseRequest";
export interface DemoInterface {
id: number,
name?: string
}
type ApiDemoType = ResponseResultInterface
export default function demoApi(
req: NextApiRequest,
res: NextApiResponse
):void {
let data:ApiDemoType= {
Header:{},
Body:{
id:-1
}
};
if(req.method == "GET"){
const id:number = Number(req.query.id);
data.Body.id = id;
switch (id) {
case 1:
data.Body.name = "我是API服务1"
break;
}
res.status(200).json(data)
}else{
res.status(200).json(data)
}
}
- 修改pages>demo>[id].tsx
import Image from "next/image";
import {NextRouter, useRouter} from "next/router";
import {GetServerSideProps} from "next";
import {ParsedUrlQuery} from "querystring";
import idCard from "@img/idCard.png";
import useStyle from "@hook/styleHook";
import baseRequest, {MethodEnum, RequestInterface} from "@common/baseRequest";
import {DemoInterface} from "@api/demoApi";
import mobileStyle from "./mobile.module.scss";
import pcStyle from "./pc.module.scss";
const Demo = (props: DemoInterface) => {
const styles = useStyle(pcStyle,mobileStyle);
const router:NextRouter = useRouter();
console.log(router.query)
console.log(props);
return
;
};
/**
* getServerSideProps 适用于SSR,不适用于SSG
* getStaticProps SSR 和 SSG 均支持,但仅在网站build时候发起API请求
* getServerSideProps 和 getStaticProps请求都是在服务端进行不涉及跨域
* */
export const getServerSideProps: GetServerSideProps = async (paths) => {
const query:ParsedUrlQuery = paths.query;
const requestOption:RequestInterface={
url:`http://localhost:3000/api/demoApi?id=${query.id}`,
method:MethodEnum.Get
}
const requestResult = await baseRequest({
url: requestOption.url,
method:requestOption.method
});
return {
props: requestResult.Body
}
}
/**
* SSG 静态生成
* getStaticPaths build 时会生成多个页面
* 只是用于getStaticProps
* */
// export const getStaticPaths: GetStaticPaths = async () => {
// // const arr: string[] = ['1', '2'];
// // const paths = arr.map((id) => {
// // return {
// // params: { id },
// // }
// // })
// // return {
// // //这里的路由参数提供给getStaticProps使用
// // paths,
// // //不在以上参数路由将返回404
// // fallback: false
// // }
// const id1:DemoParams = {id: '1'};
// const id2:DemoParams = {id: '2'};
// const staticPathOption = {
// //这里的路由参数提供给getStaticProps使用
// path: [{
// params: id1
// }, {
// params: id2
// }],
// //不在以上参数路由将返回404dc
// // fallback: false
// }
// return staticPathOption
// }
export default Demo
九、生成静态网站(SSG)
- 设置SSG的export命令,修改package.json
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint"
},
- 然后执行yarn build,该命令回显执行next build 再执行 next export,输出目录为根目录下的out目录
- 设置静态资源的bassePath,如果发布在服务器二级目录需要设置,更改next.config.js 配置;/app为二级目录
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
basePath: process.env.NODE_ENV == "development"? '':'/app'
images:{
unoptimized:true
}
}
module.exports = nextConfig
- 设置输出目录export输出目录为app
"scripts": {
"dev": "next dev",
"build": "next build && next export -o app",
"start": "next start",
"lint": "next lint"
},
十、以SSR模式运行项目
yarn build
yarn start -p 4000 //默认端口3000
十一、动态生成项目目录
- 安装依赖
npm install cross-env -g
- package.json
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint",
"customBuild": "cross-env BASE_PSTH=%npm_config_base% next build && next export -0 %npm_config_base%",
"customBuild": "cross-env BASE_PSTH=$npm_config_base next build && next export -0 $npm_config_base%"//mac
},
- 运行npm run customBuild --base=/demo --out=demo
十二、多环境开发配置
- 安装依赖
yarn add cross-env --dev
- 根目录下创建.env.production(生产环境)、.env.development(开发环境)、.env.test(测试环境)、.env.local(默认环境,始终覆盖默认设置)、.env(所有环境)
//.env.test
TEST=test //只有服务端可以获取到
NEXT_PUBLIC_HOST=http://127.0.0.1:3000/ //变量暴漏给浏览器端,加NEXT_PUBLIC_
- 指定环境运行,修改package.json
"scripts": {
"dev:test": "cross-env NODE_ENV=test next dev",
},
页面打印:
console.log(process.env.TEST);
console.log(process.env.NEXT_PUBLIC_HOST);
十二、项目部署
- 安装nginx:https://nginx.org/en/download.html,下载合适的版本,解压到任意位置nginx 配置
- 启动cd到nginx目录运行 nginx.exe
- 启动cd到nginx目录运行start nginx,访问http://localhost:80 正常访问nginx运行成功
- 启动cd到nginx目录运行重新加载配置
- 启动cd到nginx目录运行nginx -s stop 停止
- 启动cd到nginx目录运行nginx -s quit 正常关闭
- 启动cd到nginx目录运行nginx -s reopen 重新打开
- 配置nginx.conf
server {
listen 9001;
server_name localhost;
# server_name btyhub.site, www.btyhub.site;
# ssl两个文件,放在 nginx的conf目录中
# ssl_certificate btyhub.site_bundle.pem;
# ssl_certificate_key btyhub.site.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# 代理到Next的服务,默认3000端口,也可以在start的时候指定
location / {
proxy_pass http://127.0.0.1:3000/;
}
}
- 安装 pm2,node进程管理工具:npm install -g pm2
- 把打包后得.next,next.config.js 上传服务器
- package.json,运行yarn install
{
"name": "react_common",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "next start"
},
"dependencies": {
//项目下package.json 中dependencies
},
"devDependencies": {
//项目下package.json 中devDependencies
"child_process": "^1.0.2"
}
- 安装child_process,运行yarn add child_process --dev,创建start.js
let exec = require("child_process").exec;
//yarn start -p 9003 指定端口运行项目
exec("yarn start", {windowsHide: true});
- 运行pm2 start start.js --name projectName,pm2:node进程管理工具
十三、其他
- next 命令
yarn dev --help 某个命令帮助信息
next lint 设置 Next.js 的内置 ESLint 配置
next lint --no-cache 清除 ESLint 缓存
- next内置组件
- import Head from 'next/head'
- import Image from 'next/image'
- import { Inter } from 'next/font/google'
- import { Html, Head, Main, NextScript } from 'next/document'
- import type { AppProps } from 'next/app'
- import type { NextApiRequest, NextApiResponse } from 'next'
- import { useRouter } from 'next/router'
- import Link from 'next/link'
- import Script from 'next/script'
- import { useAmp } from 'next/amp'
- import type { NextRequest } from 'next/server'
- import { Inter } from 'next/font/google'
- import getConfig from 'next/config'
- next.config.js
const configs = {
// 编译文件的输出目录
distDir: 'dest',
// 是否给每个路由生成Etag
generateEtags: true,
// 页面内容缓存配置
onDemandEntries: {
// 内容在内存中缓存的时长(ms)
maxInactiveAge: 25 * 1000,
// 同时缓存多少个页面
pagesBufferLength: 2,
},
// 在pages目录下那种后缀的文件会被认为是页面
pageExtensions: ['jsx', 'js'],
// 配置buildId
generateBuildId: async () => {
if (process.env.YOUR_BUILD_ID) {
return process.env.YOUR_BUILD_ID
}
// 返回null使用默认的unique id
return null
},
// 手动修改webpack config
webpack(config, options) {
return config
},
// 修改webpackDevMiddleware配置
webpackDevMiddleware: config => {
return config
},
// 可以在页面上通过 procsess.env.customKey 获取 value
env: {
customKey: 'value',
},
//CDN 前缀支持
assetPrefix: 'https://cdn.mydomain.com',
//静态优化指标
devIndicators: {
autoPrerender: false
},
//禁止etag生成
generateEtags: false,
//禁止x-powered-by,x-powered-by用于告知网站是用何种语言或框架编写的
poweredByHeader: false
//自定义路由
exportPathMap: async ()=>{
return {
'/':{
page:'/index'
}
}
},
images:{
unoptimized:true
},
// 只有在服务端渲染时才会获取的配置
serverRuntimeConfig: {
mySecret: 'secret',
secondSecret: process.env.SECOND_SECRET,
},
// 在服务端渲染和客户端渲染都可获取的配置
publicRuntimeConfig: {
staticFolder: '/static',
},
}