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的插入,提升页面的体验.
你肯定想起了之前很火的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 (
Hello, world!
);
}
}
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(
)
const html = `
${body}
`
res.status(200).send(html)
})
// xxx
它就会输出如下的html片段
My Application
Hello, World!
然后在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(
,
document.getElementById('root')
)
这样执行的结果是在client端每个引入的css文件会都以style标签的形式进行单独插入。
原理分析
我们一定好奇,上述代码中引用的style._getCss(),style._insertCss是哪里提供的,为什么这样使用.
somorphic-style-loader的源码就是你想要的答案
isomorphic-style-loader提供了5个文件.
其中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
}
}
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(); });
}
`
}
- 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"
- 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
- _getCss以及_insertCss
这里的_getCss就是将样式css进行require进来,然后我们再回头看看server端StyleContext传入的value insertCss方法,执行的就是对每一个style执行_getCss(),进行收集css set,然后在server端html里进行输出style,这样就理清楚了。
_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