Next.js学习笔记
前端前端前端
Cover Image for Next.js学习笔记
Nice

什么是 Next.js

基于React的 SSR(服务端渲染框架)

SSR & CSR

参考链接:https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8

The main difference is that for SSR your server’s response to the browser is the HTML of your page that is ready to be rendered, while for CSR the browser gets a pretty empty document with links to your javascript. That means your browser will start rendering the HTML from your server without having to wait for all the JavaScript to be downloaded and executed. In both cases, React will need to be downloaded and go through the same process of building a virtual dom and attaching events to make the page interactive — but for SSR, the user can start viewing the page while all of that is happening. For the CSR world, you need to wait for all of the above to happen and then have the virtual dom moved to the browser dom for the page to be viewable.

Next.js 的优点

  • 更好的 SEO
  • 更快的首屏渲染速度

Next.js 基础(与 Rect 开发的不同之处)

https://www.nextjs.cn/learn/basics/create-nextjs-app?utm_source=next-site&utm_medium=nav-cta&utm_campaign=next-website

路由映射

在 Next.js 中,一个 page(页面) 就是一个从 .jsjsx.ts.tsx 文件导出(export)的 React 组件 ,这些文件存放在 pages 目录下。每个 page(页面)都使用其文件名作为路由(route)。

js
1pages/about.js/jsx/ts/tsx → /about 2 3pages/dashboard/settings/username.js/dashboard/settings/username 4

自带路由:router/link

使用与react-router类似,包括编程式跳转router.push以及组件式跳转<Link href="/about"><a>click me</a></Link>

js
1import { useRouter } from 'next/router'; 2 3const router = useRouter(); 4router.push({ 5 pathname: '/activities/experience-lesson/course-info', 6 query: { ...queryData, isFree: 0 } 7}); 8router.push('/about'); 9

渲染方式

预渲染

  • 静态生成(Static Generation)(HTML 重用、build 生成)
  • 服务器端渲染(Server-side Rendering)(每次请求生成的 HTML 不同、用户请求时生成)

相关 API

  • 静态生成

    • getStaticProps(context)
    • getStaticPaths(context)
  • 服务器渲染

    • getServerSideProps(context)
  • 客户端获取数据

    • SWR(官方推荐)

注意在开发环境中getStaticPropsgetStaticPaths每次请求都会被调用

使用,在页面文件中导出

