移至Next.js和Webpack

本文介绍了如何使用Parcel代替Next.js和Webpack,构建一个具有样式组件和流式服务器端渲染的React应用。作者指出Parcel的零配置特性使得构建过程更为简单,虽然可能比Next.js更繁琐,但提供了更多定制空间。文中详细阐述了从项目设定、包裹开发模式、添加样式、设置路由、布局和导航到实现流式服务器端渲染的步骤。
摘要由CSDN通过智能技术生成

具有样式组件和宗地的简单流式SSR React

通过我的Adobe Stock Photo许可进行照片。

当我第一次使用Next.js时,我最喜欢的一件事是它使Webpack所需的大量样板几乎消失了。 它还规定了简单,合乎逻辑的约定,如果您遵循这些约定,则可以轻松获得成功。

与以前创建服务器端渲染(SSR)React应用程序的复杂性相比,我发现它在简化性方面有了巨大的进步。

但是,去年年初,我意识到可以在与核心React API保持紧密联系的同时为我解决相同问题的新工具。

我对Next.js的最大了解之一是它的自定义路由-尽管使用简单-替代的React Router确实很棒,并且附带了很棒的动画库,我喜欢创建漂亮的,易于使用的东西!

因此,在2018年初,我放弃了Next.js和Webpack,以寻求“更接近金属”的东西,并开始使用Parcel构建React应用。

在本文中,我想向您展示如何使用Parcel构建应用程序以创建具有样式组件的流服务器端渲染的React应用程序。

如果您想知道让我感到兴奋还是尚未尝试过Parcel的东西-Parcel是Javascript Land中较新的模块捆绑包。

您认为:“这是我必须学习的另一种工具”。

没事 包裹不会那样滚动。 这是零配置。

它只是工作。

您可以导入.css文件,图像以及任何其他所需的文件,它的工作原理与您期望的完全相同。

这使得制作使用React生态系统中所有最新和最出色功能的通用应用程序变得非常容易,包括代码拆分,流式渲染,甚至是差分捆绑,这使得轻松获得最新的性能优化变得非常容易!

我想使用新的React lazy和Suspense API来实现代码拆分,但是,服务器端仍不支持它,因此我们将使用类似的替代方法。

在某些情况下仍可能略高于 Next.js更冗长,但为我用的情况下,我更喜欢额外的定制。 我想如果您对工具进行了评估已经有一段时间了,就会发现事情变得如此简单,您会感到惊讶。

旨在使您能够继续学习并获得一个不错的新样板。

我始终有一个个人目标,就是要使东西尽可能轻便。 如果这不是SSR,我建议您完全检出Hyperapp而不是React。 我为Shopify插件构建了一个非常酷的JS SDK,该插件在整个夏天都为使用它的机器学习提供了建议。

那么,我们还等什么呢? 让我们开始吧!

1.设定

首先,使用以下目录结构创建一个新项目-一个文件,两个文件夹。

- app/
- server/
.gitignore

我们将使用mkdir创建一个名为stream-all-the-things的目录。 然后,我们将进入该目录,并创建一个名为app的文件夹和一个名为server的文件夹。 最后,我们将使用touch创建我们的.gitignore文件。

这是一个快速的小片段。 随意键入每一行或复制并

将整个内容粘贴到您的终端中。

mkdir stream-all-the-things && cd stream-all-the-things
mkdir app
mkdir server
touch .gitignore

这是我们的.gitignore的内容

node_modules
*.log
.cache
dist

接下来,让我们安装所需的依赖项。

npm init

npm i --save react react-dom react-router styled-components react-helmet- async @ 0.2 .0 react-imported-component

npm i --save-dev parcel-bundler react-hot-loader

好了,有一点要在那儿解压缩。 尽管您之前从未见过很多。

您之前可能已经使用过基本的依赖项... reactreact-domreact-router 。 然后,我们还使用样式化组件来利用其流式渲染支持 。 除了样式组件是支持流渲染的CSS-in-JS库之外,我已经更喜欢styled-components ! 它自以为是的方法有助于实施最佳实践以及对CSS开发人员友好。

react-helmet-async是与流SSR一起使用的流行库react-helmet的异步版本。 它允许您在导航时更改HTML文档开头的信息。 例如,更新页面title

另外,我们有parcel-bundler ,可以捆绑, cross-env来解决Windows中的问题, nodemon ,用于开发服务器, react-hot-loader用于开发客户端, rimraf用于清理。

2.包裹开发模式

