cisen / blog

Time waits for no one.
133 stars 20 forks source link

next.js 是一个基于React实现的服务端渲染框架 #168

Open cisen opened 5 years ago

cisen commented 5 years ago

https://github.com/zeit/next.js 使用Next.js实现服务端渲染是一件非常简单的事,在这里,你完全可以不用自己去写webpack等配置,Next.js全都帮你做了。本文先从简单地基础概念开始,一步一步带大家认识Next.js。

先初始化我们的项目目录结构:

mkdir learn-next
cd learn-next
npm init -y
npm install react react-dom next -S
mkdir pages

可以看到,我们最后一步的时候创建了一个命名为pages的文件夹,这是因为Next.js采用的是文件系统作为API,每一个放在pages中的文件都会映射为一个路由,路由名称与文件名相同。

打开package.json文件,配置我们的项目启动命令

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

然后在命令行中启动我们的项目:

npm run dev

打开http://localhost:3000,可以看到Next.js给我们报了404,这是因为我们还没写任何内容。

基础路由

在pages目录下新建index.js,输入以下内容:

export default () => (
    <h1>Hello Next.js</h1>
)

这时候,我们可以看到Next.js已经把我们的内容渲染出来了,如下所示:

图片描述

页面间导航 页面间跳转是很正常的事,因此,Next.js为我们准备了Link这个高阶组件,用于页面导航。我们先新建一个about.js文件

export default () => (
    <h1>This is about page</h1>
)

然后将我们的index.js更改为:

import Link from 'next/link'

export default () => (
    <div>
        <Link href="/about" >
            <a>About Page</a>
        </Link>
        <h1>Hello Next.js</h1>
    </div>
)

共用组件 我们的组件不可能都是孤立的,组件间复用是很常见的事,例如页面的头部,底部,导航条等等,因此我们可以在根目录下新建一个components目录用于存放共用的组件。

新建一个Header.js文件

import Link from 'next/link'

const linkStyle = {
  marginRight: 15
}

export default () => (
    <div>
        <Link href="/">
          <a style={linkStyle}>Home</a>
        </Link>
        <Link href="/about">
          <a style={linkStyle}>About</a>
        </Link>
    </div>
)

新建一个Layout.js文件

import Header from './Header'

const layoutStyle = {
  margin: 20,
  padding: 20,
  border: '1px solid #DDD'
}

export default (props) => (
  <div style={layoutStyle}>
    <Header />
    {props.children}
  </div>
)

更改我们的pages/index.js文件

import Layout from '../components/Layout.js'

export default () => (
  <Layout>
       <h1>Hello Next.js</h1>
  </Layout>
)

打开http://localhost:3000,可以看到我们的共用组件生效了:

图片描述

动态页面 假设有一个post页面,该页面接收一个id,并将该id展示出来,那么怎么做呢。

在pages下新建post.js文件,内容如下:

import Layout from '../components/Layout.js'

export default (props) => (
    <Layout>
       <h1>{props.url.query.id}</h1>
       <p>This is the post page.</p>
    </Layout>
)

如上所示:我们从url.query.id中拿到页面传过来的id

那么怎么把id从index页面传过去呢,回到pages/index.js页面,代码更改如下:

import Layout from '../components/Layout.js'
import Link from 'next/link'

const PostLink = (props) => (
  <li>
    <Link as={`/p/${props.id}`}  href={`/post?id=${props.id}`}>
      <a>{props.id}</a>
    </Link>
  </li>
)

export default () => (
  <Layout>
    <h1>My Blog</h1>
    <ul>
      <PostLink id="hello-nextjs" />
      <PostLink id="learn-nextjs" />
      <PostLink id="deploy-nextjs" />
    </ul>
  </Layout>
)

在上面的代码中,我们在Link标签中使用了as属性,它的作用是更改路由的名称,当我们点击"learn-nextjs"时,我们可以看到,地址栏的地址显示为http://localhost:3000/p/learn-nextjs

服务端路由 上面的代码其实是有问题的,这只适合在浏览器端进行导航,但是当我们在http://localhost:3000/p/learn-nextjs下刷新页面时,会看到服务器给我们报了404,因此我们需要同步适配一下服务端的路由。

我们选用express来作为服务端框架,当然你也可以使用koa。

