mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
support next/head
This commit is contained in:
parent
9150521c55
commit
89f96cc160
|
@ -3,13 +3,15 @@ import ReactDOM from 'react-dom'
|
|||
import App from '../lib/app'
|
||||
import Link from '../lib/link'
|
||||
import Css from '../lib/css'
|
||||
import Head from '../lib/head'
|
||||
|
||||
const modules = new Map([
|
||||
['react', React],
|
||||
['react-dom', ReactDOM],
|
||||
['next/app', App],
|
||||
['next/link', Link],
|
||||
['next/css', Css]
|
||||
['next/css', Css],
|
||||
['next/head', Head]
|
||||
])
|
||||
|
||||
/**
|
||||
|
|
69
client/head-manager.js
Normal file
69
client/head-manager.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import HTMLDOMPropertyConfig from 'react/lib/HTMLDOMPropertyConfig'
|
||||
|
||||
const DEFAULT_TITLE = ''
|
||||
|
||||
export default class HeadManager {
|
||||
updateHead (head) {
|
||||
const tags = {}
|
||||
head.forEach((h) => {
|
||||
const components = tags[h.type] || []
|
||||
components.push(h)
|
||||
tags[h.type] = components
|
||||
})
|
||||
|
||||
this.updateTitle(tags.title ? tags.title[0] : null)
|
||||
|
||||
const types = ['meta', 'base', 'link', 'style', 'script']
|
||||
types.forEach((type) => {
|
||||
this.updateElements(type, tags[type] || [])
|
||||
})
|
||||
}
|
||||
|
||||
updateTitle (component) {
|
||||
let title
|
||||
if (component) {
|
||||
const { children } = component.props
|
||||
title = 'string' === typeof children ? children : children.join('')
|
||||
} else {
|
||||
title = DEFAULT_TITLE
|
||||
}
|
||||
if (title !== document.title) document.title = title
|
||||
}
|
||||
|
||||
updateElements (type, components) {
|
||||
const headEl = document.getElementsByTagName('head')[0]
|
||||
const oldTags = Array.prototype.slice.call(headEl.querySelectorAll(type + '.next-head'))
|
||||
const newTags = components.map(reactElementToDOM).filter((newTag) => {
|
||||
for (let i = 0, len = oldTags.length; i < len; i++) {
|
||||
const oldTag = oldTags[i]
|
||||
if (oldTag.isEqualNode(newTag)) {
|
||||
oldTags.splice(i, 1)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
oldTags.forEach((t) => t.parentNode.removeChild(t))
|
||||
newTags.forEach((t) => headEl.appendChild(t))
|
||||
}
|
||||
}
|
||||
|
||||
function reactElementToDOM ({ type, props }) {
|
||||
const el = document.createElement(type)
|
||||
for (const p in props) {
|
||||
if (!props.hasOwnProperty(p)) continue
|
||||
if ('children' === p || 'dangerouslySetInnerHTML' === p) continue
|
||||
|
||||
const attr = HTMLDOMPropertyConfig.DOMAttributeNames[p] || p.toLowerCase()
|
||||
el.setAttribute(attr, props[p])
|
||||
}
|
||||
|
||||
const { children, dangerouslySetInnerHTML } = props
|
||||
if (dangerouslySetInnerHTML) {
|
||||
el.innerHTML = dangerouslySetInnerHTML.__html || ''
|
||||
} else if (children) {
|
||||
el.textContent = 'string' === typeof children ? children : children.join('')
|
||||
}
|
||||
return el
|
||||
}
|
|
@ -2,6 +2,7 @@ import { createElement } from 'react'
|
|||
import { render } from 'react-dom'
|
||||
import evalScript from './eval-script'
|
||||
import Router from './router'
|
||||
import HeadManager from './head-manager'
|
||||
import DefaultApp from '../lib/app'
|
||||
|
||||
const {
|
||||
|
@ -12,7 +13,8 @@ const App = app ? evalScript(app).default : DefaultApp
|
|||
const Component = evalScript(component).default
|
||||
|
||||
const router = new Router({ Component, props })
|
||||
const headManager = new HeadManager()
|
||||
const container = document.getElementById('__next')
|
||||
const appProps = { Component, props, router }
|
||||
const appProps = { Component, props, router, headManager }
|
||||
|
||||
render(createElement(App, { ...appProps }), container)
|
||||
|
|
82
lib/head.js
Normal file
82
lib/head.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import React from 'react'
|
||||
import sideEffect from './side-effect'
|
||||
|
||||
class Head extends React.Component {
|
||||
static contextTypes = {
|
||||
headManager: React.PropTypes.object
|
||||
}
|
||||
|
||||
render () {
|
||||
return null
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.context.headManager.updateHead([])
|
||||
}
|
||||
}
|
||||
|
||||
function reduceComponents (components) {
|
||||
return components
|
||||
.map((c) => c.props.children)
|
||||
.filter((c) => !!c)
|
||||
.map((children) => React.Children.toArray(children))
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
.reverse()
|
||||
.filter(unique())
|
||||
.reverse()
|
||||
.map((c) => {
|
||||
const className = (c.className ? c.className + ' ' : '') + 'next-head'
|
||||
return React.cloneElement(c, { className })
|
||||
})
|
||||
}
|
||||
|
||||
function mapOnServer (head) {
|
||||
return head
|
||||
}
|
||||
|
||||
function onStateChange (head) {
|
||||
if (this.context && this.context.headManager) {
|
||||
this.context.headManager.updateHead(head)
|
||||
}
|
||||
}
|
||||
|
||||
const METATYPES = ['name', 'httpEquiv', 'charSet', 'itemProp']
|
||||
|
||||
// returns a function for filtering head child elements
|
||||
// which shouldn't be duplicated, like <title/>.
|
||||
|
||||
function unique () {
|
||||
const tags = new Set()
|
||||
const metaTypes = new Set()
|
||||
const metaCategories = {}
|
||||
|
||||
return (h) => {
|
||||
switch (h.type) {
|
||||
case 'title':
|
||||
case 'base':
|
||||
if (tags.has(h.type)) return false
|
||||
tags.add(h.type)
|
||||
break
|
||||
case 'meta':
|
||||
for (let i = 0, len = METATYPES.length; i < len; i++) {
|
||||
const metatype = METATYPES[i]
|
||||
if (!h.props.hasOwnProperty(metatype)) continue
|
||||
|
||||
if ('charSet' === metatype) {
|
||||
if (metaTypes.has(metatype)) return false
|
||||
metaTypes.add(metatype)
|
||||
} else {
|
||||
const category = h.props[metatype]
|
||||
const categories = metaCategories[metatype] || new Set()
|
||||
if (categories.has(category)) return false
|
||||
categories.add(category)
|
||||
metaCategories[metatype] = categories
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export default sideEffect(reduceComponents, onStateChange, mapOnServer)(Head)
|
101
lib/side-effect.js
Normal file
101
lib/side-effect.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import React, { Component } from 'react'
|
||||
|
||||
export default function withSideEffect (reduceComponentsToState, handleStateChangeOnClient, mapStateOnServer) {
|
||||
if (typeof reduceComponentsToState !== 'function') {
|
||||
throw new Error('Expected reduceComponentsToState to be a function.')
|
||||
}
|
||||
|
||||
if (typeof handleStateChangeOnClient !== 'function') {
|
||||
throw new Error('Expected handleStateChangeOnClient to be a function.')
|
||||
}
|
||||
|
||||
if (typeof mapStateOnServer !== 'undefined' && typeof mapStateOnServer !== 'function') {
|
||||
throw new Error('Expected mapStateOnServer to either be undefined or a function.')
|
||||
}
|
||||
|
||||
function getDisplayName (WrappedComponent) {
|
||||
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
|
||||
}
|
||||
|
||||
return function wrap (WrappedComponent) {
|
||||
if (typeof WrappedComponent !== 'function') {
|
||||
throw new Error('Expected WrappedComponent to be a React component.')
|
||||
}
|
||||
|
||||
const mountedInstances = new Set()
|
||||
let state
|
||||
let shouldEmitChange = false
|
||||
|
||||
function emitChange (component) {
|
||||
state = reduceComponentsToState([...mountedInstances])
|
||||
|
||||
if (SideEffect.canUseDOM) {
|
||||
handleStateChangeOnClient.call(component, state)
|
||||
} else if (mapStateOnServer) {
|
||||
state = mapStateOnServer(state)
|
||||
}
|
||||
}
|
||||
|
||||
function maybeEmitChange (component) {
|
||||
if (!shouldEmitChange) return
|
||||
shouldEmitChange = false
|
||||
emitChange(component)
|
||||
}
|
||||
|
||||
class SideEffect extends Component {
|
||||
// Try to use displayName of wrapped component
|
||||
static displayName = `SideEffect(${getDisplayName(WrappedComponent)})`
|
||||
|
||||
static contextTypes = WrappedComponent.contextTypes
|
||||
|
||||
// Expose canUseDOM so tests can monkeypatch it
|
||||
static canUseDOM = 'undefined' !== typeof window
|
||||
|
||||
static peek () {
|
||||
return state
|
||||
}
|
||||
|
||||
static rewind () {
|
||||
if (SideEffect.canUseDOM) {
|
||||
throw new Error('You may only call rewind() on the server. Call peek() to read the current state.')
|
||||
}
|
||||
|
||||
maybeEmitChange()
|
||||
|
||||
const recordedState = state
|
||||
state = undefined
|
||||
mountedInstances.clear()
|
||||
return recordedState
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
mountedInstances.add(this)
|
||||
shouldEmitChange = true
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
maybeEmitChange(this)
|
||||
}
|
||||
|
||||
componentWillUpdate () {
|
||||
shouldEmitChange = true
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
maybeEmitChange(this)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
mountedInstances.delete(this)
|
||||
shouldEmitChange = false
|
||||
emitChange(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return <WrappedComponent>{ this.props.children }</WrappedComponent>
|
||||
}
|
||||
}
|
||||
|
||||
return SideEffect
|
||||
}
|
||||
}
|
|
@ -15,7 +15,8 @@ export default function bundle (src, dst) {
|
|||
{
|
||||
[require.resolve('react')]: 'react',
|
||||
[require.resolve('../../lib/link')]: 'next/link',
|
||||
[require.resolve('../../lib/css')]: 'next/css'
|
||||
[require.resolve('../../lib/css')]: 'next/css',
|
||||
[require.resolve('../../lib/head')]: 'next/head'
|
||||
}
|
||||
],
|
||||
resolveLoader: {
|
||||
|
|
|
@ -26,7 +26,8 @@ const babelOptions = {
|
|||
{ src: `npm:${babelRuntimePath}`, expose: 'babel-runtime' },
|
||||
{ src: `npm:${require.resolve('react')}`, expose: 'react' },
|
||||
{ src: `npm:${require.resolve('../../lib/link')}`, expose: 'next/link' },
|
||||
{ src: `npm:${require.resolve('../../lib/css')}`, expose: 'next/css' }
|
||||
{ src: `npm:${require.resolve('../../lib/css')}`, expose: 'next/css' },
|
||||
{ src: `npm:${require.resolve('../../lib/head')}`, expose: 'next/head' }
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -3,6 +3,7 @@ import { createElement } from 'react'
|
|||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||
import fs from 'mz/fs'
|
||||
import Document from '../lib/document'
|
||||
import Head from '../lib/head'
|
||||
import App from '../lib/app'
|
||||
import { StyleSheetServer } from '../lib/css'
|
||||
|
||||
|
@ -28,10 +29,12 @@ export async function render (path, req, res, { dir = process.cwd(), dev = false
|
|||
return renderToString(app)
|
||||
})
|
||||
|
||||
const head = Head.rewind() || []
|
||||
|
||||
const doc = createElement(Document, {
|
||||
head: [],
|
||||
html: html,
|
||||
css: css,
|
||||
html,
|
||||
head,
|
||||
css,
|
||||
data: { component },
|
||||
hotReload: false,
|
||||
dev
|
||||
|
|
Loading…
Reference in a new issue