首頁 > web前端 > js教程 > 淺談react 同構之樣式直出

淺談react 同構之樣式直出

小云云
發布: 2017-12-26 13:47:51
原創
2245 人瀏覽過

之前我們講到透過同構服務端渲染,可以直出html結構,雖然講解了樣式,圖片等靜態資源在服務端引入問題的解決方案,但是並沒有實際進行相關操作,這篇文章就講解一下如何讓樣式像html一樣直出。本文主要介紹了淺談react 同構之樣式直出,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟著小編過來看看吧,希望能幫助大家。

PS: 直出,我的理解就是輸入url發起get請求存取服務端,直接得到完整回應結果,而不是同過ajax非同步去獲取。

React 同構的關鍵要素

完善的 Compponent 屬性及生命週期與客戶端的 render 時機是 React 同構的關鍵。

DOM 的一致性

在前後端渲染相同的 Compponent,將輸出一致的 Dom 結構。

不同的生命週期

在服務端上 Component 生命週期只會到 componentWillMount,客戶端則是完整的。

客戶端render 時機

同構時,服務端結合資料將Component 渲染成完整的HTML 字串並將資料狀態傳回給客戶端,客戶端會判斷是否可以直接使用或需要重新掛載。

以上便是 React 在同構/服務端渲染的提供的基礎條件。在實際專案應用中,還需要考慮其他角落問題,例如伺服器端沒有 window 對象,需要做不同處理等。以下將透過在手Q家校群上的具體實踐,分享一些同構的Tips 及優化成果

加入樣式文件

目前我們的專案中還不存在任何樣式文件,所以要先寫一個,就給元件App寫一個樣式檔吧。

安裝依賴

下面這些依賴都是後續會用到的,先安裝一下,下面會詳細講解每個依賴的作用。

複製程式碼如下:


npm install postcss-loader postcss-import postcss-cssnext postcss-nested postcss-functions css-loader style-loader isomorphic-style-loader -- save-dev

創建.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 (
   <p className={styles.root}>hello world</p>
  );
 }
...
登入後複製

這時候你會發現編輯器裡是這樣的:

出現這個問題是因為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來看:

  1. camelCase为true运行使用驼峰写法来写类名

  2. importLoaders的值为N是因为在css-loader之前有N个loader已经处理过文件了,这里的N值是1,因为之前有一个postcss-loader,这个值一定要设置对,否则会影响@import语句,我的这个表述可能不是太正确,详细可参见 Clarify importLoaders documentation? 这个地方详细讲解了,我翻译一下大概意思是,这个属性的值N代表的是对于@import的文件要经过css-loader后面的N个loader的处理,英文不太好,大家可以自行理解。

  3. localIdentName这个就是指生成的类名啦,具体看后续结果截图就一目了然了。

  4. modules为true即启用模块化

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 (
   <p className={styles.root}>hello world</p>
  );
 }
}

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可以得到这部分样式代码字符串。这样就可以得到完整的直出样式了。

相关推荐:

Node直出理论与实践总结_html/css_WEB-ITnose

详解React、ajax、java实现上传图片并预览功能

详解在React 组件中使用Echarts的正确姿势

以上是淺談react 同構之樣式直出的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板