npm install express -S

在根目录下新建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 = '/post'
    const queryParams = { id: req.params.id } 
    app.render(req, res, actualPage, queryParams)
  })

  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.error(ex.stack)
  process.exit(1)
})

更改package.json中我们的项目启动方式:

{
  "scripts": {
    "dev": "node server.js"
  }
}

这时候刷新页面,可以看到我们的页面也被正确渲染了。

本篇教程到此结束,后面会跟大家介绍Next.js的服务端渲染(ssr)及css in js以及部署相关的一下概念及示例代码。

cisen commented 5 years ago

Next.js 原文地址

Naoyuki Kanezawa (@nkzawa), Guillermo Rauch (@rauchg) 和 Tony Kovanen (@tonykovanen) 周二,2016年10月25日

我们非常自豪的开源了Next.js,他是一个小巧的基于React、Webpack、Babel的客户端渲染universal JavasScript web app框架。

要开始使用Next.js,只需在一个有package.json的文件夹里执行以下命令:

$ npm install next --save
$ mkdir pages

生成pages/index.js:

import React from 'react'
export default () => <div>Hello world!</div>

然后在package.json里添加一个script:

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

然后执行:

$ npm run dev

本文主要阐述该项目的设计理念和它背后的哲学思想。

如果想学习如何使用Next.js,请移步README,在那你只需花几分钟就能学习完所有功能。

首先我们会深入该项目的后端并逐一描述以下6个原则:

零配置,使用文件系统作为API

只有JavaScript,一切都是函数

自动服务端渲染和代码分割

完全可定制的数据获取方式

预测是提高性能的关键

部署简单

背景

从很多年以前,我们就一直追求universal JavaScript applications的道路。

Node.js展示了这种可能性,客户端服务端代码共享,也扩展了开发者的视野。

为了能在Node上开发app和网页,开发者做了很多很多尝试。无数的模板语言和框架应运而生……但是技术始终被分割为前端和后端。

假设你选择Express和Jade开发,HTML会首先被服务端渲染,然后另外一个项目(jQuery或是其他类似库)才会接管过去。

这样的状况其实一点也不比传统的PHP方式好多少。从很多方面来说,PHP都更适合服务端渲染HTML这样的工作。在出现async/await之前,在JS中查询数据并不容易。捕获和处理request/response的异常也非常麻烦。

然而,一些值得关注的概念的出现,使得我们能够填补这个空白。其中最重要的就是能够根据数据返回对应UI的纯函数。

这个模型(因React而流行)非常重要,不过仅仅有他还不能从众多的模板系统中脱颖而出。另外一个重要的概念就是组件生命周期。

生命周期的钩子函数允许我们在前端接管某些之前由服务端渲染的的页面。比如说,你可以在一开始只渲染静态数据,监听服务端的更新,并根据数据改变页面。或者什么也不做,让这个页面保持静态。

Next.js是我们在这条路上更近一步的成果。

零配置,使用文件系统作为API 工具假设你的项目具有特定的文件结构。

一般来说,我们开始一个新项目时,都会新建一个文件夹,在里面放一个package.json,在./node_modules中安装模块。

Next.js扩展了这种结构,引入了一个放置顶级组件的文件夹叫pages。

例如,你可以新建pages/index.js,它会自动映射到/路由:

import React from 'react'
export default () => <marquee>Hello world</marquee>

然后新建pages/about.js,它会映射到/about路由:

import React from 'react'
export default () => <h1>About us</h1>

