Кода за тази глава можете да намерите в master-no-services
клон на JS-Stack-Boilerplate repository.
Окей! Време е да направите приложението си да изгежда красиво. Ще използваме Twitter Bootstrap, за да приложим основни стилове върху него. След това ще добавим една CSS-in-JS библиотека, за да добавим още някои по-специфични стилове.
💡 Twitter Bootstrap е библиотека с компоненти за построяване на потребителски интерфейс (UI components).
Има два начина за интегриране на Bootstrap в React приложение. И двата имат техните предимства и недостатъци:
- Да се използва официалния релийз, който използва jQuery и Tether за функционалността на компонентите.
- Да се използва отделна библиотека, която реимплементира всички Bootstrap компоненти в React, като например React-Bootstrap или Reactstrap.
Third-party библиотеките предлагат доста удобни за употреба React компоненти, които намаляват значително много кода, който се използва, в сравнение с официалните HTML компоненти, а освен това интеграцията им с React е много добре направена. И въпреки казаното до тук, аз не съм голям привърженик на използването им, тъй като те винаги остават назад от официалните релийзи (понякога значително назад). Също така те биха били несъвестими с Bootstrap теми, които използват свой собствен JS. Това е доста сериозен недостатък, имайки предвид, че едно от основните предимства на Bootstrap е голямата му поддръжка от дизайнери, които създават красиви теми.
Поради тази причина ще използвам официалния релийз, който върви с jQuery и Tether. Едно от нещата, на които трябва да обърнем внимание когато използваме този подход е размера на файловете на нашия пакет. За ваша информация, размерът на пакета е около 200Кб (Gzipped) с включени jQuery, Tether и JavaScript-a на Bootstrap. Мисля, че това е разумно, но в случай че е прекалено много за вас, може би трябва да помислите за алтернатива на Bootstrap или дори за вариант да не го използвате въобще.
-
Изтрийте
public/css/style.css
-
Изпълнете
yarn add bootstrap@4.0.0-alpha.6
-
Копирайте
bootstrap.min.css
иbootstrap.min.css.map
отnode_modules/bootstrap/dist/css
във вашатаpublic/css
папка. -
Редактирайте
src/server/render-app.jsx
, както следва:
<link rel="stylesheet" href="${STATIC_PATH}/css/bootstrap.min.css">
Сега, след като вече сме заредили стиловете от Bootstrap на нашата страница, ще ни трябват JavaScript функционалността на компонентите.
-
Изпълнете
yarn add jquery tether
-
Редактирайте
src/client/index.jsx
, както следва:
import $ from 'jquery'
import Tether from 'tether'
// [right after all your imports]
window.jQuery = $
window.Tether = Tether
require('bootstrap')
Това ще зареди JavaScript кода на Bootstrap.
Окей, време е да копирате няколко файла.
- Редактирайте
src/shared/component/page/hello-async.jsx
файла, както следва:
// @flow
import React from 'react'
import Helmet from 'react-helmet'
import MessageAsync from '../../container/message-async'
import HelloAsyncButton from '../../container/hello-async-button'
const title = 'Async Hello Page'
const HelloAsyncPage = () =>
<div className="container mt-4">
<Helmet
title={title}
meta={[
{ name: 'description', content: 'A page to say hello asynchronously' },
{ property: 'og:title', content: title },
]}
/>
<div className="row">
<div className="col-12">
<h1>{title}</h1>
<MessageAsync />
<HelloAsyncButton />
</div>
</div>
</div>
export default HelloAsyncPage
- Редактирайте
src/shared/component/page/hello.jsx
файла, както следва:
// @flow
import React from 'react'
import Helmet from 'react-helmet'
import Message from '../../container/message'
import HelloButton from '../../container/hello-button'
const title = 'Hello Page'
const HelloPage = () =>
<div className="container mt-4">
<Helmet
title={title}
meta={[
{ name: 'description', content: 'A page to say hello' },
{ property: 'og:title', content: title },
]}
/>
<div className="row">
<div className="col-12">
<h1>{title}</h1>
<Message />
<HelloButton />
</div>
</div>
</div>
export default HelloPage
- Редактирайте
src/shared/component/page/home.jsx
файла, както следва:
// @flow
import React from 'react'
import Helmet from 'react-helmet'
import ModalExample from '../modal-example'
import { APP_NAME } from '../../config'
const HomePage = () =>
<div>
<Helmet
meta={[
{ name: 'description', content: 'Hello App is an app to say hello' },
{ property: 'og:title', content: APP_NAME },
]}
/>
<div className="jumbotron">
<div className="container">
<h1 className="display-3 mb-4">{APP_NAME}</h1>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-md-4 mb-4">
<h3 className="mb-3">Bootstrap</h3>
<p>
<button type="button" role="button" data-toggle="modal" data-target=".js-modal-example" className="btn btn-primary">Open Modal</button>
</p>
</div>
<div className="col-md-4 mb-4">
<h3 className="mb-3">JSS (soon)</h3>
</div>
<div className="col-md-4 mb-4">
<h3 className="mb-3">Websockets</h3>
<p>Open your browser console.</p>
</div>
</div>
</div>
<ModalExample />
</div>
export default HomePage
- Редактирайте
src/shared/component/page/not-found.jsx
файла, както следва:
// @flow
import React from 'react'
import Helmet from 'react-helmet'
import { Link } from 'react-router-dom'
import { HOME_PAGE_ROUTE } from '../../routes'
const title = 'Page Not Found!'
const NotFoundPage = () =>
<div className="container mt-4">
<Helmet title={title} />
<div className="row">
<div className="col-12">
<h1>{title}</h1>
<div><Link to={HOME_PAGE_ROUTE}>Go to the homepage</Link>.</div>
</div>
</div>
</div>
export default NotFoundPage
- Редактирайте
src/shared/component/button.jsx
, както следва:
// [...]
<button
onClick={handleClick}
className="btn btn-primary"
type="button"
role="button"
>{label}</button>
// [...]
- Създайте
src/shared/component/footer.jsx
файл, съдържащ:
// @flow
import React from 'react'
import { APP_NAME } from '../config'
const Footer = () =>
<div className="container mt-5">
<hr />
<footer>
<p>© {APP_NAME} 2017</p>
</footer>
</div>
export default Footer
- Създайте
src/shared/component/modal-example.jsx
файл, съдържащ:
// @flow
import React from 'react'
const ModalExample = () =>
<div className="js-modal-example modal fade">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Modal title</h5>
<button type="button" className="close" data-dismiss="modal">×</button>
</div>
<div className="modal-body">
This is a Bootstrap modal. It uses jQuery.
</div>
<div className="modal-footer">
<button type="button" role="button" className="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
export default ModalExample
- Редактирайте
src/shared/app.jsx
файла, както следва:
const App = () =>
<div style={{ paddingTop: 54 }}>
Това е пример за React inline style.
Това ще се преведе в: <div style="padding-top:54px;">
във вашия DOM. Искаме този стил да избута съдържанието под лентата за навигация. React inline styles е чудесен начин за изолиране на стиловете на вашите компоненти от глобалната CSS зона на действие (namespace), но като всяко нещо и това си има цена : Не можете да използвате някои вградени CSS свойства като :hover
, Media Queries, анимации или font-face
. Това е една от причините, поради която ще интегрираме CSS-in-JS библиотека, JSS, по-късно в тази глава.
- Редактирайте
src/shared/component/nav.jsx
файла, както следва:
// @flow
import $ from 'jquery'
import React from 'react'
import { Link, NavLink } from 'react-router-dom'
import { APP_NAME } from '../config'
import {
HOME_PAGE_ROUTE,
HELLO_PAGE_ROUTE,
HELLO_ASYNC_PAGE_ROUTE,
NOT_FOUND_DEMO_PAGE_ROUTE,
} from '../routes'
const handleNavLinkClick = () => {
$('body').scrollTop(0)
$('.js-navbar-collapse').collapse('hide')
}
const Nav = () =>
<nav className="navbar navbar-toggleable-md navbar-inverse fixed-top bg-inverse">
<button className="navbar-toggler navbar-toggler-right" type="button" role="button" data-toggle="collapse" data-target=".js-navbar-collapse">
<span className="navbar-toggler-icon" />
</button>
<Link to={HOME_PAGE_ROUTE} className="navbar-brand">{APP_NAME}</Link>
<div className="js-navbar-collapse collapse navbar-collapse">
<ul className="navbar-nav mr-auto">
{[
{ route: HOME_PAGE_ROUTE, label: 'Home' },
{ route: HELLO_PAGE_ROUTE, label: 'Say Hello' },
{ route: HELLO_ASYNC_PAGE_ROUTE, label: 'Say Hello Asynchronously' },
{ route: NOT_FOUND_DEMO_PAGE_ROUTE, label: '404 Demo' },
].map(link => (
<li className="nav-item" key={link.route}>
<NavLink to={link.route} className="nav-link" activeStyle={{ color: 'white' }} exact onClick={handleNavLinkClick}>{link.label}</NavLink>
</li>
))}
</ul>
</div>
</nav>
export default Nav
Тук използваме нещо ново - handleNavLinkClick
. Един от проблемите, който срещнах при използването на компонента navbar
на Bootstrap в едно SPA (single page application) или приложение състоящо се от една страница, е, че кликването върху линк при мобилните устройства не работи правилно, тоест не затваря менюто и не премества страницата до най-горната й част (not collapse the menu and does not scroll back to the top of the page). Това е чудесна възможност да ви покажа пример как да използвате jQuery / Bootstrap-specific код във вашето приложение:
import $ from 'jquery'
// [...]
const handleNavLinkClick = () => {
$('body').scrollTop(0)
$('.js-navbar-collapse').collapse('hide')
}
<NavLink /* [...] */ onClick={handleNavLinkClick}>
Забележка: Нарочно съм премахнал кода, който отговаря за достъпността (accessibility-related attributes, such as aria
attributes), за да го направя по-четим в контекста на това ръководство. Задължително трябва да го върнете обратно. Прочетете повече в документацията на Bootstrap и примерите с код.
🏁 Сега вашето приложение би трябвало да бъде изцяло стилизирано с Bootstrap.
През 2016, това би бил типичен Javascript пакет от инструменти (JavaScript stack). Различните библиотеки и инструменти използвани в това ръководство са най-новото в индустрията (въпреки че за една година могат да се окажат вече остарели). Да, това е доста комплексен набор от инструменти, но поне повечето от фронт-енд програмистите са съгласни, че React-Redux-Webpack е пътя, по който трябва да се върви. Сега, относно CSS, имам някои доста лоши новини - все още няма нищо определено, никакви стандарти и прочие.
SASS, BEM, SMACSS, SUIT, Bass CSS, React Inline Styles, LESS, Styled Components, CSSX, JSS, Radium, Web Components, CSS Modules, OOCSS, Tachyons, Stylus, Atomic CSS, PostCSS, Aphrodite, React Native for Web и много други, които забравям, са различни подходи или инструменти, за да се свърши работата. Всички те се справят добре, което всъщност е проблема, че няма ясен победител и всичко е една голяма бъркотия.
Феновете на React са привърженици на React inline styles, CSS-in-JS, или CSS Modules подходите, тъй като те се интегрират доста добре с React и решават доста проблеми, които стандартните CSS подходи не могат.
CSS Modules вършат добра работа, но не използват силата на JavaScript и неговите предимства пред обикновения CSS. Те предлагат просто енкапсулация, което е добре, но по мое мнение React inline styles и CSS-in-JS отнасят стилиризирането на приложенията на едно по-високо ниво. Личната ми препоръка би била използването на React inline styles за общи неща (за които също бихте могли да използвате React Native) и на CSS-in-JS библиотека за неща като :hover
and media queries.
Съществуват тонове CSS-in-JS библиотеки. JSS е една от тях - богата на функционалност, добре обоснована и представяща се добре от гледна точка на производителността.
💡 JSS e една CSS-in-JS библиотека за писане на стилове в JavaScript и инжектирането им във вашето приложение.
Сега, след като имаме основен Bootstrap темплейт, нека напишем малко собствен CSS код. По-рано споменах, че React inline styles нямат поддръжка за :hover
and media queries, затова ще покажем едно просто примерче с JSS на началната ни страница. JSS може да бъде използван чрез react-jss
, библиотека, която е удобна за употреба когато се работи с React компоненти.
- Изпълнете
yarn add react-jss
Добавете следното във вашия .flowconfig
файл, тъй като в момента Flow има проблем с JSS:
[ignore]
.*/node_modules/jss/.*
JSS може да рендира стилове от страната на сървъра при началното зареждане.
- Добавете следните константи в
src/shared/config.js
:
export const JSS_SSR_CLASS = 'jss-ssr'
export const JSS_SSR_SELECTOR = `.${JSS_SSR_CLASS}`
- Редактирайте
src/server/render-app.jsx
, както следва:
import { SheetsRegistry, SheetsRegistryProvider } from 'react-jss'
// [...]
import { APP_CONTAINER_CLASS, JSS_SSR_CLASS, STATIC_PATH, WDS_PORT } from '../shared/config'
// [...]
const renderApp = (location: string, plainPartialState: ?Object, routerContext: ?Object = {}) => {
const store = initStore(plainPartialState)
const sheets = new SheetsRegistry()
const appHtml = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={location} context={routerContext}>
<SheetsRegistryProvider registry={sheets}>
<App />
</SheetsRegistryProvider>
</StaticRouter>
</Provider>)
// [...]
<link rel="stylesheet" href="${STATIC_PATH}/css/bootstrap.min.css">
<style class="${JSS_SSR_CLASS}">${sheets.toString()}</style>
// [...]
Първото нещо, което клиента трябва да направи след зареждането на приложението, е да се отърве от сървърно генерираните JSS стилове.
- Добавете следното в
src/client/index.jsx
следReactDOM.render
извикванията (предиsetUpSocket(store)
например):
import { APP_CONTAINER_SELECTOR, JSS_SSR_SELECTOR } from '../shared/config'
// [...]
const jssServerSide = document.querySelector(JSS_SSR_SELECTOR)
// flow-disable-next-line
jssServerSide.parentNode.removeChild(jssServerSide)
setUpSocket(store)
Редактирайте src/shared/component/page/home.jsx
, както следва:
import injectSheet from 'react-jss'
// [...]
const styles = {
hoverMe: {
'&:hover': {
color: 'red',
},
},
'@media (max-width: 800px)': {
resizeMe: {
color: 'red',
},
},
specialButton: {
composes: ['btn', 'btn-primary'],
backgroundColor: 'limegreen',
},
}
const HomePage = ({ classes }: { classes: Object }) =>
// [...]
<div className="col-md-4 mb-4">
<h3 className="mb-3">JSS</h3>
<p className={classes.hoverMe}>Hover me.</p>
<p className={classes.resizeMe}>Resize the window.</p>
<button className={classes.specialButton}>Composition</button>
</div>
// [...]
export default injectSheet(styles)(HomePage)
За разлика от React inline styles, JSS използва класове. Подаваме стилове към injectSheet
и CSS класовете се превръщат в свойства на вашия компонент.
🏁 Изпълнете yarn start
и yarn dev:wds
. Отворете началната страница и вижте сорс кода й (не в инспектора). Ще видите, че JSS стиловете са в DOM при началното зареждане, в <style class="jss-ssr">
елемента (само на началната страница). Важно е да се спомене, че те не бива да се виждат в инспектора, тъй като би трябвало да са заместени от <style type="text/css" data-jss data-meta="HomePage">
.
Забележка: В производствена среда (production mode), data-meta
е обфускирано (obfuscated).
Ако посочите с мишката върху елемента "Hover me", би трябвало да стане червен. Ако оразмерите прозореца на вашия браузър да стане по-тесен от 800px, "Resize your window" надписа би трябвало да стане червен. Зеленият бутон разширява CSS класовете на Bootstrap, използвайки JSS свойството composes
.
Следваща глава: 09 - Travis, Coveralls, Heroku
Назад към предишната глава или към съдържанието.