从0开始微操SSR之nextjs项目实践

标签: 微操 ssr nextjs | 发表时间:2020-10-02 18:35 | 作者:phone_t
出处:https://juejin.im/frontend

节目开始先放一张图片:

这是一个电商类的网站,对于这种电商类的网站,我们一般会想要的效果是:

1、用户从百度或者一些搜索引擎直接搜索关键字,就能够导流到我的网站上,这就是SEO(Search Engine Optimization)。关于SEO百度百科的解释是: SEO(Search Engine Optimization):汉译为搜索引擎优化。是一种方式:利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。目的是让其在行业内占据领先地位,获得品牌收益。很大程度上是网站经营者的一种商业行为,将自己或自己公司的排名前移。
2、首页打开的速度足够快,这里的足够快,指的是打开首页的时候页面上的数据已经从服务端获取,而不是还需要异步去请求接口。

以上的两点要求,就是SSR(Server Slider Rendering,即服务端渲染)主要解决的两个问题。而关于SSR更多的内容,网络上已经有太多的资源介绍了,本文不再赘述。本文主要介绍React技术栈中实现SSR架构的解决方案--nextjs,是怎么从0构建我们的网站的。关于nextjs的更多文档请看: www.nextjs.cn/

项目背景

本项目主要实现一个电商项目,主要包括的模块如下图所示:

一般用户可以浏览首页上商品和推荐的商户(见开始的图片),也可以点击商户或商品进入到详情里面,但是下单的时候要检查用户是否登录,如果没有登录则要必须登录才能下单。

代码结构

1、目录结构

直接放截图吧:

2、简单说明

针对几个比较重要的文件夹和文件简单说明一下:

   api ------------- 后端接口相关文件夹
build ----------- 编译出来的资源文件夹
components ------ 抽离的公用组件夹
pages ----------- 前端页面文件夹,根据pages文件结构,生成页面地址
static ---------- 前端图片资源存放文件夹
.babelrc -------- antd按需加载配置文件
next.config.js -- nextjs项目配置文件(项目中重要的配置文件)
复制代码

3、pages文件说明

对于next项目来说,有两种实现路由:

  • 1、页面路由使用的是文件系统路由。这是默认提供,我认为也是最简单的方式。那就是把pages文件夹的结构就当做路由的配置,我这个项目也是基于此来搞的。什么意思呢?就是比如pages文件夹下有的index.js,默认就是项目的首页地址,对应'/',如果有一个about.js,那么其对应的地址就是:'/about',如果要把某些页面归总到pages下面的文件下,比如有个user,下面有login.js,那么login地址就是:'user/login',依次类推。反正新增一个页面,就在pages下或pages下的某个文件夹下新增一个js文件就好,简单粗暴。
  • 2、除了页面路由之外,还提供了所谓的api路由,这种路有相当于写接口的maock数据测试用的,根据自己情况选择就好(我觉得可能一般用不上)具体的可以看看官网。

主要配置

1、从package.json文件说起

作为项目的最重要最基本的配置文件,我先放出配置,配置项后的注释是为了对配置做说明,实际运行的时候没有注释的:

   {
"name": "web",
"version": "0.1.0",
"private": true,
"scripts": {
  "dev": "next dev",//本地开发时候启动项目的命令,默认是电脑的3000端口
  "build": "next build",//项目编译打包
  "start": "next start", // 部署项目之后启动服务命令(其实就是启动个node服务器)
  "start:dev": "APP_ENV='dev' next dev -p 8090", // 本地开发启动项目命令,电脑端口改为8090端口
  "start:test": "APP_ENV='test'  next start -p 8091",// 测试环境启动服务命令(如果有测试环境部署的话),端口为8091
  "start:prod": "APP_ENV='prod' next start -p 8092"// 生产环境启动服务命令,端口为9092
},
"dependencies": {
  "@ant-design/icons": "^4.1.0",
  "@zeit/next-css": "^1.0.1",
  "@zeit/next-less": "^1.0.1",
  "add": "^2.0.6",
  "antd": "^4.2.3",
  "axios": "^0.19.2",
  "babel-plugin-import": "^1.13.0",
  "less": "^3.11.2",
  "less-vars-to-js": "^1.3.0",
  "lodash": "^4.17.15",
  "next": "9.4.0",//最新版似乎Next 9.5 版本已正式发布
  "next-antd-aza-less": "^1.0.2",
  "next-cookies": "^2.0.3",
  "next-size": "^2.1.0",
  "path": "^0.12.7",
  "react": "16.13.1",
  "react-dom": "16.13.1",
  "webpack-bundle-analyzer": "^3.7.0",
  "webpack-filter-warnings-plugin": "^1.2.1",
  "yarn": "^1.22.4"
},
"devDependencies": {
  "next-compose-plugins": "^2.2.0"
}
}
复制代码

