上文讲到经过同构服务端渲染,能够直出html结构,虽然讲解了样式,图片等静态资源在服务端引入问题的解决方案,可是并无实际进行相关操做,这篇文章就讲解一下如何让样式像html同样直出。css
PS: 直出,个人理解就是输入url发起get请求访问服务端,直接获得完整响应结果,而不是同过ajax异步去获取。
html
目前咱们的项目中还不存在任何样式文件,因此须要先写一个,就给组件App写一个样式文件吧。react
下面这些依赖都是后续会用到的,先安装一下,下面会详细讲解每一个依赖的做用。webpack
npm install postcss-loader postcss-import postcss-cssnext postcss-nested postcss-functions css-loader style-loader isomorphic-style-loader --save-dev
css文件的后缀是.css,less文件的后缀是.less,这里我选择使用PostCSS配合其插件来写样式,因此我就本身定义一个后缀.pcss好了。git
// ./src/client/component/app/style.pcss .root { color: red; }
设定一个root类,样式就是简单的设置颜色为红色。而后在App组件里引用它。es6
// ./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文件:github
// ./@types/index.d.ts declare module '*.pcss' { const content: any; export = content; }
保存以后就不会看到编辑器报错了,可是terminal里webpack打包会提示出错,由于咱们尚未加对应的loader。web
js都组件化了,css模块化也是颇有必要的,不用再为避免取重复类名而烦恼。咱们在base配置里新导出一个方法用以获取postcss的规则。ajax
// ./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,至于这个变量是什么,咱们能够看使用到该方法的地方:npm
// ./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是一个使用js来转换css的工具,这个是官方介绍。其配合webpack使用的loader就是postcss-loader,可是只有单个postcss-loader其实没有什么用,须要配合其插件来实现强大的功能。
讲这么多,写代码举个栗子吧~
咱们在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模块化,不用再担忧全局类名的问题。咱们根据上述css-loader的options来看:
在客户端,使用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
根据其官方介绍,咱们在不使用其整合完毕的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,具体概念内容可参见Decorators in ES7。使用该语法,须要配置tsconfig:
// ./tsconfig.json // ./src/webpack/tsconfig.client(server).json { ... "compilerOptions": { ... "experimentalDecorators": true, ... }, ... }
因为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能够获得这部分样式代码字符串。这样就能够获得完整的直出样式了。
By devlee