我们相信这是一个很好的起步默认配置,而且非常便于项目浏览。当需要更复杂的路由时,我们也允许开发人员自行控制[#25]。

启动一个项目所需要的所有操作仅仅是运行:

$ next

除非必要,没有额外的配置。自动代码热替换,自动错误报告,自动source maps,自动为老旧浏览器编译代码。

只有JavaScript,一切都是函数

每个Next.js的路由都是一个仅仅是一个export一个函数或一个继承自React.Component的子类所构成的ES6模块。

这个方式和其他类似模型相比的好处是,整个系统都能保持高可组合性和可测试性。一个组件可以被直接渲染也可以被其他顶级组件导入并渲染。

组件也可以改变整个page的:

import React from 'react'
import Head from 'next/head'
export default () => (
  <div>
    <Head>
      <meta name="viewport" content="width=device-width, initial-scale=1" />
    </Head>
    <h1>Hi. I'm mobile-ready!</h1>
  </div>
)

并且,不需要任何包装或改动就能对整个系统进行测试。只需在你的测试集中导入并shallow-render你的路由。

拥抱CSS-in-JS。通过使用glamor使得我们能在完全不理会CSS解析和编译的情况下拥有完整的CSS功能:

import React from 'react'
import css from 'next/css'

export default () => <p className={style}>Hi there!</p>

const style = css({
  color: 'red',
  ':hover': {
    color: 'blue'
  },
  '@media (max-width: 500px)': {
    color: 'rebeccapurple'
  }
})

我们认为这种方式提供了无与伦比的性能,可组合性以及和服务器端渲染流程的良好集成。我们在FAQ中会讨论更多关于这个决定的一切。

自动服务端渲染和代码分割 有两个非常想实现同时又非常困难的任务:

服务端渲染

代码分割

在Next.js中,每个pages/下面的组件都会自动的连同内联的脚本一起被服务端渲染。

当组件是通过或路由自动加载时,我们会获取一个基于JSON的页面,这个页面同样会包含他自己的脚本。

这意味着一个页面可以有很多的imports:

import React from 'react'
import d3 from 'd3'
import jQuery from 'jquery'

… 这并不会对其余的页面有任何影响。

这点对于那些需要技术业务需求不同的团队互相合作的场景下特别有用。一个组件的性能问题不会影响到整个系统。

完全可定制的数据获取方式 服务端渲染的静态JSX确实非常了不起,但现实世界的应用往往需要处理来自不同API调用的数据。

Next.js给React的组件添加了一个重要的扩展:getInitialProps。

import React from 'react'
import 'isomorphic-fetch'
export default class extends React.Component {
  static async getInitialProps () {
    const res = await fetch('https://api.company.com/user/123')
    const data = await res.json()
    return { username: data.profile.username }
  }
}

我们关于转换哪些功能的立场简单来说就是:我们紧紧跟随V8。因为我们的目标是服务端和客户端的代码共享,当我们用Chrome或者Brave开发,并在Node上执行代码时,这种做法给了我们极好的体验。

正如你所见,我们的扩展非常简单:getInitialProps必须返回一个能resolve为一个JavaScript对象的Promise,该对象会被用来生成组件的props。

这使得Next.js能很好的和REST APIs、GraphQL,甚至是全局状态管理Redux等很好的协作,这有一个示例在我们的wiki上。

无论组件是服务端渲染的还是通过客户端路由动态加载的,都可以使用同一个方法获得数据:

static async getInitialProps ({ res }) {
  return res
    ? { userAgent: res.headers['user-agent'] }
    : { userAgent: navigator.userAgent }
}

预测是提高性能的关键 我们认为即使没有网络也能给予用户即时响应的能力使得完全服务端渲染偏向“单页应用”或“完全没有服务端渲染”两个极端。

在www.zeit.co我们在Next.js上实现了一种技术,让我们能同时享受两种方式各自的好处:每个标签都会在后台通过一个ServiceWorker提前获取组件的JSON表现。

一旦预加载完成,如果你在页面上随意跳转,你点击的某个链接或路由已经提前加载好了。

更好的是,因为数据也通过一个专用的方法getInitialProps,我们能提前加载而不用怕引发不必要的服务端负载和数据加载。这比之前的web 1.0预加载机制强多了。

部署简单

我们创建Next.js是因为我们相信同构的应用是未来web应用的重要组成部分。

提前绑定和编译(预测)是一个非常有效的部署方式。

部署一个Next.js应用只需要运行next build和next start。

你的package.json文件和以下类似:

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

这样,你就简单的部署成功了。

最后,这是我们对于这个特定问题的贡献。我们认为他在灵活性和好用的默认配置之间取得了不错的平衡,不过这肯定不是解决所有问题的方法。

在接下来的几周里,我们希望能更多的讨论和思考其他解决方案,比如Vue.JS, Gatsby, Ember+Fastboot等等。 如果你有兴趣加入我们的社区,做出自己的贡献,请一定要加入zeit.chat, 查看issues,参与未来方向的讨论。