初识nextjs
Next.js是一个基于react的服务端渲染框架。提供生产环境所需的所有功能以及最佳的开发体验:包括静态及服务器端融合渲染、 支持 TypeScript、智能化打包、 路由预取等功能 无需任何配置。
主要功能:
- 服务器端渲染(默认)
- 自动代码切分, 加速页面加载
- 简单的客户端路由(基于页面)
- 基于Webpack的开发环境, 支持热模块替换(HMR: Hot Module Replacement)
- 可以使用Express,koa或其他Node.js服务器实现
- 使用Babel和Webpack配置定制
基于react渲染的SSR框架nextjs,基于vue渲染的SSR框架nuxtjs
什么是服务端渲染
首先要清楚一个渲染的概念:渲染即是数据与模版组装成html;
为了更好的理解服务端渲染,我们可以将服务端渲染与客户端渲染对比着来看。
- 客户端
前端做视图和交互,后端只提供接口数据,前端通过ajax向服务端请求数据,获取到数据后通过js生成DOM插入HTML页面,最终渲染给用户。页面代码在浏览器源代码中看不到。 - 服务端
服务端在返回html之前,在特定的区域,符号里用数据填充生成html,再发送给客户端html,客户端解析html最终渲染出页面给用户,页面代码在浏览器源代码中看得到。 -
对比
本质上两种渲染都是一样的,都是进行的字符串拼接生成html,两者的差别最终体现在时间消耗以及性能消耗上。客户端在不同网络环境下进行数据请求,客户端需要经历从js加载完成到数据请求再到页面渲染这个时间段。导致了大量时间的消耗以及浏览器性能的消耗。而服务端在内网请求,数据响应快,不需要等待js代码加载,可以先请求数据再渲染可视部分然后返回给客户端,客户端再做二次渲染,这样大部分消耗的是服务端的性能。客户端页面响应时间也更快。
渲染路线图:
渲染路线图
为什么需要服务端渲染
- 首屏加载快
客户端渲染下,除了加载html,还要等待js/css加载完成,之后执行js渲染出页面,这个期间用户一直在等待,而服务端只需要加载当前页面的内容,而不需要一次性加载全部的 js 文件。等待时间大大缩短,首屏加载变快。 - 利于SEO优化
服务端渲染出的页面有助于搜索引擎识别页面内容,有利于SEO, 所谓SEO,指的是利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。对于客户端渲染来说,搜索引擎并不能收录到 ajax 爬取数据之后然后再动态 js 渲染出来的页面。而服务端渲染的页面代码都可以在源代码中看到,这有助于搜索引擎识别。
总的一句话:实际开发根据实际场景。
接下来我们来动手搭建一个基于nextjs+react+ts+antd的服务端渲染框架。
项目创建
- 使用脚手架
官方建议使用 create-next-app创建新的 Next.js 应用程序,它会自动为你设置所有内容,create-next-app文档
// 使用ts开发项目,可以通过``--typescript``参数创建ts项目
npx create-next-app --typescript
// or
yarn create-next-app
- 手动安装
//1、安装项目所需
npm install next react react-dom
//or
yarn add next react react-dom
//2、package.json中添加script配置
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
目录结构
//脚手架生成的目录
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── hello.ts
│ └── index.tsx
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── styles
│ ├── Home.module.css
│ ├── common.css
│ ├── common.less
│ └── globals.css
└── tsconfig.json
//改造之后的目录
.
├── api //接口
│ ├── client.js
│ └── server.js
├── axios //请求封装
│ ├── host.js
│ ├── http.js
│ └── index.js
├── components //组件
│ ├── header-custom
│ ├── index-component
│ ├── layout.jsx
├── constants //公共
│ └── menus.js
├── ecosystem.json //pm2发布配置
├── less //公共样式
│ ├── br-common.less
├── lib //公共js
│ ├── cookie.js
│ └── mockuser.js
├── next-env.d.ts
├── next.config.js //next默认配置,可增加webpack配置等
├── package.json
├── pages //页面即路由
│ ├── _app.js
│ ├── _error.js
│ ├── index
│ └── login
├── server.js
├── tsconfig.json
└── yarn-error.log
路由
Next.js 是围绕着 页面(pages) 的概念构造的。一个页面(page)就是一个从 pages
目录下的 .js
、.jsx
、.ts
或 .tsx
文件导出的 React 组件,例如:pages/about.js 被映射到 /about
//pages/about.js
function About() {
return (
<p>
welcome nextjs!
</p>
)
}
export default About
1、link路由切换
import Link from 'next/link';
<Link href="/detail?id=123">
<a>Detail</a>
</Link>;
2、router切换
import Router from 'next/router';
Router.push('/index')
引入antd,使用css
引入antd公共的css(或者是import外部css,less等),需要安装@zeit/next-less,@zeit/next-css,并在next.config.js引入,tips:原因是less3.0之后,默认不开启内联JavaScript,需要传入{ javascriptEnabled:true }手动开启
如下图:
const withLess = require('@zeit/next-less')
const withCss = require('@zeit/next-css')
const config = {
//1、集成antd插件
lessLoaderOptions: {//如果是antd就需要,antd-mobile不需要
javascriptEnabled: true //覆盖默认webpack配置
},
}
module.exports = withLess(withCss(config))
获取数据
使用一个 async 函数 getInitialProps 来获取数据;自动区分服务端执行or客户端执行;
在当前路由刷新才会在服务端执行,如果是从其他路由跳转过来的,没有刷新页面就会在浏览器端执行的;
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async (ctx) => {
const res = await fetch('https://api.github.com/repos/vercel/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
export default Page
自定义APP
Next.js 使用 App 组件来初始化页面。你可以覆盖该 App 组件并控制页面的初始化,比如:切换页面保持页面布局、添加全局css、向页面注入数据等;
要覆盖默认的APP,需要再pages目录下创建_app.js文件;
//引入公共样式,公共布局,向页面注入公共数据
import { useRouter} from "next/router";
import { useEffect } from 'react'
import UserLogin from '../pages/login/index';
import LayoutBasic from '../components/layout';
import "antd/dist/antd.css";
function App({ Component, pageProps,USERINFO }){
const router = useRouter()
useEffect(() => {
if (!USERINFO) {
router.push('/login')
}
}, [USERINFO])
const { pathname } = router;
// 判断渲染登录
let layout = (
<LayoutBasic userInfo={USERINFO}>
<Component {...pageProps} {...USERINFO} />
</LayoutBasic>
);
if (pathname == '/login') {
layout = (
<UserLogin>
<Component {...pageProps} />
</UserLogin>
);
}
return (
<>
<style global jsx>
{`
#__next,
.ant-layout {
min-height: 100vh;
}
`}
</style>
{layout}
</>
);
}
App.getInitialProps = async ({ Component, router, ctx }) => {
let pageProps = {};
let USERINFO = {};
USERINFO = ctx.userInfo = {name:'zj'};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps, USERINFO };
};
export default App
next.config.js
自定义webpack配置、打包编译目录、重定向、环境变量等。
// webpack配置
// css跟less 并存使用
const path = require('path');
const withLess = require('@zeit/next-less')
const withCss = require('@zeit/next-css')
const {
PHASE_DEVELOPMENT_SERVER,
PHASE_PRODUCTION_BUILD,
} = require('next/constants')
module.exports = (phase)=>{
//自定义环境变量,在package.json中设置不生效,由于next build会执行线上编译
const isDev = phase === PHASE_DEVELOPMENT_SERVER
const isDaily = phase === PHASE_PRODUCTION_BUILD && process.env.STAGING !== '1'
const isProduction =
phase === PHASE_PRODUCTION_BUILD && process.env.STAGING === '1'
console.log(`isDev:${isDev} isDaily:${isDaily} isProduction:${isProduction}`)
const nodeEnv = isDev ? 'dev' : isDaily ? 'daily' : 'production';
const config = {
//1、集成antd插件
lessLoaderOptions: {//如果是antd就需要,antd-mobile不需要
javascriptEnabled: true
},
webpack(config, { dev }) {
const webpack = require('webpack')
config.resolve.alias = {
'@components': path.resolve('./components')
}
return config;
},
env: {
NODEENV: nodeEnv
},
// distDir: 'build' //打包文件目录
}
return withLess(withCss(config))
}
服务端【如果是动态路由,拦截路由,接口,操作接口返回等可以增加服务端】
/*
* @Descripttion:
* @Author: zj
* @Date: 2021-03-17 16:36:22
* @LastEditors: zj
* @LastEditTime: 2021-06-08 14:13:28
*/
const Koa = require('koa')
const Router = require('koa-router');
const cors = require('koa-cors');
const bodyParser = require('koa-bodyparser')
const logManager = require('@bairong/lib-util/logger');
const log = logManager.getLogger('systemService');
const next = require('next')// nextjs 作为中间件
const dev = process.env.NODE_ENV == 'dev' ? true : false; //true开启热更新,false不开启
console.log('环境变量',process.env.NODE_ENV,dev);
const app = next({ dev })// 初始化 nextjs,判断它是否处于 dev:开发者状态,还是production: 正式服务状态
const handler = app.getRequestHandler()// 拿到 http 请求的响应
app.prepare().then(() => {
const server = new Koa()
const router = new Router();
/** 这是 Koa 的核心用法:中间件。通常给 use 里面写一个函数,这个函数就是中间件。
* params:
* ctx: Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为请求上下文对象
* next: 调用后将执行流程转入下一个中间件,如果当前中间件中没有调用 next,整个中间件的执行流程则会在这里终止,后续中间件不会得到执行
*/
server.use(
bodyParser({
enableTypes: ['json', 'form', 'text']
})
);
server.use(cors()); //处理跨域
router.get('/login', async (ctx) => {
await app.render(ctx.req, ctx.res, '/login', ctx.query)
ctx.respond = false
})
router.all('(.*)', async (ctx) => {
await handler(ctx.req, ctx.res)
ctx.respond = false
})
server.use(async (ctx, next) => {
ctx.res.statusCode = 200
await next()
})
server.use(router.routes());
server.listen(3080, () => {
log.info(`🌎 服务已启动 server is running at http://localhost:3080`)
})
})
构建、部署
使用pm2发布并做进程守护,首先安装pm2,然后根目录新建ecosystem.json文件,然后修改package.json
sudo npm i pm2
//ecosystem.json
{
"apps": [
{
"name": "demo", //项目名称
"script": "server.js" //执行的文件
}
],
"deploy": {
"production": { //线上
"user": "zj", //用户名
"host": [
"172.18.30.16" //发布的服务器ip
],
"ref": "origin/master",
"repo": "git@192.168.23.221:fed/demo.git",
"ssh_options": "StrictHostKeyChecking=no",
"path": "/opt/demo",
"post-deploy": "git pull origin master && npm install --registry=http://registry.shurongdai.cn/ && npm run productionpush",
"env": {
"NODE_ENV": "production"
}
},
"daily": { //日常
"user": "zj",
"host": [
"172.18.31.121"
],
"ref": "origin/deploydaily", //拉取分支
"repo": "git@192.168.23.221:fed/demo.git",
"path": "/opt/demo",
"post-deploy": "git pull origin deploydaily && npm install --registry=http://registry.shurongdai.cn/ && npm run dailypush",
"env": {
"NODE_ENV": "daily"
}
}
}
}
//package.json
"scripts": {
"nextdev": "next dev",
"dev": "cross-env NODE_ENV=dev node server.js",
"build": "next build",
"start": "node server.js",
"dailypush": "cross-env NODE_ENV=daily npm run build && cross-env NODE_ENV=daily PM2_HOME='/opt/demo/.pm2' pm2 startOrRestart ecosystem.json",
"productionpush": "cross-env NODE_ENV=production npm run build && cross-env NODE_ENV=production PM2_HOME='/opt/demo/.pm2' pm2 startOrRestart ecosystem.json",
"status": "PM2_HOME='/opt/demo/.pm2' pm2 status",
"log": "PM2_HOME='/opt/demo/.pm2' pm2 log",
"stop": "PM2_HOME='/opt/demo/.pm2' pm2 delete all"
},
网友评论