似乎我们的目标是如何发展,让我们从发展模式开始。

package.json的脚本部分添加一个dev脚本。

"scripts" : {
     "dev" : "parcel app/index.html"
}

使用Parcel,您可以简单地为其指定应用程序的入口点,作为开始开发的唯一参数。

现在,让我们创建我们引用的app/index.html文件。

<!DOCTYPE html>
< html >
   <head>
    <meta charset="UTF-8">
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
    <meta content="utf-8" http-equiv="encoding">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">      
  </head>
  
  <body>
    <div id="app"></div>
    <script id="js-entrypoint" src="./client.js"></script>
  </body>
</html>

在其中,另一个对我们尚未创建的文件的引用: client.js

这是我们客户应用程序的入口点。 换句话说,就是起点。 这是将渲染初始树的地方。

让我们创建app/client.js ,然后将其分解。

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { HelmetProvider } from 'react-helmet-async' ;

const element = document .getElementById( 'app' )

const app = (
  < HelmetProvider >
     <App /> 
  </ HelmetProvider >
)

ReactDOM.render(app, element)

// Enable Hot Module Reloading
if ( module .hot) {
  module .hot.accept();
}

最后,在我们可以测试任何东西之前,我们还需要app/App.jsx

import React from 'react'
import Helmet from 'react-helmet-async'
const App = () => (
  < React.Fragment >
     <Helmet>
      <title>Home Page</title>
    </Helmet>
    <div>
      Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a>
    </div> 
    
  </ React.Fragment >
)
export default App

现在,您应该可以运行npm run dev来启动您的开发服务器,并重新加载热代码!

➜  npm run dev
> stream-all-the-things@ 1.0 .0 dev Users/me/dev/patrickleet/stream-all-the-things
> parcel app/index.html
Server running at http: //localhost:1234
✨  Built in 192 ms.

让我们来看看!

因为您不是我,所以尝试将页面更新为您自己的链接,并请注意您无需重新加载即可查看更改!

3.添加一些样式

我混合使用了全局样式和样式化组件。

让我们添加一些基本的重置和样式,以及定义几个有用的CSS变量,这些变量将在数学上帮助我们进行即将来临的设计冒险。

创建一个文件styles.js

import { createGlobalStyle } from 'styled-components'
export const GlobalStyles = createGlobalStyle `
/* Base 10 typography scale courtesty of @wesbos 1.6rem === 16px */
html {
  font-size: 10px;
}
body {
  font-size: 1.6rem;
}
/* Relative Type Scale */
/* https://blog.envylabs.com/responsive-typographic-scales-in-css-b9f60431d1c4 */
:root {
  --step-up-5: 2em;
  --step-up-4: 1.7511em;
  --step-up-3: 1.5157em;
  --step-up-2: 1.3195em;
  --step-up-1: 1.1487em;
  /* baseline: 1em */
  --step-down-1: 0.8706em;
  --step-down-2: 0.7579em;
  --step-down-3: 0.6599em;
  --step-down-4: 0.5745em;
  --step-down-5: 0.5em;
  /* Colors */
  --header: rgb(0,0,0);
}
/* https://css-tricks.com/snippets/css/system-font-stack/ */
/* Define the "system" font family */
/* Fastest loading font - the one native to their device */
@font-face {
  font-family: system;
  font-style: normal;
  font-weight: 300;
  src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
/* Modern CSS Reset */
/* https://alligator.io/css/minimal-css-reset/ */
body, h1, h2, h3, h4, h5, h6, p, ol, ul, input[type=text], input[type=email], button {
  margin: 0;
  padding: 0;
  font-weight: normal;
}
body, h1, h2, h3, h4, h5, h6, p, ol, ul, input[type=text], input[type=email], button {
  font-family: "system"
}
*, *:before, *:after {
  box-sizing: inherit;
}
ol, ul {
  list-style: none;
}
img {
  max-width: 100%;
  height: auto;
}
/* Links */
a {
  text-decoration: underline;
  color: inherit;
&.active {
    text-decoration: none;
  }
}
`

app/App.jsx导入GlobalStyles

import { Global Styles } from './styles'

然后更改App以呈现GlobalStyles组件。

const App = () => (
  < div >
     <GlobalStyles /> 
    Follow me at  <a href="https://medium.com/@patrickleet">@patrickleet</a> 
  </ div >
)

您的应用看起来不那么难看。

4.路由

接下来我们需要使页面变得简单。

让我们添加React Router。

在您的客户端中,我们需要从React Router导入BrowserRouter ,然后将其包装在一起。