其他的不需要多说,但是针对以上的配置文件的scripts配置,说明一下:因为ssr项目其实相当于项目自带了一个node服务器,就说我们项目开发好后经过编译出来的文件,丢到正式服务器上后,项目要运行起来,需要启动这个node服务器,而此处的启动node服务器的命令就是start命令,如果指定端口,就是采用默认端口。而我们平时本地开发的时候,启动的服务器其实是dev命令,这里与一般的前后端分离的前端项目有所不同。简单的说,就是:

   1、本地开发时的命令:
yarn dev
或者像我上面这儿想要修改一下端口的话
yarn start:dev
2、项目开发完成后:
第一步,编译出资源文件:
yarn build
第二步,启动node服务:
yarn start
或者像我上面这儿如果要部署到测试环境就:
yarn start:test
部署到生成环境
yarn start:prod
复制代码

而关于部署这块,除了直接丢资源文件到服务容器中,启动node服务这种默认的操作之外,还可以做一些加固或者其他操作(比如pm2进程守护等),此处我没有深入研究,不好扩展(主要是懒......)。当然,对于一般不是很大的项目来说,默认的这个操作也够用了。

2、next.config.js文件

   const withLess = require("@zeit/next-less");
const withCss = require("@zeit/next-css");
const withPlugins = require("next-compose-plugins");
const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js");
const path = require('path');
const fs = require('fs');
const lessToJS = require('less-vars-to-js');
const FilterWarningsPlugin = require('webpack-filter-warnings-plugin');

