SSR之next.js--篇一

4,784 阅读7分钟

1、名词解释

       SSR(server-side-render),根据名称我们都知道叫做服务端渲染,我相信对于工作几年的前端开发者肯定很熟悉这个名词,多少也都用过,在这里,我想说的是针对咱们刚刚踏入前端圈的朋友们的一点总结和感悟,希望我们能够不断的提高对next的认知,相互进步,相互成长!这就是我们的目标~
  • 服务端渲染就是页面的渲染和生成由服务器来完成,并将渲染好的页面返回客户端
  • 客户端渲染就是页面的生成和数据的渲染过程是在客户端(或浏览器或APP)完成

其中掘金就使用vue的ssr功能做了全栈服务端渲染,切效果良好。

那我们用next.js的优点是什么呢?

2、next.js的特性

  • 默认服务端渲染模式,以文件系统为基础的客户端路由
  • 代码自动分割使得页面加载更快
  • (以页面为基础)的简介的客户端路由
  • 以webpack的热替换为基础的开发环境
  • 使用React的JSX和ES6的module,模块化和维护更方便
  • 可以运行在Express和其他node.js的http服务器上
  • 可以定制化专属的babel和webpack配置
那我们开始上手吧,hello world!

3、hello world

照做以下步骤吧~


接着打开package.json添加如下代码,如下:

{
  "scripts": {
    "dev": "next"
}}

到这里位置项目的准备工作已完成,运行以下命令开启项目服务器

npm run dev

执行完毕后,在浏览器打开localhost:3000,就会看到以下页面:


这是next.js默认生成的404页面,儿开启服务后访问的之所以是现在的404页面,是因为我们还没有设置项目主页。

那接下来就是创建页面了~~~啦啦啦~~~我学next我开心~~~

4、创建页面

next.js是从服务器生成页面,再返回给前端展示。

重点内容开始了哦~~~~

  • next.js默认从pages目录下取页面进行渲染返回给前端展示,并默认取pages/index.js为系统是首页进行展示
  • 注意,pages是默认存放页面的目录,路由的根路径也是pages目录。

我们在pages/index.js中创建一个React函数式组件:

const Index = () => (
    <div>
        <p>小英 study next.js</p>
    </div>
)
export default Index

next.js默认使用webpack构建项目,webpack的热部署功能一样能提升开发效率。创建完pages/index.js后,再访问http://localhost:3000即可看到设置好的页面。


很好,不在是我们不喜欢的404了,看到内容了啊~~

再来构建下多页面的吧~

使用next.js的目的就是构建非SPA的多页面项目,下面开始创建第二个页面。

在pages目录下创建文件pages/content.js

export default () => (  
  <div>    
      <p>这是content 页面</p> 
   </div>
)

打开路由localhost:3000/content,看到我们新建的第二个页面了。


所以现在我们明白所有的路由都是通过后端服务器来控制的 ,要想实现客户端路由,需要借助next.js的Link API,所以接下来我们看下Link API吧~~

5、Link API

从next/link中可以引用到Link组件。在pages/index.js文件中引用Link,修改如下:

import Link from "next/link"
const Index = () => (  
    <div>    
        <Link href="/content">     
             <a>去往content页面</a>    
        </Link>    
        <p>小英 study next.js</p>  
    </div>)
export default Index

我们使用的Link组件,其实可以当做a标签使用,最终渲染的时候也是a标签,页面渲染如下:


点击超链接,即可进入到content页面。下图是实际渲染的结果:


结论:link组件是通过location.history的浏览器API保存历史路由,所以,可以通过浏览器左上角的前进后退按钮来切换历史路由。而在开发过程中,我们不需要单独写客户端路由的配置。


6、组件复用

next.js是以多页面为中心,只要将页面文件放在pages目录下,就可以通过浏览器上以文件名为路由名来访问到,相反,只要不想让用户通过页面直接访问的组件,都不放在pages目录下,开发者想放在哪个目录自定义即可。

接下来,我们建一个components目录,里面建一个公共的Header组件,用于头部导航,通过导航可以再页面间切换。

import Link from 'next/link'
const linkStyle = {  marginRight: 15}
const Header = () => (  
    <div>    
        <Link href="/">     
             <a style={linkStyle}>首页</a>    
        </Link>    
        <Link href="/content">      
            <a style={linkStyle}>内容</a>    
        </Link>  
    </div>)
export default Header

好了,定义完组件,我们在pages/index.js里面引用看一下:

import Header from '../components/Header.js'
const Index = () => (  
    <div>    
        <Header />    
        <p>小英 study next.js</p>  
    </div>)
export default Index

在 pages/content.js 中同样引入 Header 组件,在浏览器上通过点击导航切换页面。

