简介:该教程兼容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 <Component {...pageProps} /> }
- index.tsx
import {NextPage} from "next"; const Home: NextPage = (props) => { return <div>dsada</div> }; export default Home
三、静态资源assets
- 创建assets>css、assets>font、assets>img
- 安装依赖
yarn add sass
- 集成字体图标,下载阿里icon库,解压后把压缩包里的文件复制到assets>font
<i class="iconfont icon-usename"></i>
- 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 <Component {...pageProps} /> }
- 引入全局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 <ConfigProvider theme={{token: them}}> <Component {...pageProps}/> </ConfigProvider> }
- 集成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/], //设置忽略文件,用正则做目录名匹配 } }, };
四、集成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 <ConfigProvider theme={{token: them}}> <Component {...pageProps}/> </ConfigProvider> } 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<Store> = () => { const sagaMiddleware:SagaMiddleware = createSagaMiddleware() const store:Store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware))) sagaMiddleware.run(mainSaga) return store } export default createWrapper<Store>(makeStore)
- 封装pc+移动端兼容性button组件,创建兼容性ui框架,需要把4. antd、antd-mobile二次封装,并合并一些共同的参数,修改成共同的样式;创建非ui框架的组件,只要注意像素单位的兼容就行,如:mobile.module.scss、pc.module.scss,postcss已限制pc.module 样式文件的转换
- 修改.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 <PcButton className={styles.button} type={props.type} size={props.size} onClick={props.onClick}> { props.children } </PcButton > }); 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 <MobileButton className={styles.button} color={props.type} size={props.size} onClick={props.onClick}> { props.children } </MobileButton> }); 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 ? <MobileButton {...props}/> : <PcButton {...props}/>} </> }); 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"; <Button type="primary">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<Store> = () => { 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<Store>(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 <> <Head> <title>页面标题</title> </Head> <ConfigProvider theme={{token: them}}> <Component {...pageProps}/> </ConfigProvider> </> }
- 设置页面框架代码:_document.tsx,只会在初始化预渲染,设置的内容只会在build后生效
import {Html, Head, Main, NextScript} from 'next/document' export default function Document() { return ( <Html lang="en"> <Head> <link rel="icon" href="/favicon.ico"></link> <meta name="description" content="页面框架"></meta> </Head> <body> <Main/> <NextScript/> </body> </Html> ) }
- 自定义404页面,pages下新建404.tsx页面
export default function Custom_404(){ return <>404页面</> }
六、图片引用
- 方法一:原生img,使用’ '可能会导致LCP变慢和带宽增加,build时会有此警告
import idCard from '@img/idCard.png'; <img src={idCard.src}/>
- 方法二:使用 next/image;简单的图片引用建议用原生img
//建议用div包括起来,不单独使用,单独使用会自动生成很多自带的样式;Image会自适应div大小 import idCard from '@img/idCard.png'; import Image from 'next/image'; <div><Image src={idCard} alt=""/></div>
- 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 <div className={styles.P_demo}> <Image src={idCard} alt=""/> </div>; }; 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(); <Button onClick={() => router.push('/demo')}>goDemo</Button>
- 自定义路由,改动 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 <div className={styles.P_demo}> <Image src={idCard} alt=""/> </div>; }; export default Demo
- 修改pages>index.tsx
<Button onClick={() => router.push('/demo/1')}>goDemo</Button>
- 路由嵌套
- [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<Body> { Header:{}, Body: Body } //请求参数 export interface RequestInterface<params> { 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 <params,responseData>(requestPar:RequestInterface<params>):Promise<responseData> => { 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<DemoInterface> export default function demoApi( req: NextApiRequest, res: NextApiResponse<ApiDemoType> ):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 <div className={styles.P_demo}> <Image src={idCard} alt=""/> </div>; }; /** * getServerSideProps 适用于SSR,不适用于SSG * getStaticProps SSR 和 SSG 均支持,但仅在网站build时候发起API请求 * getServerSideProps 和 getStaticProps请求都是在服务端进行不涉及跨域 * */ export const getServerSideProps: GetServerSideProps = async (paths) => { const query:ParsedUrlQuery = paths.query; const requestOption:RequestInterface<undefined>={ url:`http://localhost:3000/api/demoApi?id=${query.id}`, method:MethodEnum.Get } const requestResult = await baseRequest<DemoInterface>({ url: requestOption.url, method:requestOption.method }); return { props: requestResult.Body } } /** * SSG 静态生成 * getStaticPaths build 时会生成多个页面 * 只是用于getStaticProps * */ // export const getStaticPaths: GetStaticPaths<DemoParams> = 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
```
十一·、以SSR模式运行项目
- 安装依赖
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
十三、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',
},
}