module.exports = withPlugins([withLess,withCss], {
   distDir: 'build',
   publicRuntimeConfig: {APP_ENV: process.env.APP_ENV},
   //1、集成antd插件
   lessLoaderOptions : {//如果是antd就需要,antd-mobile不需要
       javascriptEnabled : true,
       modifyVars: lessToJS(
        //2、修改antd的主题
           fs.readFileSync(path.resolve(__dirname, './util/antd.less'), 'utf8')
       )
   },
   //3、开启css模块化
   cssModules: true,
   cssLoaderOptions: {
       camelCase: true,
       localIdentName: "[local]___[hash:base64:5]",
       getLocalIdent: (context, localIdentName, localName, options) => {
           let hz = context.resourcePath.replace(context.rootContext, "");
           if (/node_modules/.test(hz)) {
               return localName;
           } else {
               return cssLoaderGetLocalIdent(
                   context,
                   localIdentName,
                   localName,
                   options
               );
           }
       }
   },
   webpack(config,{isServer}){
       // Fixes npm packages that depend on `fs` module
       if (!isServer) {
           config.node = {
               fs: 'empty'
           }
       }
       //4、设置不把antd打包进入项目中,减小项目的包大小
       if(config.externals){
           const includes = [/antd/];
           config.externals = config.externals.map(external => {
               if (typeof external !== 'function') return external;
               return (ctx, req, cb) => {
                   return includes.find(include =>
                       req.startsWith('.')
                           ? include.test(path.resolve(ctx, req))
                           : include.test(req)
                   )
                       ? cb()
                       : external(ctx, req, cb);
               };
           });
       }
       config.plugins.push(
           new FilterWarningsPlugin({
               exclude: /mini-css-extract-plugin[^]*Conflicting order between:/
           })
       );i

复制代码

对于这个配置文件,可以说是next项目最重要的配置文件,因为我这个项目集成了antd,所以关键点已经注释出来,不再赘述,如要详细了解next.config.js配置,可到官网上查看: www.nextjs.cn/docs/api-re…

项目代码实现的问题分析

1、怎么算是SSR实现了?

关于这个问题,我们先看一张图:

其实,对于next项目的页面,如果你查看网页源代码,那么你会看到跟一般的没有ssr的前端页面有所不同,页面结构代码和数据是组装在一起的(图中圈起来的部分就是数据),这样的网页,就能SEO时候有较好的权重给予。也就是说,这是从感性上来看的,从理性上来说,要在react项目中做到这点,其实主要依赖于next框架中的一个非常重要的生命周期钩子函数:getInitialProps

关于这个生命周期函数,文档上会说很多,但是其实总结起来就一句话:

把从componentDidMount中发出数据请求的操作,变成从getInitialProps中发出,就实现了SSR。

如果还要加一句就是:

这个getInitialProps只能在pages下的父页面中存在(文件系统路由中的文件),子组件中不能有这个操作。

具体看个例子:

   import React from 'react';
import {
   getStoreBannerApi,
   getStoreGoodsListApi,
   getStoreInfoApi
} from '../api/Api';
import HtmlHead from '../components/HtmlHead';
import MyCarousel from '../components/MyCarousel';
import SearchArea from '../components/SearchArea';
import StoreGoods from '../components/StoreDetailPage/StoreGoods';
import StoreLogo from '../components/StoreDetailPage/StoreLogo';


/**
* 商户详情页面(这必须是在pages文件夹下面的文件)
*/
const StoreDetail = ({
                        goodsList, //组件的这些属性都是从getInitialProps返回的对象中取出的
                        storeInfo,
                        banners
                    }) => {
   return (
       <>
           
           
           {/*    
*/} { banners.length ? : null }
{/*StoreGoods是子组件,其是没有getInitialProps生命周期的*/}
); }; // 1、如果是函数组件,调用getInitialProps的方式是这样的 StoreDetail.getInitialProps = async (props) => { try { //2、从路由中传过来的参数获取 const {storeId} = props.query; //3、发出异步请求 const res = await getStoreGoodsListApi({ page: 0, size: 9999, storeId: storeId }).catch(e => ({})); const res1 = await getStoreInfoApi(storeId).catch(e => ({})); const banners = await getStoreBannerApi(storeId).catch(e => ({})); // 4、每个getInitialProps必须返回key-value的对象,这个对象直接注入到组件的props中 return { goodsList: res.code === 20000 ? res.data : [], storeInfo: res1.code === 20000 ? res1.data : [], storeId: storeId, banners: banners.code === 20000 ? banners.data : [], }; } catch (e) { } }; export default StoreDetail; 复制代码

上面代码中要注意第1点,函数组件对getInitialProps钩子函数的调用跟class组件写法上有所不同,class组件的写法如下:

   import React from 'react'

class Page extends React.Component {
 static async getInitialProps(ctx) {
   const res = await fetch('https://api.github.com/repos/vercel/next.js')
   const json = await res.json()
   return { stars: json.stargazers_count }
 }

 render() {
   return     
Next stars: {this.props.stars}
} } export default Page 复制代码

2、关于接口请求的问题?

在ssr项目中,首先要明确一点,一个数据的请求有可能发生在浏览器端,也有可能发生在服务端(node端,node在这儿其实是一个中间层,我们要数据还是从一个真正的后端服务器去要,比如java写的)。在浏览器端我们无需多说,直接ajax就好,但是node端有个问题,没有ajax对象,所以我们要使用一种既可以在浏览器,也可以在node端使用的规范,去发请求。在此我比较推荐的是axios。本项目中使用的axios实例封装代码如下:

   import { message } from 'antd';
import axios from 'axios';
import Router from 'next/router';
import { clearLoginStorage, getToken } from './saveLogin';
//1、服务端调接口需要用户认证时的token,初始化渲染的时候给其赋值的
export let serverAuthorization = Object.create(null);

const request = axios.create({
   baseURL: 'http://139.9.113.127:8080',
   timeout: 30000,
});

// 拦截器
request.interceptors.response.use((response) => {
   // console.log(response)
   let resObj = {};
   if (response.data && response.data.code === 20000) {
       /*if (process.browser) {
           resObj = {
               code: response.data.code,
               data: response.data.data
           }
       } else { //服务端,直接返回数据
           resObj = {...response.data.data}
       }*/
       resObj = {
           code: response.data.code,
           data: response.data.data
       };
   } else {
       resObj = {
           code: response.data.code,
           message: response.data.message,
           data: null
       };
       /*if (process.browser) {
           resObj = {
               code: response.data.code,
               message: response.data.message,
               data: null
           }
       }*/
   }
   return resObj;
}, (error) => {
   const res = error.response || {};
   console.log(res.status);
   if (process.browser && res.status === 401) { //客户端渲染的时候才需要,登录过期,跳转首页
       // console.log('---->');
       clearLoginStorage();
       message.error('登录过期,请重新登录');
       Router.replace('/user/login');
   }
   return Promise.reject(error);
});
request.interceptors.request.use((config) => {
   //const token = process.browser //(getToken() || {}) : serverAuthorization;
  // console.log(config.url);
   let auth = '';
   //2、区分是浏览器端发出的请求还是node端发出的请求
   if (process.browser) {
       auth = getToken()['access_token'] || '';
   } else { // 3、服务端请求,从request中拿出token
       auth = '';//request['access_token'] || '';
   }
   return {
       ...config,
       headers: {
           Authorization: `${auth ? 'Bearer ' + auth : ''}`,
           Accept: 'application/json',
           'Content-Type': 'application/json; charset=utf-8',
       },
       //data: config.param ? JSON.stringify(config.param) : ''

   };
}, (error) => {
   return Promise.reject(error);
});

export default request;

复制代码

以上代码比较简单,就不多说了。

3、关于token同步的问题?

关于这个问题,其实跟第2点有点相似。其实上面我们说到的getInitialProps生命周期函数会在服务端渲染和非服务端渲染的时候都去执行,这样我们就不用专门去关注什么时候该SSR还是非SSR。但是这就造成了一个问题,因为我们的token是缓存在浏览器端的(此项目保存在session中),所以,当请求是node端发出的时候,我是根本拿不到浏览器缓存的token的,而如果此时这个由node发出的请求,又需要token的话,该怎么办呢?在本项目中,采用了一个比较简单粗暴的方式。在pages文件夹下的_app.js文件,这相当于是项目全局的入口配置文件:

   import { ConfigProvider } from "antd";
// import zhCN from 'antd/es/locale/zh_CN';
import zhCN from 'antd/lib/locale/zh_CN';
import App from 'next/app';
import getCofnig from 'next/config';
import Router from 'next/router';
import React from 'react';
import LayoutBasic from '../components/Layout/BasicLayout';
import UserLayout from "../components/Layout/UserLayout";
import '../static/styles/base.less';
import request from "../util/request";
import { getToken } from "../util/saveLogin";

Router.events.on('routeChangeComplete', () => {
   if (process.env.NODE_ENV !== 'production' && document) {
       const els = document.querySelectorAll('link[href*="/_next/static/css/styles.chunk.css"]');
       const timestamp = new Date().valueOf();
       els[0].href = '/_next/static/css/styles.chunk.css?v=' + timestamp;
   }
})

const {serverRuntimeConfig, publicRuntimeConfig} = getCofnig()

// console.log(serverRuntimeConfig, publicRuntimeConfig)

class NextApp extends App {

   //服务端渲染调用的方法
   static async getInitialProps({Component = {}, ctx}) {
       const token = getToken(ctx) || {};
       // 需要将token赋值给服务端请求
       request['access_token'] = token.access_token;
       let pageProps;
       if (Component.getInitialProps) {
           pageProps = await Component.getInitialProps(ctx);
       }
       return {
           pageProps,
       };

   }

   render() {
       const {Component, pageProps, store, router = {}} = this.props;
       const {pathname} = router;
       let LayOut = (
           
               
           
       )
       if (pathname.indexOf('/user/') === 0) { //用户登录模块
           LayOut = (
               
                   
               
           )
       }
       return (
           
               {LayOut}
           
       )
   }
}

export default NextApp;

复制代码

关键是看如下这句代码:

    // 需要将token赋值给服务端请求
 request['access_token'] = token.access_token;
复制代码

因为这个文件是全局入口文件,之前说过,getInitialProps生命周期函数可以在服务端和浏览器端执行,那么我在浏览器端执行的时候将浏览器缓存的token取出,放入到axios请求的实例对象的access_token中,因为node发的请求也要用这个实例,那么就可以从里面取出token放入node发出的请求参数中了,这样就达到了一个token在浏览器端和node端都同步的问题。。。

总结

以上总结一下:

  • SSR对于页面加载(特别是首页加载)和SEO优化具有好处
  • nextjs是react技术栈实现SSR方式的非常有名的框架,上手也比较简单,基本上跟着文档走就好
  • nextjs实现SSR关键在于getInitialProps生命周期函数的使用,其他的跟编写一般前端应用没啥区别

当然,以上其实只是对nextjs的项目实践梳理了一些关键点,让项目能跑起来,能够上手。其实关于SSR这块的东西,大概看起来东西不多,但是真正实践起来其实有很多细节需要考虑的,比如加入express或koa包装中间层,比如token同步这块,感觉文中所述不是一个很好的方式,可能在node端做一些token缓存或者校验会更好等等,可能后续会做更多研究,也希望有大佬们多给予指教。文档写的有点累,如果对你有所启发,求鼓励三连

项目源码: github.com/phonet/next…

相关 [微操 ssr nextjs] 推荐:

从0开始微操SSR之nextjs项目实践

- - 掘金前端
这是一个电商类的网站,对于这种电商类的网站,我们一般会想要的效果是:. 1、用户从百度或者一些搜索引擎直接搜索关键字,就能够导流到我的网站上,这就是SEO(Search Engine Optimization). SEO(Search Engine Optimization):汉译为搜索引擎优化. 是一种方式:利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名.

助力ssr,使用concent为nextjs应用加点料

- - SegmentFault 最新的文章
开源不易,感谢你的支持, ❤ star concent^_^. 这里我们将使用 create-next-app命令来安装一个基础的next示例应用. 执行完毕后,可以看到一个如下的目录结构. |____public |____pages | |____ _app.js // next应用默认的根组件 | |____index.js // 默认首页 | |____api.

当 SSR 遇上 Serverless,轻松实现页面瞬开

- -

带你五步学会Vue SSR - 前端学习 - SegmentFault 思否

- -
SSR大家肯定都不陌生,通过服务端渲染,可以优化SEO抓取,提升首页加载速度等,我在学习SSR的时候,看过很多文章,有些对我有很大的启发作用,有些就只是照搬官网文档. 通过几天的学习,我对SSR有了一些了解,也从头开始完整的配置出了SSR的开发环境,所以想通过这篇文章,总结一些经验,同时希望能够对学习SSR的朋友起到一点帮助.

ssr vuejs/vue-hackernews-2.0: HackerNews clone built with Vue 2.0, vue-router & vuex, with server-side rendering

- -
This is a demo primarily aimed at explaining how to build a server-side rendered Vue app, as a companion to our SSR documentation. #install dependenciesnpm install#or yarn#serve in dev mode, with hot reload at localhost:8080npm run dev#build for productionnpm run build#serve in production modenpm start.

React服务端渲染(ssr)之Next.js框架

- - 掘金前端
Nextjs是 React生态中非常受欢迎的SSR(server side render——服务端渲染)框架,只需要几个步骤就可以搭建一个支持SSR的工程(_Nextjs_的快速搭建见 Next.js入门). 本文的案例代码来自于 前端标准模板项目. 提供了便捷强大的服务端渲染功能—— getInitialProps(),通过这个方法可以简单为服务端和前端同时处理异步请求数据:.

电信 ss/ssr 速度慢 电信国际出口速度慢 被 QoS 限速

- - DiyCode - 致力于构建开发工程师高端交流分享社区社区
很多人跟我反应,同一条线路, 电信用户的国际出口速度很慢,而移动/联通用户却还不错,可能移动/联通可以流畅看1080P,而电信卡的连国外网页都打不开. 明明电信的国际出口宽带是三家中最高的,为什么只有电信的速度慢呢. 本文简单分析下电信运行商慢的原因( QoS限速),并推荐下针对电信用户优化的CN2线路来提升国际出口速度.

能让你纵享丝滑的SSR技术,转转这样实践

- - 掘金 前端
秒开率对于用户的留存率有直接的影响,数据表明, 网页加载时间过长会直接导致用户流失.转转集团作为一家电商公司, 对于H5页面的秒开率有着更加严格的需求, 在主要的卖场侧页面(手机频道页、3c频道页、活动页)等重要流量入口我们都采用了SSR(服务端渲染)技术来构建页面,今天就带大家了解一下我们摸索出来的一些最佳实践..

愚蠢的人类啊!在AI的极限微操下颤抖吧![v]

- YiLeuang - 煎蛋
这两个视频是由名叫“Automaton 2000”的微操bot演示的微操极限. 第一个视频: 20机枪兵,对战40自爆虫. 机枪兵们啃兴奋剂且战且退,将游击战发挥到极致,无一死亡全歼自爆虫;. 第二个视频: 100小狗,对战20辆架了炮的坦克. 如果一堆狗冲过去的话只能干爆两辆坦克,该怎样微操呢. Read the rest of 愚蠢的人类啊.