import Header from '../components/Header.js'
export default () => (  
  <div>    
    <Header/>    
    <p>这是content 页面</p> 
 </div>)

这样就可以再index和content两个页面来回切换了


进一步我们在封装下Header组件,Layout组件,包含我们的Header和页面content的组件;

/**Layout**组件/
import Header from './Header'
const layoutStyle = {  margin: 0,  padding: 0,  border: '1px solid #DDD'}
const Layout  = (props) => (  
    <div style={layoutStyle}>    
        <Header/>    
        {props.children}  
    </div>)
export default Layout

/**在index中使用Layout组件**/
import Layout from '../components/Layout.js'
import Link from 'next/link'
const linkArr = [  
    '我是链接1',  
    '我是链接2',  
    '我是链接3',  
    '我是链接4',  
    '我是链接5',  
    '我是链接6',]
const LinkContent = (props) => ( 
     <li>    
        <Link href={`tolink?title=${props.title}`}>    
            <a>{props.title}</a>    
        </Link> 
     </li>)
const Index = () => (  
    <Layout>    
        <h1>这是我的地盘</h1>    
        <ul>    
            {      
                linkArr.map(item => {        
                    return  <LinkContent title={item} />      
                })   
             }    
        </ul>  
    </Layout>)
export default Index

页面渲染如下:(以下相当于是列表页,点击列表中的链接相当于到详情页,以下就形成了动态页面之间的跳转)


/**详情页**/
import Layout from '../components/Layout.js'export default (props) => (  <Layout>    <h1>{props.url.query.title}</h1>    <p>这是不同title对应的详情页</p>  </Layout>)


总结:使用next.js创建动态页面,与使用React和Vue创建一个spa的页面大体相同,区别就是页面的渲染主题不同
  • 前者是nodejs服务器获取到后端数据渲染完页面后再返回给前端展示
  • 后者是前端先获取页面主体架构,再通过ajax的方式请求后端的数据,在前端渲染展示

7、Route Masking

next.js提供一个独特的特性:路由遮盖(Route Masking)。它可以使得在浏览器上显示的是路由A,而在app内部实际显示的是路由B。这个特性可以使我们的路由简洁,以上边为例,地址栏显示的是http://localhost:3000/tolink?title=%E6%88%91%E6%98%AF%E9%93%BE%E6%8E%A56,这个地址含有个title参数,看着很不整洁吗,接下来我们就要next改造路由,目标是改成http://localhost:3000/p/0

这里我们要用一个Link组件的as属性,并给组件添加一个id属性:(以index为例)

import Layout from '../components/Layout.js'
import Link from 'next/link'
const linkArr = [  '我是链接1',  '我是链接2',  '我是链接3',  '我是链接4',  '我是链接5',  '我是链接6',]
const LinkContent = (props) => (  
    <li>    
        <Link     
           as={`p/${props.id}`}    
           href={`tolink?title=${props.title}`}>   
               <a>{props.title}</a>   
         </Link>  
    </li>)
const Index = () => (  
    <Layout>   
         <h1>这是我的地盘</h1>    
         <ul>    
            {     
             linkArr.map((item,index) => {        
                return  <LinkContent id={index} title={item} />     
             })    
            }    
        </ul>  
    </Layout>)
export default Index


But。。。。我们刷新下页面,咦怎么404了呢???


结论:

  • 当在 Link 组件上使用 as 属性时,浏览器上显示的是 as 属性的值,走的是客户端路由,而服务器真正映射的是 href 属性的值,走的是服务端路由。
  • 显示 404页面,是因为路由遮盖默认只在客户端路由中有效,要想在服务端也支持路由遮盖,需要在服务端单独设置路由解析的方法。

8、服务端支持路由遮盖

下面以express为例创建后端服务器讲解如何设置服务器来支持路由遮盖。

先来安装一个express

npm install --save express

在项目目录下创建server.js文件

const express = require('express')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {  
    const server = express()     
    server.get('/p/:id', (req, res) => {    
        const actualPage = '/tolink'    
        const queryParam = {      title: req.param.id    }    
        app.render(req, res, actualPage, queryParam)  
    })  
    server.get('*', (req, res) => {    
        return handle(req, res)  
    }) 
    server.listen(3000, (err) => {   
         if(err) throw err   
         console.log('> ready on http://localhost:3000')  
    })}).catch(ex => {  
        console.log(ex.stack)  
        process.exit(1)
    })   