js
1function Page({ data }) { 2 // Render data... 3} 4 5// This gets called on every request 6export async function getServerSideProps() { 7 // Fetch data from external API 8 const res = await fetch(`https://.../data`); 9 const data = await res.json(); 10 11 // Pass data to the page via props 12 return { props: { data } }; 13} 14 15export default Page; 16

项目结构

配置 Eslint+Prettier

https://github.com/paulolramos/eslint-prettier-airbnb-react

https://dev.to/karlhadwen/setup-eslint-prettier-airbnb-style-guide-in-under-2-minutes-a27

https://dev.to/bybruno/configuring-absolute-paths-in-react-for-web-without-ejecting-en-us-52h6

解决eslint无法识别动态引入语法import():

相关 issue

js
1// eslint 配置 2parserOptions: { 3 ecmaVersion: 2020, // Use the latest ecmascript standard 4 sourceType: 'module', // Allows using import/export statements 5 ecmaFeatures: { 6 jsx: true // Enable JSX since we're using React 7 } 8}, 9

配置 alias

next.config.js配置

js
1/* eslint-disable no-param-reassign */ 2const path = require('path'); 3 4module.exports = { 5 webpack: (config) => { 6 // Note: we provide webpack above so you should not `require` it 7 // Perform customizations to webpack config 8 config.resolve.alias['@'] = path.resolve(__dirname, './src'); 9 // Important: return the modified config 10 return config; 11 } 12}; 13

eslint 无法识别 alias,需要在根目录下创建文件jsconfig.json并在.eslintrc.js 配置settings

js
1// .eslintrc.js 2module.exports = { 3 root: true, // Make sure eslint picks up the config at the root of the directory 4 extends: ['airbnb', 'airbnb/hooks', 'plugin:prettier/recommended', 'prettier/react'], 5 env: { 6 browser: true, 7 commonjs: true, 8 es6: true, 9 jest: true, 10 node: true 11 }, 12 globals: { 13 wx: true 14 }, 15 parserOptions: { 16 ecmaVersion: 2020, // Use the latest ecmascript standard 17 sourceType: 'module', // Allows using import/export statements 18 ecmaFeatures: { 19 jsx: true // Enable JSX since we're using React 20 } 21 }, 22 rules: { 23 'react/react-in-jsx-scope': 0, 24 'jsx-a11y/alt-text': 0, // img alt 25 'react/prop-types': 0, 26 'jsx-a11y/click-events-have-key-events': 0, 27 'jsx-a11y/no-static-element-interactions': 0, 28 'dot-notation': 0, 29 'import/prefer-default-export': 0, 30 'react/jsx-props-no-spreading': 0, 31 'jsx-a11y/href-no-hash': ['off'], 32 'react/no-array-index-key': 0, 33 'no-console': 0, 34 'no-alert': 0, 35 'consistent-return': 0, 36 // eslint-disable-next-line prettier/prettier 37 eqeqeq: 1, 38 'react/self-closing-comp': 0, 39 'react-hooks/exhaustive-deps': 0, 40 'react/no-danger': 0, 41 'no-shadow': 0, 42 'jsx-a11y/label-has-associated-control': 0, 43 'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }], 44 'max-len': [ 45 'warn', 46 { 47 code: 120, 48 tabWidth: 2, 49 comments: 120, 50 ignoreComments: false, 51 ignoreTrailingComments: true, 52 ignoreUrls: true, 53 ignoreStrings: true, 54 ignoreTemplateLiterals: true, 55 ignoreRegExpLiterals: true 56 } 57 ] 58 }, 59 settings: { 60 'import/resolver': { 61 alias: { 62 map: [['@', './src']], 63 extensions: ['.ts', '.js', '.jsx', '.json'] 64 } 65 } 66 } 67}; 68
json
1{ 2 "compilerOptions": { 3 "baseUrl": "src", 4 "paths": { 5 "@/*": ["./*"] 6 } 7 }, 8 "exclude": ["node_modules", "**/node_modules/*"] 9} 10

封装 axios 在每次请求时显示 spin 组件

这里要注意一点,由于服务端不存在document,所以要判断一下当前所处的环境再去执行操作。

jsx
1import axios from 'axios'; 2import ReactDOM from 'react-dom'; 3import Spin from '../components/Spin/Spin'; 4 5const Axios = axios.create({ 6 timeout: 20000 7}); 8 9const csr = process.browser; 10 11// 当前正在请求的数量 12let requestCount = 0; 13 14function showLoading() { 15 if (requestCount === 0) { 16 var dom = document.createElement('div'); 17 dom.setAttribute('id', 'loading'); 18 document.body.appendChild(dom); 19 ReactDOM.render(<Spin />, dom); 20 } 21 requestCount++; 22 console.log('showLoading', requestCount); 23} 24 25function hideLoading() { 26 requestCount--; 27 if (requestCount === 0) { 28 document.body.removeChild(document.getElementById('loading')); 29 } 30 console.log('hideLoading', requestCount); 31} 32 33Axios.interceptors.request.use( 34 (config) => { 35 csr && showLoading(); 36 return config; 37 }, 38 (err) => { 39 csr && hideLoading(); 40 return Promise.reject(err); 41 } 42); 43 44Axios.interceptors.response.use( 45 (res) => { 46 csr && hideLoading(); 47 return res; 48 }, 49 (err) => { 50 csr && hideLoading(); 51 return Promise.reject(err); 52 } 53); 54 55export default Axios; 56

自定义 input hook

使用后可以免去给每个表单组件设置onChange

jsx
1import { useState } from 'react'; 2 3// 自定义input hook 4// 参考资料:https://rangle.io/blog/simplifying-controlled-inputs-with-hooks/ 5export function useInput(initialValue) { 6 const [value, setValue] = useState(initialValue); 7 8 return { 9 value, 10 setValue, 11 reset: () => setValue(''), 12 bind: { 13 value, 14 onChange: (e) => { 15 setValue(e.target.value); 16 } 17 } 18 }; 19} 20

使用:

jsx
1// 没使用前 2const [phone, setPhone] = useState(''); 3 4<input 5 name="phone" 6 type="number" 7 placeholder="请输入您的手机号码(必填)" 8 className={`${styles['cell-content']} ${styles['cell-content-right']}`} 9 value={phone} 10 onChange={() => setPhone(e.target.value)} 11/>; 12 13// 使用后 14const { value: phone, bind: bindPhone } = useInput(''); 15 16<input 17 name="phone" 18 type="number" 19 placeholder="请输入您的手机号码(必填)" 20 className={`${styles['cell-content']} ${styles['cell-content-right']}`} 21 {...bindPhone} 22/>; 23

封装 Dialog

jsx
1import { createPortal } from 'react-dom'; 2import styles from './Modal.module.css'; 3 4export default function Modal({ content, show, onOk }) { 5 const modal = show && ( 6 <div className={styles['overlay']}> 7 <div className={styles['modal']}> 8 {/* 防止冒泡关闭窗口 */} 9 <div className={styles['wrapper']} onClick={(e) => e.stopPropagation()}> 10 <div className={styles['content']}>{content}</div> 11 <div className={styles['readed_btn']} onClick={() => onOk()}> 12 好 的 13 </div> 14 </div> 15 </div> 16 </div> 17 ); 18 19 const ProtalContent = () => { 20 // 用来处理服务端不存在document的问题 21 try { 22 // 将modal挂在到body上 23 return document && createPortal(modal, document.body); 24 } catch (error) { 25 return null; 26 } 27 }; 28 29 // 动态引入组件 30 // import dynamic from 'next/dynamic'; 31 // const Modal = dynamic(() => import('./components/Modal/Modal'), { ssr: false }); 32 33 return ( 34 <> 35 <ProtalContent /> 36 </> 37 ); 38} 39

移动端适配

使用插件postcss-px-to-viewport

在根目录下新建文件postcss.config.js

js
1module.exports = { 2 plugins: { 3 'postcss-px-to-viewport': { 4 // 视窗的宽度,对应的是我们设计稿的宽度,我们公司用的是375 5 viewportWidth: 375, 6 // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置 7 // viewportHeight: 1334, 8 // 指定`px`转换为视窗单位值的小数位数 9 unitPrecision: 3, 10 // 指定需要转换成的视窗单位,建议使用vw 11 viewportUnit: 'vw', 12 // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名 13 selectorBlackList: ['.ignore'], 14 // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值 15 minPixelValue: 1, 16 // 允许在媒体查询中转换`px` 17 mediaQuery: false 18 // exclude: undefined 19 } 20 } 21}; 22

使用 Docker+coding 实现自动化部署

dockerfile

docker
1# node版本号 2FROM node:12-alpine 3 4# docker build时传进来的值 docker image build -t <name> --build-arg API_ENV=development . 5ARG API_ENV 6 7RUN echo ${API_ENV} 8 9ENV NEXT_PUBLIC_API_ENV=${API_ENV} 10 11# Create app directory 12RUN mkdir -p /usr/src/app 13WORKDIR /usr/src/app 14 15# Install app dependencies 16COPY package*.json /usr/src/app/ 17RUN npm install 18 19# Bundle app source 20COPY . /usr/src/app 21 22RUN npm run build 23EXPOSE 3000 24 25CMD [ "npm", "run", "start" ] 26

在 coding 上设置代码 push 触发规则,触发生成制品库。

使用 redux

https://github.com/vercel/next.js/tree/canary/examples/with-redux

https://github.com/vercel/next.js/tree/canary/examples/with-redux-thunk

里面有使用到一个 js 新特性Nullish coalescing operator

Next.js 踩坑

环境变量配置

环境变量在客户端无法获取,背景:由于我在项目中需要根据环境变量来使用不同环境的 API 域名。

解决方案:官方提供了以NEXT_PUBLIC_开头的环境变量名,这样就可以在客户端和服务端都访问得到环境变量了。