美文网首页
如何快速搭建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