淺談react 同構(gòu)之樣式直出
前言
上文講到通過(guò)同構(gòu)服務(wù)端渲染,可以直出html結(jié)構(gòu),雖然講解了樣式,圖片等靜態(tài)資源在服務(wù)端引入問(wèn)題的解決方案,但是并沒(méi)有實(shí)際進(jìn)行相關(guān)操作,這篇文章就講解一下如何讓樣式像html一樣直出。
PS: 直出,我的理解就是輸入url發(fā)起get請(qǐng)求訪問(wèn)服務(wù)端,直接得到完整響應(yīng)結(jié)果,而不是同過(guò)ajax異步去獲取。
React 同構(gòu)的關(guān)鍵要素
完善的 Compponent 屬性及生命周期與客戶端的 render 時(shí)機(jī)是 React 同構(gòu)的關(guān)鍵。
DOM 的一致性
在前后端渲染相同的 Compponent,將輸出一致的 Dom 結(jié)構(gòu)。
不同的生命周期
在服務(wù)端上 Component 生命周期只會(huì)到 componentWillMount,客戶端則是完整的。
客戶端 render 時(shí)機(jī)
同構(gòu)時(shí),服務(wù)端結(jié)合數(shù)據(jù)將 Component 渲染成完整的 HTML 字符串并將數(shù)據(jù)狀態(tài)返回給客戶端,客戶端會(huì)判斷是否可以直接使用或需要重新掛載。
以上便是 React 在同構(gòu)/服務(wù)端渲染的提供的基礎(chǔ)條件。在實(shí)際項(xiàng)目應(yīng)用中,還需要考慮其他邊角問(wèn)題,例如服務(wù)器端沒(méi)有 window 對(duì)象,需要做不同處理等。下面將通過(guò)在手Q家校群上的具體實(shí)踐,分享一些同構(gòu)的 Tips 及優(yōu)化成果
加入樣式文件
目前我們的項(xiàng)目中還不存在任何樣式文件,所以需要先寫(xiě)一個(gè),就給組件App寫(xiě)一個(gè)樣式文件吧。
安裝依賴
下面這些依賴都是后續(xù)會(huì)用到的,先安裝一下,下面會(huì)詳細(xì)講解每個(gè)依賴的作用。
npm install postcss-loader postcss-import postcss-cssnext postcss-nested postcss-functions css-loader style-loader isomorphic-style-loader --save-dev
創(chuàng)建.pcss文件
css文件的后綴是.css,less文件的后綴是.less,這里我選擇使用PostCSS配合其插件來(lái)寫(xiě)樣式,所以我就自己定義一個(gè)后綴.pcss好了。
// ./src/client/component/app/style.pcss .root { color: red; }
設(shè)定一個(gè)root類(lèi),樣式就是簡(jiǎn)單的設(shè)置顏色為紅色。然后在App組件里引用它。
// ./src/client/component/app/index.tsx ... import * as styles from './style.pcss'; ... public render() { return ( <div className={styles.root}>hello world</div> ); } ...
這個(gè)時(shí)候你會(huì)發(fā)現(xiàn)編輯器里是這樣的:
出現(xiàn)這個(gè)問(wèn)題是因?yàn)閠s不知道這種模塊的類(lèi)型定義,所以我們需要手動(dòng)加入自定義模塊類(lèi)型定義。在項(xiàng)目根目錄下新建@types文件夾,在此目錄下建立index.d.ts文件:
// ./@types/index.d.ts declare module '*.pcss' { const content: any; export = content; }
保存之后就不會(huì)看到編輯器報(bào)錯(cuò)了,但是terminal里webpack打包會(huì)提示出錯(cuò),因?yàn)槲覀冞€沒(méi)有加對(duì)應(yīng)的loader。
配置.pcss文件的解析規(guī)則
js都組件化了,css模塊化也是很有必要的,不用再為避免取重復(fù)類(lèi)名而煩惱。我們?cè)赽ase配置里新導(dǎo)出一個(gè)方法用以獲取postcss的規(guī)則。
// ./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'); }, }, }), ], }, }, ], }); ...
我們可以從上面這個(gè)方法看到,要處理 .pcss 文件需要用到三個(gè)loader,按處理順序從下往上分別是postcss-loader, css-loader, 還有一個(gè)變量styleLoader,至于這個(gè)變量是什么,我們可以看使用到該方法的地方:
// ./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', }), ... ); ...
客戶端和服務(wù)端處理樣式文件需要使用到不同的styleLoader。
PostCSS簡(jiǎn)介
PostCSS是一個(gè)使用js來(lái)轉(zhuǎn)換css的工具,這個(gè)是官方介紹。其配合webpack使用的loader就是postcss-loader,但是只有單個(gè)postcss-loader其實(shí)沒(méi)有什么用,需要配合其插件來(lái)實(shí)現(xiàn)強(qiáng)大的功能。
1、postcss-import
這個(gè)插件我這里使用的原因是為了在樣式文件中@import時(shí)避免復(fù)雜的路徑編寫(xiě),我設(shè)定好path值,那么我在其它任何層級(jí)下的樣式文件中要引入path對(duì)應(yīng)文件夾里的公共變量樣式文件(假設(shè)叫"variables.pcss")時(shí)就非常方便,只需要寫(xiě)import 'variables.pcss';就可以了,當(dāng)然如果找不到對(duì)應(yīng)的文件,它會(huì)忽略path使用默認(rèn)相對(duì)路徑來(lái)查找。
2、postcss-cssnext
這個(gè)插件可以使用下一代css語(yǔ)法。
3、postcss-nested
這個(gè)插件可以嵌套編寫(xiě)樣式。
4、postcss-functions
這個(gè)插件可以自定義函數(shù),并在樣式文件中調(diào)用。
講這么多,寫(xiě)代碼舉個(gè)栗子吧~
我們?cè)赾lient目錄下新增style文件夾,用于存放一些樣式reset,變量文件之類(lèi)的東西。然后創(chuàng)建兩個(gè)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)); }
引入我們剛寫(xiě)的index.pcss
// ./src/client/index.tsx ... import './style/index.pcss'; ...
CSS Modules簡(jiǎn)介
簡(jiǎn)單來(lái)說(shuō)就是css模塊化,不用再擔(dān)心全局類(lèi)名的問(wèn)題。我們根據(jù)上述css-loader的options來(lái)看:
- camelCase為true運(yùn)行使用駝峰寫(xiě)法來(lái)寫(xiě)類(lèi)名
- importLoaders的值為N是因?yàn)樵赾ss-loader之前有N個(gè)loader已經(jīng)處理過(guò)文件了,這里的N值是1,因?yàn)橹坝幸粋€(gè)postcss-loader,這個(gè)值一定要設(shè)置對(duì),否則會(huì)影響@import語(yǔ)句,我的這個(gè)表述可能不是太正確,詳細(xì)可參見(jiàn) Clarify importLoaders documentation? 這個(gè)地方詳細(xì)講解了,我翻譯一下大概意思是,這個(gè)屬性的值N代表的是對(duì)于@import的文件要經(jīng)過(guò)css-loader后面的N個(gè)loader的處理,英文不太好,大家可以自行理解。
- localIdentName這個(gè)就是指生成的類(lèi)名啦,具體看后續(xù)結(jié)果截圖就一目了然了。
- modules為true即啟用模塊化
isomorphic-style-loader
在客戶端,使用style-loader,它會(huì)動(dòng)態(tài)的往dom里插入style元素,而服務(wù)端由于缺少客戶端的相關(guān)對(duì)象及API,所以需要isomorphic-style-loader,目前用到它只是為了避免報(bào)錯(cuò)哈哈,后續(xù)還有大作用,樣式直出全靠它。
打包運(yùn)行
注意:打包運(yùn)行之前不要忘了給tsconfig.client.json和tsconfig.server.json引入我們的自定義模塊定義文件index.d.ts,不然webpack編譯就會(huì)報(bào)找不到pcss這種模塊啦。
// ./src/webpack/tsconfig.client(server).json ... "include": [ ... "../../@types/**/*", ... ] ...
運(yùn)行結(jié)果如下:
雖然style元素已經(jīng)存在,但是這個(gè)是由style-loader生成的,并不是服務(wù)端直出的,看page source就知道了。
而且在刷新頁(yè)面的時(shí)候能很明顯的看到樣式變化閃爍的效果。
直出樣式
我們利用isomorphic-style-loader來(lái)實(shí)現(xiàn)服務(wù)端直出樣式,原理的話根據(jù)官方介紹就是利用了react的context api來(lái)實(shí)現(xiàn),在服務(wù)端渲染的過(guò)程中,利用注入的insertCss方法和高階組件(hoc high-order component)來(lái)獲取樣式代碼。
安裝依賴
npm install prop-types --save-dev
改寫(xiě)App組件
根據(jù)其官方介紹,我們?cè)诓皇褂闷湔贤戤叺膇somorphic router的情況下,需要寫(xiě)一個(gè)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組件里的具體內(nèi)容遷移到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;
疑問(wèn)一:AppProvider組件是做什么的?
答:Provider的意思是 供應(yīng)者,提供者 。顧名思義,AppProvider為其后代組件提供了一些東西,這個(gè)東西就是context,它有一個(gè)insertCss方法。根據(jù)其定義,該方法擁有默認(rèn)值,返回空字符串的函數(shù),即默認(rèn)沒(méi)什么作用,但是可以通過(guò)props傳入context來(lái)達(dá)到自定義的目的。通過(guò)設(shè)定childContextTypes和getChildContext,該組件后代凡是設(shè)定了contextTypes的組件都會(huì)擁有this.context對(duì)象,而這個(gè)對(duì)象正是getChildContext的返回值。
疑問(wèn)二:AppContent為何要獨(dú)立出去?
答:接上一疑問(wèn),AppProvider組件render其子組件,而要使得context這個(gè)api生效,其子組件必須是定義了contextTypes的,但是我們并沒(méi)有看見(jiàn)AppContent有這個(gè)定義,這個(gè)是因?yàn)檫@個(gè)定義在高階組件withStyles里面(參見(jiàn)其 源碼 )。
疑問(wèn)三:@withStyles是什么語(yǔ)法?
答:這個(gè)是裝飾器,屬于es7。使用該語(yǔ)法,需要配置tsconfig:
// ./tsconfig.json // ./src/webpack/tsconfig.client(server).json { ... "compilerOptions": { ... "experimentalDecorators": true, ... }, ... }
改寫(xiě)服務(wù)端bundle文件
由于App組件的改寫(xiě),服務(wù)端不能再?gòu)?fù)用該組件,但是AppProvider和AppContent目前還是可以復(fù)用的。
// ./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對(duì)象,通過(guò)css這個(gè)變量來(lái)存儲(chǔ)style信息。我們?cè)萺ender函數(shù)直接返回renderToString的html字符串,而現(xiàn)在多了一個(gè)style,所以我們返回?fù)碛衕tml和style屬性的對(duì)象。
疑問(wèn)四:官方示例css是一個(gè)Set類(lèi)型實(shí)例,這里怎么是一個(gè)數(shù)組類(lèi)型實(shí)例?
答:Set是es6中新的數(shù)據(jù)結(jié)構(gòu),類(lèi)似數(shù)組,但可以保證無(wú)重復(fù)值,只有tsconfig的編譯選項(xiàng)中的target為es6時(shí),且加入es2017的lib時(shí)才不會(huì)報(bào)錯(cuò),由于我們的target是es5,所以是數(shù)組,且使用數(shù)組并沒(méi)有太大問(wèn)題。
處理服務(wù)端入口文件
由于bundle的render值變更,所以我們也要處理一下。
// ./src/server/index.tsx ... router.get('/*', (ctx: Koa.Context, next) => { // 配置一個(gè)簡(jiǎn)單的get通配路由 const renderResult = bundle ? bundle.render() : {}; // 獲得渲染出的結(jié)果對(duì)象 const { html = '', style = '' } = renderResult; ... ctx.body = ` ... <head> ... ${style ? `<style>${style}</style>` : ''} ... </head> ... `; ... }); ...
直出結(jié)果
樣式直出后的page source:
找回丟失的公共樣式文件
從上面的直出結(jié)果來(lái)看,缺少./src/style/index.pcss這個(gè)樣式代碼,原因顯而易見(jiàn),它不屬于任何一個(gè)組件,它是公共的,我們?cè)诳蛻舳巳肟谖募镆肓怂?。?duì)于公共樣式文件,服務(wù)端要直出這部分內(nèi)容,可以這么做:
./src/server/bundle.tsx ... import * as commonStyles from '../client/style/index.pcss'; ... const css = [commonStyles._getCss()]; ...
我們利用isomorphic-style-loader提供的api可以得到這部分樣式代碼字符串。這樣就可以得到完整的直出樣式了。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
React-Native之TextInput組件的設(shè)置以及如何獲取輸入框的內(nèi)容
這篇文章主要介紹了React-Native之TextInput組件的設(shè)置以及如何獲取輸入框的內(nèi)容問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05react-native 配置@符號(hào)絕對(duì)路徑配置和絕對(duì)路徑?jīng)]有提示的問(wèn)題
本文主要介紹了react-native 配置@符號(hào)絕對(duì)路徑配置和絕對(duì)路徑?jīng)]有提示的問(wèn)題,文中通過(guò)圖文示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01在create-react-app中使用css modules的示例代碼
這篇文章主要介紹了在create-react-app中使用css modules的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07基于React.js實(shí)現(xiàn)簡(jiǎn)單的文字跑馬燈效果
剛好手上有一個(gè)要實(shí)現(xiàn)文字跑馬燈的react項(xiàng)目,然后ant-design上面沒(méi)有這個(gè)組件,于是只能自己手?jǐn)]一個(gè),文中的實(shí)現(xiàn)方法講解詳細(xì),希望對(duì)大家有所幫助2023-01-01React實(shí)現(xiàn)多標(biāo)簽在有限空間內(nèi)展示
在業(yè)務(wù)中,需要在一個(gè)卡片組件中展示多個(gè)標(biāo)簽,標(biāo)簽組件高度相同,寬度和出現(xiàn)順序不同,要求標(biāo)簽只能在有限的空間內(nèi)展示,所以本文給大家介紹了React實(shí)現(xiàn)多標(biāo)簽在有限空間內(nèi)展示,需要的朋友可以參考下2023-12-12react-native滑動(dòng)吸頂效果的實(shí)現(xiàn)過(guò)程
這篇文章主要給大家介紹了關(guān)于react-native滑動(dòng)吸頂效果的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用react-native具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06