最简单的服务端渲染框架-Next.js

最简单的服务端渲染框架-Next.js

快速入门

Next.js是一个用于React应用的极简的服务端渲染框架。框架中集成了Webpack,Babel等一系列React相关的工具并进行了默认的配置。因此省去了复杂的配置过程,实现了一键搭建开发环境和打包构建。同时提供了自定义配置接口,可以在默认配置的基础上对工具进行自定义配置,满足个性化需求。

安装

使用npm安装: npm install next --save

为了方便的使用next提供的命令,把命令写在package.json文件的scripts中:

{
  "scripts": {
    "dev": "next",  // 运行开发服务器,并监控源代码,具备hod reload功能
    "build": "next build", // 以生产模式打包代码
    "start": "next start"  // 启动Next服务器,可以自定义服务器和端口
    "init": "next init" // 初始化项目,创建基础的文件夹和index页面文件
  }
}

之后,在项目的根目录下创建pages文件夹和static文件夹,分别用来放对应的页面资源和静态资源。

Note:也可以使用npm run init命令自动生成。

运行

如果使用npm run init命令的话,现在pages文件夹下已经有了index.js文件,如果是手动创建pages文件夹的话,现在在该文件下创建一个index.js文件,内容为:

export default () => <p>Hello, world</p>

接着执行npm run dev命令并在浏览器中打开http://localhost:3000

现在,就得到了一个采用服务端渲染的极简React应用,这个应用还实现了自动代码分割,保证每个页面只会加载自身的依赖,不会有依赖冗余。

Next的核心就是pages和static文件夹。其中pages文件夹用于存放每个页面的顶层组件,static用于存放项目中的静态资源。

Next会将pages中的文件结构自动映射为对应的路由结构,例如现在该文件夹下有两个文件:pages/index.js和pages/about.js。则对应的路由分别为//about。并且支持多级目录,例如page/foo/bar.js对应的路由为/foo/bar

static文件夹用来存放静态文件,例如现在有一个图片文件static/image.png,使用的时候引用/static/image.png就可以了:

export default () => (
  <img src="/static/mage.png" />
)

打包完成后,Next会在项目根目录生成一个.next文件夹,其中的两个文件夹distbundlesdist文件夹中存放着编译后的源代码,用于服务端渲染。bunldes文件夹中存放着pages中每个页面打包后的整体代码的JSON格式。在应用的初始页面,会使用dist文件夹中的代码进行服务端渲染,而其他使用路由到达的页面,则将bundles文件夹中的对应JSON格式的代码返回客户端执行渲染。

Next的出现大大简化了React应用开发的配置和构建工作,使开发者能够专注于组件的开发,而不需要在Webpack,Babel等工具上花费过多的精力。基于简单的文件系统,就可以创建包含路由功能和服务端渲染的React应用。需要注意的是:创建的应用中只有初始页面采用服务端渲染,其他通过路由操作到达的页面均为客户福渲染。

组件

Next对React组件的getInitialProps生命周期方法做了改造,传入一个上下文对象,该对象在服务端渲染和客户端渲染时,具有不同的属性:

  • req: HTTP请求对象(服务端渲染独有)
  • res: HTTP响应对象(服务端渲染独有)
  • pathname: URL中的路径部分
  • query:URL中的查询字符串部分解析出的对象
  • err:错误对象,如果在渲染时发生了错误
  • xhr:XMLHttpRequest对象(客户端渲染独有)

因此,可以在组件的getInitialProps方法中处理上下文对象,控制传入组件的props数据。例如:

import React from 'react'
export default class extends React.Component {
  static async getInitialProps ({ req }) {
    return req
      ? { userAgent: req.headers['user-agent'] }
      : { userAgent: navigator.userAgent }
  }
  render () {
    return <div>
      Hello World {this.props.userAgent}
    </div>
  }
}

上面的例子根据是否有req对象来判断是服务端渲染还是客户端渲染,然后采用对应的方式取得用户代码数据并传入组件的props中。

获取数据

组件的getInitialProps还可以用来获取数据:

import React, { Component } from 'react';
import 'isomorphic-fetch';

export default class extends Component {
  static async getInitialProps() {
    const res = await fetch('https://api.github.com/repos/zeit/next.js');
    const json = await res.json();
    return { 
      stars: json.stargazers_count 
    };
    
  }
  
  render() {
    return <div>{this.props.stars}</div>
  }
}

需要注意的一点是,getInitialProps方法执行完毕之后,才会执行组件的render方法。这也就导致了如果网络状况不佳的情况下,会出现长时间的等待。并且只有每个页面的顶层组件的getInitialProps会被执行,所以想在子组件中获取数据的话只能在其他生命周期函数例如componentDidMount配合组件的state实现:

export default class extends Component {
  constructor(props) {
    super(props);
    this.state = {
      stars: 0
    }
  }