app/client.js

import { BrowserRouter } from 'react-router-dom'
// ...
const app = (
  < HelmetProvider >
     <BrowserRouter>
      <GlobalStyles />
      <App />
    </BrowserRouter> 
  </ HelmetProvider >
)

现在在app/App.jsx我们需要将当前内容提取到一个新组件中,然后通过路由器加载。 让我们从创建一个新页面开始,使用与App.jsx当前几乎相同的内容。

创建app/pages/Home.jsx

import React from 'react'
import Helmet from 'react-helmet-async'
const Home = () => (
  < React.Fragment >
     <Helmet>
      <title>Home Page</title>
    </Helmet>
    <div>
      Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a>
    </div> 
    
  </ React.Fragment >
)
export default Home

然后,修改App.jsx以具有以下内容:

import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import Home from './pages/Home'
const App = () => (
  < React.Fragment >
     <GlobalStyles />
    <Switch>
      <Route exact path="/" component={Home} />
      <Redirect to="/" />
    </Switch>
  </React.Fragment>
)
export default App

现在,当我们运行我们的应用程序时,它的外观应与之前相同,只是这次它是根据路由/的匹配通过我们的路由器渲染的。

在继续之前,让我们添加第二条路线,但这一次是“代码拆分”。

让我们创建第二个页面, app/pages/About.jsx

import React from 'react'
import Helmet from 'react-helmet-async'
const About = () => (
  < React.Fragment >
     <Helmet>
      <title>About Page</title>
    </Helmet>
    <div>
      This is the about page
    </div> 
    
  </ React.Fragment >
)
export default About

app/pages/Loading.jsx有一个加载组件:

import React from 'react'
const Loading = () => (
  < div >
    Loading...
  </ div >
)
export default Loading

最后是app/pages/Error.jsx的错误组件:

import React from 'react'
const Error = () => (
  < div >
    Error!
  </ div >
)
export default Error

为了导入它,不幸的是,我想使用新的React.lazy和Suspense API,尽管它们将在客户端上运行,但是一旦进入服务器端渲染,我们会发现ReactDomServer尚不支持Suspense。

相反,我们将依赖于另一个名为react-imported-component的库,它将与客户端和服务器端渲染的应用程序一起使用。

这是我们更新的app/App.jsx

import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom' ;
import importComponent from 'react-imported-component' ;
import Home from './pages/Home.jsx'
import LoadingComponent from './pages/Loading'
import ErrorComponent from './pages/Error'
const About = importComponent( () => import ( "./pages/About" ), {
  LoadingComponent,
  ErrorComponent
});
const App = () => (
  < React.Fragment >
     <GlobalStyles />
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/about" render={() => <About />} />
      <Redirect to="/" />
    </Switch>
  </React.Fragment>
)
export default App

现在,我们应该能够导航到/ about来查看我们的新页面。 如果您快速浏览,您将看到页面内容之前出现正在加载...。

5.布局和导航

现在,我们需要在地址栏中输入路线进行导航,这并不理想。 在转到“服务器端渲染”之前,让我们为页面添加一个通用布局,并添加一个带有导航功能的标题。

让我们从Header开始,以便我们获得clickin'。

创建app/components/Header.jsx

import React from 'react' ;
import styled from 'styled-components'
import { NavLink } from 'react-router-dom' ;
const Header = styled.header `
  z-index: 100;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  max-width: 90vw;
  margin: 0 auto;
  padding: 1em 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
`
const Brand = styled.h1 `
  font-size: var(--step-up-1);
`
const Menu = styled.ul `
  display: flex;
  justify-content: flex-end;
  align-items: center;
  width: 50vw;
`
const MenuLink = styled.li `
  margin-left: 2em;
  text-decoration: none;
`
export default () => (
  < Header >
     <Brand>Stream all the things!</Brand>
    <Menu>
      <MenuLink>
        <NavLink 
          to="/"
          exact activeClassName="active"
        >Home</NavLink>
      </MenuLink>
      <MenuLink>
        <NavLink 
          to="/about" 
          exact activeClassName="active"
        >About</NavLink>
      </MenuLink>
    </Menu> 
  </ Header >
)

我们需要将其导入并将其放入我们的应用中。

这是更新的App.jsx

