简介:该教程兼容pc+移动端,如只需一端,可忽略兼容部分教程,根据需要运行的客户端构建项目
Next.js,这是一个 React 的同构应用开发框架。
yarn create next-app “文件名” --typescript
yarn dev
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return
}
import {NextPage} from "next";
const Home: NextPage = (props) => {
return dsada
};
export default Home
yarn add sass
//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字体大小
"paths": {
...
"@css/": [
"./src/assets/css/"
],
"@img/": [
"./src/assets/img/"
],
...
}
import type { AppProps } from "next/app";
import "@css/iframe.scss";
export default function App({ Component, pageProps }: AppProps) {
return
}
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
:root:root {
--adm-color-primary: #ff4d4f;
}
/* 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;
}
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
}
yarn add postcss-px-to-viewport-8-plugin --dev
//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/], //设置忽略文件,用正则做目录名匹配
}
},
};
yarn add redux react-redux redux-saga
yarn add @types/react-redux @types/redux-saga next-redux-wrapper redux-devtools-extension --dev
"paths": {
...
"@reducers/*": [
"./src/redux/store/reducers/*"
],
"@sagas/*": [
"./src/redux/store/sagas/*"
],
"@store/*": [
"./src/redux/store/*"
],
...
}
/**
* @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;
/**
* @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;
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;
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)
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)
{
"extends": "next/core-web-vitals",
"rules": {
"react/display-name": "off"
}
}
//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>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;
//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;
}
@import "./index";
import Button from "@/components/antd/button";
yarn add redux-persist
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)
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 <>
页面标题
>
}
import {Html, Head, Main, NextScript} from 'next/document'
export default function Document() {
return (
)
}
export default function Custom_404(){
return <>404页面>
}
import idCard from '@img/idCard.png';
//建议用div包括起来,不单独使用,单独使用会自动生成很多自带的样式;Image会自适应div大小
import idCard from '@img/idCard.png';
import Image from 'next/image';
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images:{
unoptimized:true
}
}
module.exports = nextConfig
yarn add sharp
//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";
import {NextRouter, useRouter} from "next/router";
const router:NextRouter = useRouter();
const nextConfig = {
...
//自定义路由,通常不需要自定义路由,适用于SS
exportPathMap: async ()=>{
return {
'/':{
page:'/index'
}
}
},
...
}
module.exports = nextConfig
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
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,然后静态化输出。由于是完全静态化输出,当数据变化时,必须重新静态化才能更新页面
yarn add 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;
"paths": {
...
"@common/*": [
"./src/common/*"
],
"@api/*": [
"./src/pages/api/*"
],
...
}
// 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)
}
}
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
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint"
},
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
basePath: process.env.NODE_ENV == "development"? '':'/app'
images:{
unoptimized:true
}
}
module.exports = nextConfig
"scripts": {
"dev": "next dev",
"build": "next build && next export -o app",
"start": "next start",
"lint": "next lint"
},
```
yarn build
yarn start -p 4000 //默认端口3000
```
npm install cross-env -g
"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
},
yarn add cross-env --dev
//.env.test
TEST=test //只有服务端可以获取到
NEXT_PUBLIC_HOST=http://127.0.0.1:3000/ //变量暴漏给浏览器端,加NEXT_PUBLIC_
"scripts": {
"dev:test": "cross-env NODE_ENV=test next dev",
},
页面打印:
console.log(process.env.TEST);
console.log(process.env.NEXT_PUBLIC_HOST);
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/;
}
}
{
"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"
}
let exec = require("child_process").exec;
//yarn start -p 9003 指定端口运行项目
exec("yarn start", {windowsHide: true});
```
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',
},
}