/**index.js**/
import Layout from '../components/Layout.js'
import { withRouter } from "next/router"
class ToLink extends React.Component {
    // 没有该函数是不能实现服务端路由遮盖的  
    static getInitialProps ({ query: { title } }) {  
      return { title } 
     }  
    render() {    
        let { title } = this.props    
        return (    
           <Layout>        
                <h1>{title}</h1>        
                <p>这是不同title对应的详情页</p>      
           </Layout>    
        )  
    }
} 
export default withRouter(ToLink)

这时在tolink页面刷新的时候就能够正常显示页面了,这里强调一点,我在参考一些文章的时候,没有提到加 getInitialProps这个方法,就说页面刷新就能正常显示是不正确的,本人多次试验,页面本身的内容是可以正常显示的,但是根据路由获取的title参数是一直拿不到的,加了这个参数就会先去获取参数,这样页面才能完整显示~~~以下提供截图证明:


所以,接下来我们就说说这个静态的方法。

9、接口请求

next.js在react的基础上为组件新增了一个特性:getInitialProps, 同于获取并处理组件的属性,返回组件的默认属性。我们可以在该方法中请求数据,获取页面需要的数据并渲染返回给前端页面。(上边的服务端路由遮盖也是用了这个方法,原理就是获取组件的默认属性

引入一个支持在客户端和服务端发送fetch请求的插件isomorphic-unfrtch, 当然我们也可以用axios等其他工具。

npm install --save isomorphic-unfetch

然后修改pages/index.js:

import Layout from '../components/Layout.js'
import { withRouter } from "next/router"
import fetch from 'isomorphic-unfetch'
import Link from 'next/link'

const LinkContent = (props) => {  
    console.log(props)  
    return (    
        <li>      
            <Link      
                 as={`/p/${props.id}`}      
                 href={{pathname: '/tolink', query: {title: props.title}}}> 
             <a>{props.title}</a>      
            </Link>   
         </li> 
   )
}
class Index extends React.Component {  
    static async getInitialProps(){    
        const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')  
        const data = await res.json()      
        console.log(`Show data fetched. Count: ${data.length}`)      
        return {      
            shows: data    
        }  
     }  
    render() {    
        const { shows } = this.props    
            return(      
                <Layout>      
                    <h1>这是我的地盘</h1>     
                    <ul>     
                         {        
                            shows.map(( { show } ) => {   
                               return  <LinkContent key={show.id} id={show.id} title={show.name} />   
                             })      
                          }                          
                    </ul>   
                 </Layout>)    
   }
}
export default withRouter(Index)

在getInitialProps中我们使用async await关键字异步获取我们需要的参数shows,这样数据就是动态获取的,从而实现动态渲染。注意:getInitialProps不能使用在子组件中。只能使用在pages页面中。

结论如下:
  • 当页面渲染时加载数据,我们使用了一个异步方法getInitialProps。它能异步获取 JS 普通对象,并绑定在props上
  • 当服务渲染时,getInitialProps将会把数据序列化,就像JSON.stringify。所以确保getInitialProps返回的是一个普通 JS 对象,而不是Date, Map 或 Set类型。
  • 当页面初始化加载时,getInitialProps只会加载在服务端。只有当路由跳转(Link组件跳转或 API 方法跳转)时,客户端才会执行getInitialProps。


结论3在这里给大家做个验证:

servert.js, 这是我在server.js中添加的一个测试id,只有在服务端渲染的时候,我们的tolink页面才会获取的这个id


pages/tolink.js,我们在该页面的 getInitialProps方法中打印一下id


这个结果是我在刷新tolink页面,或者初始化加载的时候,在终端(服务端)打印的结果,有“测试id”这几个字;


继续往下看:


这是我通过首页的列表Link的方式点击进来这个页面,在浏览器控制台打印的结果,很明显这是客户端走的这个方法,是没有”测试id“四个字的,而是undefined,所以这也就验证了结论3的正确性~~~~,(其实我刚开始也是不懂这个意思,现在明白了,希望正在看的你也能明白哦~~)

10、部署

Next.js 项目的部署,需要一个 Node.js的服务器,可以选择 Express, Koa 或其他 Nodejs 的Web服务器。本文中以 Express 为例来部署 Next 项目。

为了区分部署环境,我们需要在 package.json 中修改 script 属性如下:

"scripts": {
  "build": "next build",
  "start": "NODE_ENV=production node server.js -",
  "dev": "NODE_ENV=dev node server.js"
}

其中,build 命令是用于打包项目,start 命令是用于生产环境部署,dev 命令是用于本地开发。

执行如下命令即可将 Next项目 部署到服务器:

npm run build
npm run start

执行完命令后,可在 http://host:3000 访问。其中,host 是指服务器的IP地址。

总结:next.js部署生成环境,必须用build命令打包构建,然后再用start命令部署。


next.js还有很多内容需要我们不断总结和采坑,未完待续。。。。。。😃😄