美文网首页
如何快速搭建SSR服务之 nextjs

如何快速搭建SSR服务之 nextjs

作者: Kelly_zj | 来源:发表于2021-06-23 19:51 被阅读0次

初识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"
  },

参考文档

官网
参考地址

相关文章

网友评论

      本文标题:如何快速搭建SSR服务之 nextjs

      本文链接:https://www.haomeiwen.com/subject/xxnyyltx.html