上文講到經過同構服務端渲染,能夠直出html結構,雖然講解了樣式,圖片等靜態資源在服務端引入問題的解決方案,可是並無實際進行相關操做,這篇文章就講解一下如何讓樣式像html同樣直出。css
PS: 直出,個人理解就是輸入url發起get請求訪問服務端,直接獲得完整響應結果,而不是同過ajax異步去獲取。
html
目前咱們的項目中還不存在任何樣式文件,因此須要先寫一個,就給組件App寫一個樣式文件吧。react
下面這些依賴都是後續會用到的,先安裝一下,下面會詳細講解每一個依賴的做用。webpack
npm install postcss-loader postcss-import postcss-cssnext postcss-nested postcss-functions css-loader style-loader isomorphic-style-loader --save-dev
css文件的後綴是.css,less文件的後綴是.less,這裏我選擇使用PostCSS配合其插件來寫樣式,因此我就本身定義一個後綴.pcss好了。git
// ./src/client/component/app/style.pcss .root { color: red; }
設定一個root類,樣式就是簡單的設置顏色爲紅色。而後在App組件裏引用它。es6
// ./src/client/component/app/index.tsx ... import * as styles from './style.pcss'; ... public render() { return ( <div className={styles.root}>hello world</div> ); } ...
這個時候你會發現編輯器裏是這樣的:
出現這個問題是由於ts不知道這種模塊的類型定義,因此咱們須要手動加入自定義模塊類型定義。在項目根目錄下新建@types文件夾,在此目錄下創建index.d.ts文件:github
// ./@types/index.d.ts declare module '*.pcss' { const content: any; export = content; }
保存以後就不會看到編輯器報錯了,可是terminal裏webpack打包會提示出錯,由於咱們尚未加對應的loader。web
js都組件化了,css模塊化也是頗有必要的,不用再爲避免取重複類名而煩惱。咱們在base配置裏新導出一個方法用以獲取postcss的規則。ajax
// ./src/webpack/base.ts ... export const getPostCssRule = (styleLoader) => ({ test: /\.pcss$/, use: [ styleLoader, { loader: 'css-loader', options: { camelCase: true, importLoaders: 1, localIdentName: '[path][name]---[local]---[hash:base64:5]', modules: true, }, }, { loader: 'postcss-loader', options: { plugins: () => [ require('postcss-import')({ path: path.join(baseDir, './src/client/style'), }), require('postcss-cssnext'), require('postcss-nested'), require('postcss-functions')({ functions: { x2(v, u) { return v * 2 + (u ? u : 'px'); }, }, }), ], }, }, ], }); ...
咱們能夠從上面這個方法看到,要處理.pcss
文件須要用到三個loader,按處理順序從下往上分別是postcss-loader, css-loader, 還有一個變量styleLoader,至於這個變量是什麼,咱們能夠看使用到該方法的地方:npm
// ./src/webpack/client.ts ... (clientDevConfig.module as webpack.NewModule).rules.push( ... getPostCssRule({ loader: 'style-loader', }), ... ); ...
// ./src/webpack/server.ts ... (clientDevConfig.module as webpack.NewModule).rules.push( ... getPostCssRule({ loader: 'isomorphic-style-loader', }), ... ); ...
客戶端和服務端處理樣式文件須要使用到不一樣的styleLoader。
PostCSS是一個使用js來轉換css的工具,這個是官方介紹。其配合webpack使用的loader就是postcss-loader,可是隻有單個postcss-loader其實沒有什麼用,須要配合其插件來實現強大的功能。
講這麼多,寫代碼舉個栗子吧~
咱們在client目錄下新增style文件夾,用於存放一些樣式reset,變量文件之類的東西。而後建立兩個pcss文件:
// ./src/client/style/variables.pcss :root { --fontSizeValue: 16; }
// ./src/client/style/index.pcss @import 'variables.pcss'; body { margin: 0; font-size: x2(var(--fontSizeValue)); }
引入咱們剛寫的index.pcss
// ./src/client/index.tsx ... import './style/index.pcss'; ...
簡單來講就是css模塊化,不用再擔憂全局類名的問題。咱們根據上述css-loader的options來看:
在客戶端,使用style-loader,它會動態的往dom裏插入style元素,而服務端因爲缺乏客戶端的相關對象及API,因此須要isomorphic-style-loader,目前用到它只是爲了不報錯哈哈,後續還有大做用,樣式直出全靠它。
注意:打包運行以前不要忘了給tsconfig.client.json和tsconfig.server.json引入咱們的自定義模塊定義文件index.d.ts,否則webpack編譯就會報找不到pcss這種模塊啦。
// ./src/webpack/tsconfig.client(server).json ... "include": [ ... "../../@types/**/*", ... ] ...
運行結果以下:
雖然style元素已經存在,可是這個是由style-loader生成的,並非服務端直出的,看page source就知道了。
並且在刷新頁面的時候能很明顯的看到樣式變化閃爍的效果。
咱們利用isomorphic-style-loader來實現服務端直出樣式,原理的話根據官方介紹就是利用了react的context api來實現,在服務端渲染的過程當中,利用注入的insertCss方法和高階組件(hoc high-order component)來獲取樣式代碼。
npm install prop-types --save-dev
根據其官方介紹,咱們在不使用其整合完畢的isomorphic router的狀況下,須要寫一個Provider給App組件:
// ./src/client/component/app/provider.tsx import * as React from 'react'; import * as PropTypes from 'prop-types'; class AppProvider extends React.PureComponent<any, any> { public static propTypes = { context: PropTypes.object, }; public static defaultProps = { context: { insertCss: () => '', }, }; public static childContextTypes = { insertCss: PropTypes.func.isRequired, }; public getChildContext() { return this.props.context; } public render() { return this.props.children || null; } } export default AppProvider;
將原App組件裏的具體內容遷移到AppContent組件裏去:
// ./src/client/component/app/content.tsx import * as React from 'react'; import * as styles from './style.pcss'; /* tslint:disable-next-line no-submodule-imports */ import withStyles from 'isomorphic-style-loader/lib/withStyles'; @withStyles(styles) class AppContent extends React.PureComponent { public render() { return ( <div className={styles.root}>hello world</div> ); } } export default AppContent;
新的App組件:
// ./src/client/component/app/index.tsx import * as React from 'react'; import AppProvider from './provider'; import AppContent from './content'; class App extends React.PureComponent { public render() { return ( <AppProvider> <AppContent /> </AppProvider> ); } } export default App;
疑問一:AppProvider組件是作什麼的?
答:Provider的意思是供應者,提供者
。顧名思義,AppProvider爲其後代組件提供了一些東西,這個東西就是context,它有一個insertCss方法。根據其定義,該方法擁有默認值,返回空字符串的函數,即默認沒什麼做用,可是能夠經過props傳入context來達到自定義的目的。經過設定childContextTypes和getChildContext,該組件後代凡是設定了contextTypes的組件都會擁有this.context對象,而這個對象正是getChildContext的返回值。
疑問二:AppContent爲什麼要獨立出去?
答:接上一疑問,AppProvider組件render其子組件,而要使得context這個api生效,其子組件必須是定義了contextTypes的,可是咱們並無看見AppContent有這個定義,這個是由於這個定義在高階組件withStyles裏面(參見其源碼)。
疑問三:@withStyles是什麼語法?
答:這個是裝飾器,屬於es7,具體概念內容可參見Decorators in ES7。使用該語法,須要配置tsconfig:
// ./tsconfig.json // ./src/webpack/tsconfig.client(server).json { ... "compilerOptions": { ... "experimentalDecorators": true, ... }, ... }
因爲App組件的改寫,服務端不能再複用該組件,可是AppProvider和AppContent目前仍是能夠複用的。
// ./src/server/bundle.tsx import * as React from 'react'; /* tslint:disable-next-line no-submodule-imports */ import { renderToString } from 'react-dom/server'; import AppProvider from '../client/component/app/provider'; import AppContent from '../client/component/app/content'; export default { render() { const css = []; const context = { insertCss: (...styles) => styles.forEach((s) => css.push(s._getCss())) }; const html = renderToString( <AppProvider context={context}> <AppContent /> </AppProvider>, ); const style = css.join(''); return { html, style, }; }, };
這裏咱們傳入了自定義的context對象,經過css這個變量來存儲style信息。咱們原先render函數直接返回renderToString的html字符串,而如今多了一個style,因此咱們返回擁有html和style屬性的對象。
疑問四:官方示例css是一個Set類型實例,這裏怎麼是一個數組類型實例?
答:Set是es6中新的數據結構,相似數組,但能夠保證無重複值,只有tsconfig的編譯選項中的target爲es6時,且加入es2017的lib時纔不會報錯,因爲咱們的target是es5,因此是數組,且使用數組並無太大問題。
因爲bundle的render值變動,因此咱們也要處理一下。
// ./src/server/index.tsx ... router.get('/*', (ctx: Koa.Context, next) => { // 配置一個簡單的get通配路由 const renderResult = bundle ? bundle.render() : {}; // 得到渲染出的結果對象 const { html = '', style = '' } = renderResult; ... ctx.body = ` ... <head> ... ${style ? `<style>${style}</style>` : ''} ... </head> ... `; ... }); ...
樣式直出後的page source:
從上面的直出結果來看,缺乏./src/style/index.pcss這個樣式代碼,緣由顯而易見,它不屬於任何一個組件,它是公共的,咱們在客戶端入口文件裏引入了它。對於公共樣式文件,服務端要直出這部份內容,能夠這麼作:
./src/server/bundle.tsx ... import * as commonStyles from '../client/style/index.pcss'; ... const css = [commonStyles._getCss()]; ...
咱們利用isomorphic-style-loader提供的api能夠獲得這部分樣式代碼字符串。這樣就能夠獲得完整的直出樣式了。
By devlee