您好,登錄后才能下訂單哦!
前言
上文講到通過同構服務端渲染,可以直出html結構,雖然講解了樣式,圖片等靜態資源在服務端引入問題的解決方案,但是并沒有實際進行相關操作,這篇文章就講解一下如何讓樣式像html一樣直出。
PS: 直出,我的理解就是輸入url發起get請求訪問服務端,直接得到完整響應結果,而不是同過ajax異步去獲取。
React 同構的關鍵要素
完善的 Compponent 屬性及生命周期與客戶端的 render 時機是 React 同構的關鍵。
DOM 的一致性
在前后端渲染相同的 Compponent,將輸出一致的 Dom 結構。
不同的生命周期
在服務端上 Component 生命周期只會到 componentWillMount,客戶端則是完整的。
客戶端 render 時機
同構時,服務端結合數據將 Component 渲染成完整的 HTML 字符串并將數據狀態返回給客戶端,客戶端會判斷是否可以直接使用或需要重新掛載。
以上便是 React 在同構/服務端渲染的提供的基礎條件。在實際項目應用中,還需要考慮其他邊角問題,例如服務器端沒有 window 對象,需要做不同處理等。下面將通過在手Q家校群上的具體實踐,分享一些同構的 Tips 及優化成果
加入樣式文件
目前我們的項目中還不存在任何樣式文件,所以需要先寫一個,就給組件App寫一個樣式文件吧。
安裝依賴
下面這些依賴都是后續會用到的,先安裝一下,下面會詳細講解每個依賴的作用。
創建.pcss文件
css文件的后綴是.css,less文件的后綴是.less,這里我選擇使用PostCSS配合其插件來寫樣式,所以我就自己定義一個后綴.pcss好了。
// ./src/client/component/app/style.pcss .root { color: red; }
設定一個root類,樣式就是簡單的設置顏色為紅色。然后在App組件里引用它。
// ./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文件:
// ./@types/index.d.ts declare module '*.pcss' { const content: any; export = content; }
保存之后就不會看到編輯器報錯了,但是terminal里webpack打包會提示出錯,因為我們還沒有加對應的loader。
配置.pcss文件的解析規則
js都組件化了,css模塊化也是很有必要的,不用再為避免取重復類名而煩惱。我們在base配置里新導出一個方法用以獲取postcss的規則。
// ./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,至于這個變量是什么,我們可以看使用到該方法的地方:
// ./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簡介
PostCSS是一個使用js來轉換css的工具,這個是官方介紹。其配合webpack使用的loader就是postcss-loader,但是只有單個postcss-loader其實沒有什么用,需要配合其插件來實現強大的功能。
1、postcss-import
這個插件我這里使用的原因是為了在樣式文件中@import時避免復雜的路徑編寫,我設定好path值,那么我在其它任何層級下的樣式文件中要引入path對應文件夾里的公共變量樣式文件(假設叫"variables.pcss")時就非常方便,只需要寫import 'variables.pcss';就可以了,當然如果找不到對應的文件,它會忽略path使用默認相對路徑來查找。
2、postcss-cssnext
這個插件可以使用下一代css語法。
3、postcss-nested
這個插件可以嵌套編寫樣式。
4、postcss-functions
這個插件可以自定義函數,并在樣式文件中調用。
講這么多,寫代碼舉個栗子吧~
我們在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 Modules簡介
簡單來說就是css模塊化,不用再擔心全局類名的問題。我們根據上述css-loader的options來看:
isomorphic-style-loader
在客戶端,使用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
改寫App組件
根據其官方介紹,我們在不使用其整合完畢的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。使用該語法,需要配置tsconfig:
// ./tsconfig.json // ./src/webpack/tsconfig.client(server).json { ... "compilerOptions": { ... "experimentalDecorators": true, ... }, ... }
改寫服務端bundle文件
由于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可以得到這部分樣式代碼字符串。這樣就可以得到完整的直出樣式了。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。