  async componentDidMount() {
    const res = await fetch('https://api.github.com/repos/zeit/next.js');
    const json = await res.json();
    this.setState({
      stars: json.stargazers_count
    });
  }
  
  render() {
    return <div>{this.state.stars}</div>
  }
}

CSS

NEXT组件中声明CSS,目前主要有两种方式:

  • 1、内嵌CSS
  • 2、CSS-in-JS

内嵌(Built-in)CSS

Next采用的内嵌CSS方案是styled-jsx库,也是Next所推荐的CSS声明方式。优点是具有组件级的独立作用域,避免了样式污染问题。并且支持完整的CSS功能,如:hover等。

import React from 'react'

export default () => (
  <div>
    Hello world
    <p>scoped!</p>
    <style jsx>{`
      p {
        color: blue;
      }
      div {
        background: red;
      }
      div:hover {
        background: blue;
      }
      @media (max-width: 600px) {
        div {
          background: blue;
        }
      }
    `}</style>
  </div>
)

CSS-in-JS

Next支持多种CSS-in-JS方案,例如基本的在组件style属性中写样式:

import React from 'react'

export default () => (
  <div style={{color: red}}>
    Hello world
  </div>
)

还有其他的CSS-in-JS库,可以根据自己的需要和喜好灵活选择。

路由系统

Link组件

Next中提供了一个组件,用来实现路由功能。例如,我们的应用有两个页面:pages/index.jspages/about.js,想要实现页面跳转,只需要:
<Link>组件的工作流程和浏览器很相似:

  • 1、获取新的组件
  • 2、如果新组件定义了getInitialProps,则获取数据,如果发生错误,则渲染_error.js
  • 3、步骤1,2完成之后,执行pushState并渲染新组件

每个顶层组件中还会传入一个url对象,提供了几个路由相关的方法:

  • pathname:String-当前URL不包括查询字符串的path部分
  • query:Object-当前URL中查询字符串解析成的对象
  • back-后退
  • push(url, as=url)-使用传入的url(字符串)执行pushState操作
  • replace(url, as=url)-使用传入的url(字符串)执行replaceState操作 注意:push和replace方法中的第二个参数as为可选项,只有在服务端配置了自定义路由才有作用。

Router对象

除了使用<Link>组件之外,Next还提供了一个Router对象满足命令式写法的需要:

import Router from 'next/router'

export default () => (
  <div>Click <span onClick={() => Router.push('/about')}>here</span> to read more</div>
)

与url对象相比,Router对象多了一个route属性,值为当前的路由。 需要注意的是,Router对象中的属性和方法仅可以在客户端部分使用,服务端渲染的页面无法使用,否则会报错。

路由事件

Router对象还提供了三个路由事件方法:

  • routeChangeStart(url) - 路由变化开始时触发
  • routeChangeComplete(url) - 路由变化完成时触发
  • routeChangeError(err, url) - 路由变化发生错误时触发 如果使用Router.push(url, as)或相似的方法并传入了as参数,则路由事件方法中的url参数值为as的值,否则,url参数的值是路由舔砖目标的URL

注意:与Router对象中其他的属性和方法不同的是,这三个路由事件方法可以在服务端渲染的页面使用。

监听路由变化:

Router.onRouteChangeStart = (url) => {
  console.log('App is changing to: ', url)
}

取消监听:

Router.onRouteChangeStart = null;

如果路由加载取消了(连续快速点击两个链接),就会触发routeChangeError的回调,传入的err参数中将包含一个cancelled属性,值为true。

Router.onRouteChangeError = (err, url) => {
  if (err.cancelled) {
    console.log(`Route to ${url} was cancelled!`)
  }
}

预获取页面

Next提供了一个基于ServiceWorker实现的,具有预获取页面功能的模块:next/prefetch。 使用预获取功能,可以使APP预加载那些可能到达的页面,提升网站的使用体验和性能。当然,前提是你的浏览器必须支持ServiceWorker。并且预获取功能只支持应用内的页面,不支持外部链接。

<Link>组件

next/prefetch模块也提供了一个具有预获取功能的<Link>组件,代替路由系统中的<Link>组件,使用方法一致:

import Link from 'next/prefetch'

export default () => (
  <nav>
    <ul>
      <li><Link href='/'><a>Home</a></Link></li>
      <li><Link href='/about'><a>About</a></Link></li>
      <li><Link href='/contact'><a>Contact</a></Link></li>
    </ul>
  </nav>
)

此外预获取功能可以精确控制到每个<Link>标签,使用prefetch属性来控制开关:

<Link href='/contact' prefetch={false}><a>Home</a></Link>

prefetch方法

和路由器一样,预获取模块也提供了一个prefetch方法,用来方便命令式的写法:

import { prefetch } from 'next/prefetch'
export default ({ url }) => (
  <div>
    <a onClick={ () => setTimeout(() => url.pushTo('/dynamic'), 100) }>
      100ms后执行路由跳转
    </a>
    {
      预获取页面
      prefetch('/dynamic')
    }
  </div>
)