import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom' ;
import importComponent from 'react-imported-component' ;
import { GlobalStyles } from './styles'
import Header from './components/Header'
import Home from './pages/Home'
import LoadingComponent from './pages/Loading'
import ErrorComponent from './pages/Error'
const About = importComponent( () => import ( "./pages/About" ), {
  LoadingComponent,
  ErrorComponent
});
const App = () => (
  < React.Fragment >
     <GlobalStyles />
    <Header />
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/about" render={() => <About />} />
      <Redirect to="/" />
    </Switch>
  </React.Fragment>

我们还创建一个Page组件,每个页面都可以使用该组件来实现一致的Page样式。

创建app/components/Page.jsx

然后,在我们的四个页面中,导入新的Page组件,并用它替换每个页面中的包装React.Fragment。

这是Home

import React from 'react'
import Helmet from 'react-helmet-async'
import Page from '../components/Page.jsx'
const Home = () => (
  < Page >
     <Helmet>
      <title>Home Page</title>
    </Helmet>
    <div>
      Follow me at <a href="https://medium.com/@patrickleet">@patrickleet</a>
    </div> 
    
  </ Page >
)
export default Home

并对“ About页面以及“错误”和“加载”页面执行相同的操作。

我们的应用程序开始看起来更好了!

显然,可以通过无数种方式来设置此应用程序的样式,因此,我将使练习变得更漂亮。

6.流式服务器端渲染

我们达到目标的下一步是添加流服务器端渲染。 如果您一直在关注,您会发现到目前为止,我们已经创建了一个静态客户端应用程序。

从客户端到同构需要在服务器上创建一个新的入口点,然后将加载与我们的客户端入口点加载的相同的App组件。

我们还将需要其他几个新的npm软件包:

npm i --save llog pino express through cheerio 
npm i --save-dev concurrently rimraf nodemon @babel/polyfill cross-env

让我们创建server / index.js:

import path from 'path'
import express from 'express'
import log from 'llog'
import ssr from './lib/ssr'
const app = express()
// Expose the public directory as /dist and point to the browser version
app.use( '/dist/client' , express.static(path.resolve(process.cwd(), 'dist' , 'client' )));
// Anything unresolved is serving the application and let
// react-router do the routing!
app.get( '/*' , ssr)
// Check for PORT environment variable, otherwise fallback on Parcel default port
const port = process.env.PORT || 1234 ;
app.listen(port, () => {
  log.info( `Listening on port ${port} ...` );
});

好的,这里有几件事要解压:

  1. 我们正在使用快递-它很可能是任何其他服务器。 我们实际上并没有做太多事情,因此转换为您选择的服务器应该不难。
  2. 我们正在为/ dist / clients目录设置一个静态文件服务器。 我们目前没有建立生产资产,但是当我们建立资产时,我们可以将它们放在那里。
  3. 其他所有路线都将进入ssr。 不用理会服务器上的路由,我们只需执行React Router所做的任何事情即可。

让我们创建ssr函数。 这可能比本教程的其余部分要复杂得多,但这只是需要做一次,然后基本上不做任何事情。

在继续之前,让我们看一下需要创建的脚本。

"scripts" : {
  "dev" : "npm run generate-imported-components && parcel app/index.html" ,
  "dev:server" : "nodemon -e js,jsx,html --ignore dist --ignore app/imported.js --exec 'npm run build && npm run start'" ,
  "start" : "node dist/server"
  "build" : "rimraf dist && npm run generate-imported-components && npm run create-bundles" ,
  "create-bundles" : "concurrently \"npm run create-bundle:client\" \"npm run create-bundle:server\"" ,
  "create-bundle:client" : "cross-env BABEL_ENV=client parcel build app/index.html -d dist/client --public-url /dist/client" ,
  "create-bundle:server" : "cross-env BABEL_ENV=server parcel build server/index.js -d dist/server --public-url /dist --target=node" ,
  "generate-imported-components" : "imported-components app app/imported.js" ,
  "start" : "node dist/server"
}

现在还有很多。 我突出显示了名称,以使其更易于阅读。 在较高的级别上,我们添加了构建脚本来生成一个包含有关导入组件信息的文件,以及一个可以使用宗地同时构建客户端和服务器捆绑包的构建脚本。

现在,对于导入的组件,我们还需要一个.babelrc文件。 也许在接下来的几个月中,这种情况将会改变。

{
  "env" : {
    "server" : {
      "plugins" : [ "react-imported-component/babel" , "babel-plugin-dynamic-import-node" ]
    },
    "client" : {
      "plugins" : [
        [ "react-imported-component/babel" ]
      ]
    }
  }
}

有了这一点,我们要解决两个主要问题。

创建SSR中间件为SSR重新使用客户端HTML数据并从中解析生成的src名称

创建server/lib/ssr.js

import React from 'react'
import { renderToNodeStream } from 'react-dom/server'
import { HelmetProvider } from 'react-helmet-async'
import { StaticRouter } from 'react-router-dom'
import { ServerStyleSheet } from 'styled-components'
import { printDrainHydrateMarks } from 'react-imported-component' ;
import log from 'llog'
import through from 'through'
import App from '../../app/App'
import { getHTMLFragments } from './client'
// import { getDataFromTree } from 'react-apollo';
export default (req, res) => {
  const context = {};
  const helmetContext = {};
const app = (
    < HelmetProvider context = {helmetContext} >
       <StaticRouter
        location={req.originalUrl}
        context={context}
      >
        <App />
      </StaticRouter> 
    </ HelmetProvider >
  );
  try {
    // If you were using Apollo, you could fetch data with this
    // await getDataFromTree(app);
    const sheet = new ServerStyleSheet()
    const stream = sheet.interleaveWithNodeStream(
      renderToNodeStream(sheet.collectStyles(app))
    )
    if (context.url) {
      res.redirect( 301 , context.url);
    } else {
      const [
        startingHTMLFragment,
        endingHTMLFragment
      ] = getHTMLFragments({ drainHydrateMarks : printDrainHydrateMarks() })
      res.status( 200 )
      res.write(startingHTMLFragment)
      stream
        .pipe(
          through(
            function write ( data )  {
              this .queue(data)
            },
            function end ( )  {
              this .queue(endingHTMLFragment)
              this .queue( null )
            }
          )
        )
        .pipe(res)
    }
  } catch (e) {
    log.error(e)
    res.status( 500 )
    res.end()
  }
};

使用server/lib/client.js我们需要读取app/index.html文件,并将其分为两个块,使上面的流式传输更加容易。

import fs from 'fs' ;
import path from 'path' ;
import cheerio from 'cheerio' ;
export const htmlPath = path.join(process.cwd(), 'dist' , 'client' , 'index.html' );
export const rawHTML = fs.readFileSync(htmlPath).toString();
export const parseRawHTMLForData = ( template, selector = "#js-entrypoint" ) => {
  const $template = cheerio.load(template);
  let src = $template(selector).attr( 'src' )
  return {
    src
  }
}
const clientData = parseRawHTMLForData(rawHTML)
const appString = '<div id="app"\>'
const splitter = '###SPLIT###'
const [ 
  startingRawHTMLFragment, 
  endingRawHTMLFragment 
] = rawHTML
      .replace(appString, ` ${appString} ${splitter} ` )
      .split(splitter)
export const getHTMLFragments = ( { drainHydrateMarks } ) => {
  const startingHTMLFragment = ` ${startingRawHTMLFragment} ${drainHydrateMarks} `
  return [startingHTMLFragment, endingRawHTMLFragment]
}

这将通过服务器呈现我们的应用程序,但是如果不对客户端进行一些小改动,它将无法成功重新连接到客户端应用程序。

我们正在通过SSR功能提供“补水标记”,但尚未使用它们。

app/client.js进行以下修改:

1.导入rehydrateMarks importedComponents

import { rehydrateMarks } from 'react-imported-component' ;
import importedComponents from './imported' ; // eslint-disable-line

2.将ReactDOM.render(app, element)替换为:

// In production, we want to hydrate instead of render
// because of the server-rendering
if (process.env.NODE_ENV === 'production' ) {
  // rehydrate the bundle marks
  rehydrateMarks().then( () => {
    ReactDOM.hydrate(app, element);
  });
} else {
  ReactDOM.render(app, element);
}

并做了!

现在,当您运行npm run dev:servernpm run build && npm run start您将使用服务器端渲染!

结论

我承认,比Next.js还要多的样板,但希望它并没有那么压倒性,并且那里的内容是透明且可理解的。 公平地说,Next.js仍在为我们做更多的事情,例如预取组件。

但是,我仍然更喜欢这种方法,因为正在发生的事情没有什么神秘之处,Webpack配置已完全消失,并且很容易利用动画库来作为我将在练习中留下的React Router。

希望您发现这很有用!

如果您这样做,最好的帮助我的方法是给我一些鼓掌和/或分享!

最好,
帕特里克·李·斯科特

PS这是GitHub上完整代码

PPS本文是系列文章的一部分。 看看下面的其他部分!

From: https://hackernoon.com/move-over-next-js-and-webpack-ba367f07545

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值