美文网首页
isomorphic-style-loader探究

isomorphic-style-loader探究

作者: 前端大飞 | 来源:发表于2019-11-15 12:06 被阅读0次

    isomorphic-style-loader基础配置

    {
        test: /.iso.scss/,
        use:[
            {
                loader: 'isomorphic-style-loader'
            },
            {
                loader: 'css-loader',
                options: {} // **自定义option
            }
        ]
    },
    

    也就是isomorphic-style-loader一定接收的是css-loader处理后的文件。

    作用:

    isomorphic-style-loader是webpack的loader,将css文件可以转化为style标签插入到html的header头中,实现server render的critical css的插入,提升页面的体验.</br>
    你肯定想起了之前很火的style-loader不是也是这种作用么?是的。isomorphic-style-loader是React版本,支持server side render. 其他的实现原理是一样的.

    使用demo case github传送门

    基础组件App

    /* App.scss */
    .root { padding: 10px }
    .title { color: red }
    /* App.js */
    import React, { Component } from 'react'
    import withStyles from 'isomorphic-style-loader/withStyles'
    import s from './App.iso.scss'
    
    @withStyles(s)
    class App extends Component {
        render() {
            return (<div className={s.root}>
                <h1 className={s.title}>Hello, world!</h1>
            </div>);
        }
    }
    
    export default App;
    

    因为isomorphic-style-loader是将css文件转为js的方式进行加载的,天然支持了CSS Modules.

    实现ssr,自然,首先在服务端,渲染出带有style标签的html

    import express from 'express'
    import React from 'react'
    import ReactDOM from 'react-dom'
    import StyleContext from 'isomorphic-style-loader/StyleContext'
    import App from './App.js'
    
    const server = express()
    const port = process.env.PORT || 3000
    
    // Server-side rendering of the React app
    server.get('*', (req, res, next) => {
      const css = new Set() // CSS for all rendered React components
      const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()))
      const body = ReactDOM.renderToString(
        <StyleContext.Provider value={{ insertCss }}>
          <App />
        </StyleContext.Provider>
      )
      const html = `<!doctype html>
        <html>
          <head>
            <script src="client.js" defer></script>
            <style>${[...css].join('')}</style>
          </head>
          <body>
            <div id="root">${body}</div>
          </body>
        </html>`
      res.status(200).send(html)
    })
    
    // xxx
    

    它就会输出如下的html片段

    <html>
      <head>
        <title>My Application</title>
        <script async src="/client.js"></script>
        <style type="text/css">
          .App_root_Hi8 { padding: 10px }
          .App_title_e9Q { color: red }
        </style>
      </head>
      <body>
        <div id="root">
          <div class="App_root_Hi8">
            <h1 class="App_title_e9Q">Hello, World!</h1>
          </div>
        </div>
      </body>
    </html>
    

    然后在client端进行渲染hydrate

    import React from 'react'
    import ReactDOM from 'react-dom'
    import StyleContext from 'isomorphic-style-loader/StyleContext'
    import App from './App.js'
    
    const insertCss = (...styles) => {
      const removeCss = styles.map(style => style._insertCss())
      return () => removeCss.forEach(dispose => dispose())
    }
    
    ReactDOM.hydrate(
      <StyleContext.Provider value={{ insertCss }}>
        <App />
      </StyleContext.Provider>,
      document.getElementById('root')
    )
    

    这样执行的结果是在client端每个引入的css文件会都以style标签的形式进行单独插入。

    原理分析

    我们一定好奇,上述代码中引用的style._getCss(),style._insertCss是哪里提供的,为什么这样使用.
    somorphic-style-loader的源码就是你想要的答案

    isomorphic-style-loader提供了5个文件.


    isomorphic1.png

    其中index.js毫无以为是webpack配置中的loader主文件,我们留在最后再进行分析,下面我们一次分析其他几个文件

    StyleContext.js
    import React from 'react'
    
    const StyleContext = React.createContext({
      insertCss: null,
    })
    
    export default StyleContext
    

    作用很简单,提供了一个Context,它的默认值是一个有一个insertCss方法的object.
    看到这里,就很容易理解上文中引用的StyleContext.Provider,为什么要提供一个insertCss方法了.

    withStyle.js
    import React from 'react'
    import hoistStatics from 'hoist-non-react-statics'
    
    import StyleContext from './StyleContext'
    
    function withStyles(...styles) {
      return function wrapWithStyles(ComposedComponent) {
        class WithStyles extends React.PureComponent {
          constructor(props, context) {
            super(props, context)
            this.removeCss = context.insertCss(...styles)
          }
    
          componentWillUnmount() {
            if (this.removeCss) {
              setTimeout(this.removeCss, 0)
            }
          }
    
          render() {
            return <ComposedComponent {...this.props} />
          }
        }
    
        const displayName = ComposedComponent.displayName || ComposedComponent.name || 'Component'
    
        WithStyles.displayName = `WithStyles(${displayName})`
        WithStyles.contextType = StyleContext
        WithStyles.ComposedComponent = ComposedComponent
    
        return hoistStatics(WithStyles, ComposedComponent)
      }
    }
    
    export default withStyles
    

    可以看到WithStyle就是一个HOC,在child子组件的基础上,增加了context以及在constructor中就执行了context的insertCss方法.
    不熟悉的同学,可以看一下react中的Context[图片上传失败...(image-ce3341-1573790756740)]

    insertCss.js

    这个文件就不详细分析了,就是做了一件事儿,根据传入的styles数组,插入到html的header标签中

    useStyle.js

    这个文件干的其实和withStyle.js一样的事儿,支持是支持react hook的函数式写法

    index.js --- 重要
    import { stringifyRequest } from 'loader-utils'
    
    module.exports = function loader() {}
    module.exports.pitch = function pitch(request) {
      if (this.cacheable) {
        this.cacheable()
      }
    
      const insertCss = require.resolve('./insertCss.js')
      return `
        var refs = 0;
        var css = require(${stringifyRequest(this, `!!${request}`)});
        var insertCss = require(${stringifyRequest(this, `!${insertCss}`)});
        var content = typeof css === 'string' ? [[module.id, css, '']] : css;
    
        exports = module.exports = css.locals || {};
        exports._getContent = function() { return content; };
        exports._getCss = function() { return '' + css; };
        exports._insertCss = function(options) { return insertCss(content, options) };
    
        // Hot Module Replacement
        // https://webpack.github.io/docs/hot-module-replacement
        // Only activated in browser context
        if (module.hot && typeof window !== 'undefined' && window.document) {
          var removeCss = function() {};
          module.hot.accept(${stringifyRequest(this, `!!${request}`)}, function() {
            css = require(${stringifyRequest(this, `!!${request}`)});
            content = typeof css === 'string' ? [[module.id, css, '']] : css;
            removeCss = insertCss(content, { replace: true });
          });
          module.hot.dispose(function() { removeCss(); });
        }
      `
    }
    
    1. stringifyRequest方法,作用是将引用的绝对路径转化为相对路径.
      我本地项目的一个路径,request原始值为/Users/zhifeizhang/pinduoduo_proj/cart2/cart/node_modules/css-loader/index.js??ref--6-rules-1!/Users/zhifeizhang/pinduoduo_proj/cart2/cart/node_modules/sass-loader/lib/loader.js??ref--6-rules-3!/Users/zhifeizhang/pinduoduo_proj/cart2/cart/cart/promotion/page.scss 转化后变为 "!!../../node_modules/css-loader/index.js??ref--6-rules-1!../../node_modules/sass-loader/lib/loader.js??ref--6-rules-3!./page.scss"
    2. pitch方法. 我们知道webpack的loader的执行顺序,默认从右到左,但是在loader中有pitch方法的时候,其实是从左向右一次执行pitch方法的,也就是会先执行isomorphic-style-loader.另外,当pitch方法有返回值的时候,后面的loader是不会执行的。

    那么,css-loader是怎么起作用的呢?

    var css = require(${stringifyRequest(this, `!!${request}`)});
    

    这个转化后的值,实际类似的是这样的, ``"!!../../node_modules/css-loader/index.js??ref--6-rules-1!../../node_modules/sass-loader/lib/loader.js??ref--6-rules-3!./page.scss"`也就是说通过这种方式直接调用的css-loader下的index.js文件进行编译的xxx.css

    1. _getCss以及_insertCss
      这里的_getCss就是将样式css进行require进来,然后我们再回头看看server端StyleContext传入的value insertCss方法,执行的就是对每一个style执行_getCss(),进行收集css set,然后在server端html里进行输出style,这样就理清楚了。</br>
      _insertCss,同样这个方法其实执行的是style标签进行插入。这个方法,我们可以看到在client端指定的就是
    const insertCss = (...styles) => {
      const removeCss = styles.map(style => style._insertCss())
      return () => removeCss.forEach(dispose => dispose())
    }
    

    至于返回值removeCss,可以看到是在componentWillUnmount中会执行,进行删除对应的style标签

    参考材料

    loader-utils
    isomorphic-style-loader
    webpack loader从上手到理解系列
    webpack的pitching loader

    相关文章

      网友评论

          本文标题:isomorphic-style-loader探究

          本文链接:https://www.haomeiwen.com/subject/jbwjictx.html