自定义配置

如果默认的配置无法满足需要的话,Next还提供了诸多的自定义配置接口,可以根据自己的需求灵活配置。

自定义服务器和路由

默认的服务器和路由系统可能无法满足需要,比如,我需要把/a的路由解析到pages/b.js,把/b的路由解析到pages/a.js,此时,就需要通过自定义,手动控制页面渲染来实现,在项目根目录下创建server.js文件:

// server.js

const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true)
    const { pathname, query } = parsedUrl

    if (pathname === '/a') {
      app.render(req, res, '/b', query)
    } else if (pathname === '/b') {
      app.render(req, res, '/a', query)
    } else {
      handle(req, res, parsedUrl)
    }
  })
  .listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})

你可以选择自己喜欢的服务端框架,express或者koa等,进行自定义。

自定义head

Next提供了<HEAD>组件,可以自定义页面<head>标签中的内容。每个组件都可以在内部自定义<head>的内容:

import Head from 'next/head'
export default () => (
  <div>
    <Head>
      <title>My page title</title>
      <meta name="viewport" content="initial-scale=1.0, width=device-width" />
    </Head>
    <p>Hello world!</p>
  </div>
)

每个页面组件只需要定义本页面需要的<head>内容,并且对于相同的标签,例如<title>。会按照组件渲染的顺序,后定义的覆盖先定义的内容。

自定义Document

在前面的例子中,服务端渲染时,所有的页面我们只需要写内容组件,这是因为使用了默认的<Document>模板。当然,可以自定义自己的服务端渲染模板。首先,创建pages/_document.js文件,写上内容:

// pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'

export default class MyDocument extends Document {
  static async getInitialProps (ctx) {
    const props = await Document.getInitialProps(ctx)
    return { ...props, customValue: 'hi there!' }
  }

  render () {
    return (
     <html>
       <Head>
         <style>{`body { margin: 0 } /* custom! */`}</style>
       </Head>
       <body className="custom_class">
         {this.props.customValue}
         <Main />
         <NextScript />
       </body>
     </html>
    )
  }
}

其中的ctx对象与其他组件中的getInitialProps方法中收到的参数一样,只不过多了一个额外的方法:renderPage()

自定义错误处理

Next中,有一个默认组件error.js,负责处理404或者500这种错误。当然,你也可以自定义一个_error.js组件覆盖默认的错误处理组件:

// _error.js

import React from 'react'
export default class Error extends React.Component {
  static getInitialProps ({ res, xhr }) {
    const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
    return { statusCode }
  }

  render () {
    return (
      <p>{
        this.props.statusCode
        ? `An error ${this.props.statusCode} occurred on server`
        : 'An error occurred on client'
      }</p>
    )
  }
}

自定义配置

相对Next进行自定义配置的话,可以在项目根目录下创建一个next.config.js

// next.config.js

module.exports = {
  /* 自定义配置 */
}

自定义Webpack配置

在创建好的next.config.js文件中,可以扩展Webpack配置:

module.exports = {
  webpack: (config, { dev }) => {
   
    // 修改config对象
   
    return config
  }
}

该函数接收默认的Webpack config对象作为参数,返回修改后的config对象。需要注意的是,next.config.js文件会被直接执行,因为只能使用本机安装的Node.js所支持的JS语法。

警告:不建议在自定义Webpack配置中添加loader以支持新的文件类型!因为只有客户端渲染的代码会经过打包,而服务端执行的是源代码,并没有经过Webpack处理,因此新的loader对服务端渲染不起作用。所以最好是使用Babel插件来处理新的文件类型,因为无论是客户端还是服务端渲染的代码,都会经过Babel处理。

自定义Babel配置

自定义Babel配置,只需要在项目根目录下创建.babelrc文件,因为自定义配置会覆盖默认配置,而不是扩展默认配置。因此需要把next preset写到.babelrc中。例如:

{
  "presets": [
    "next/babel",  // Next默认配置
    "stage-0"
  ],
}

部署

生产模式下,需要先使用生产模式构建代码,再启动服务器。因此,需要两条命令:

next build
next start

Next官方推荐使用now作为部署工具,只要在package.json文件中写入:

{
  "name": "my-app",
  "dependencies": {
    "next": "latest"
  },
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

接着运行now命令,就可以实现一键部署。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  •   今天为大家解读的来自美国作家利维的代表作品《快节奏,慢生活》,很多人看到本书的标题,还以为是一本让大家慢下来,...
    南山younger阅读 541评论 0 1
  • 2017年6月12日 仔细看,每个人的生活都是一地鸡毛! 她结婚三年没有宝宝,一个月去四次医院检查;她怀了男孩是双...
    晴真真阅读 151评论 0 0
  • 创建第一个odoo 应用 Odoo遵循传统的MVC模式。我们可以通过创建简单的To-Do 应用来具体介绍分析 mo...
    F4A3阅读 1,401评论 0 5