webpack实现SSR

SSR的一些概念,本文不做阐述,直接开始配置

  • ssr的文件打包配置,与普通的webpack打包没有什么区别。 以下是一份多页面打包的逻辑,注意output的libraryTarget需要设置为umd,并且为了解决服务端不会处理css样式的问题,我们需要对打包使用的html模板进行一些特殊处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    let path = require('path');
    let HtmlWebpackPlugin = require('html-webpack-plugin')
    let {CleanWebpackPlugin} = require('clean-webpack-plugin')
    let MiniCssExtractPlugin = require('mini-css-extract-plugin')
    let OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    let CopyWebpackPlugin = require('copy-webpack-plugin')
    let HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
    let glob = require('glob')

    const setMPA = () => {
    const entry = {};
    const htmlWebpackPlugins = [];
    const entryFiles = glob.sync(path.join(__dirname, './src/*/index-server.js'))
    Object.keys(entryFiles)
    .map((index)=>{
    const entryFile = entryFiles[index];
    const match = entryFile.match(/src\/(.*)\/index-server.js/)
    const pageName = match && match[1];
    if(pageName){
    entry[pageName] = entryFile;
    htmlWebpackPlugins.push(
    new HtmlWebpackPlugin({
    template: `./src/${pageName}/index-server.html`,
    filename: `${pageName}-server.html`,
    chunks: ['commons', pageName],
    title: 'webpack-learn',
    inject: true,
    minify: {
    html5: true,
    removeAttributeQuotes: true,
    collapseWhitespace: true,
    },
    hash: true
    }),
    )
    }
    })

    return {
    entry,
    htmlWebpackPlugins
    }
    }
    let {entry, htmlWebpackPlugins} = setMPA()

    module.exports = {
    entry,
    output: {
    filename: '[name]-server.js',
    path: path.resolve('./dist'),
    libraryTarget: 'umd'
    },
    module: {
    rules: [{
    test: /\.js$/,
    use: ['babel-loader']
    }, {
    test: /\.less$/,
    use: [MiniCssExtractPlugin.loader, {
    loader: 'css-loader'
    }, {
    loader: 'less-loader'
    }, {
    loader: 'postcss-loader',
    options: {
    plugins: ()=>[
    require('autoprefixer')({
    overrideBrowserslist: [
    'last 2 version',
    '>1%',
    'IE 10',
    'ios 7'
    ]
    })
    ]
    }
    }, {
    loader: 'px2rem-loader',
    options: {
    remUnit: 75,
    remPrecision: 8
    }
    }]
    }, {
    test: /\.(png|jpg|gif|jpeg|svg)$/,
    use: [{
    loader: 'url-loader',
    options: {
    limit: 1024 * 3
    }
    }]
    }, {
    test: /\.(otf|woff|woff2|eot|ttf)$/,
    use: [{
    loader: 'file-loader',
    options: {
    name: '[name]_[hash:8].[ext]'
    }
    }]
    }]
    },
    plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin([{
    from: './src/doc',
    to: 'public'
    }]),
    new MiniCssExtractPlugin({
    filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
    assetNameRegExp: /\.css$/g,
    cssProcessor: require('cssnano')
    }),
    new HtmlWebpackExternalsPlugin({
    externals: [{
    module: 'react',
    entry: 'https://unpkg.com/react@16/umd/react.production.min.js',
    global: 'React'
    }, {
    module: 'react-dom',
    entry: 'https://unpkg.com/react-dom@16/umd/react-dom.production.min.js',
    global: 'ReactDOM'
    }]
    }),
    ...htmlWebpackPlugins
    ],
    mode: 'production',
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!DOCTYPE html>
    <html lang="en">
    <head>
    ${require('raw-loader!./meta.html')}
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><%=htmlWebpackPlugin.options.title%></title>
    <script type="text/javascript" src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
    <script type="text/javascript" src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

    <script type="text/javascript">
    ${require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')}
    </script>
    </head>
    <body>
    <div id="root"><!--HTML_PLACEHOLDER--></div>
    </body>
    </html>

    HTML模板中添加了<!--HTML_PLACEHOLDER-->这段注释,等到服务端渲染时,会替换打包生成的html文件中的这部分的内容。

  • 最主要的区别,在于入口文件的代码编写上。入口文件的代码实例如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // import React from 'react';
    // import logo from './images/logo.svg'
    // import {a} from './tree-shaking';
    // import './style.less'
    // import './search.less'
    // import largeNumber from 'large-number-fx'
    const React = require('react');
    const logo = require('./images/logo.svg')
    const {a} = require('./tree-shaking') ;
    require('./style.less')
    require('./search.less')
    const largeNumber = require('large-number-fx')

    //模块热更新
    if(module.hot){
    module.hot.accept()
    }

    console.log(a())


    class Search extends React.Component{

    constructor(props){
    super(props);
    this.state = {
    Text: null
    }
    }

    loadComponent = () =>{
    import('./text.js').then((Text)=>{
    this.setState({
    Text: Text.default
    })
    })
    }

    render(){
    let { Text} = this.state
    let number = largeNumber('1111111111111111111', '2222222222222')
    return (
    <div className="search-text">
    Search Text 搜索组件
    <button onClick={this.loadComponent}>load text</button>
    <h1>{number}</h1>
    <img src={logo} />
    {Text? <Text />: null}
    </div>
    )
    }
    }
    module.exports = <Search/>;
    // ReactDOM.render(<Search/>, document.getElementById('root'))

    注意以上代码注释的部分:

    1. import 的语法需要全部更改为 cjs 的 require()语法
    2. ReactDOM.render()的方法需要更改为直接暴露出 <Search />组件即可
  • 我们需要新建一个server.js的文件,启动nodejs的server,来处理路由逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //避免报错
    if(typeof window === 'undefined'){
    global.window = {};
    }
    const fs = require('fs')
    const path = require('path')

    const express = require('express')
    const { renderToString } = require('react-dom/server')
    const SSR = require('../dist/app-server')
    const template = fs.readFileSync(path.join(__dirname, '../dist/app-server.html'), 'UTF-8')

    const server = (port) => {
    const app = express();
    app.use(express.static('dist'));
    app.get('/search', (req, res)=>{
    const html = renderMarkup(renderToString(SSR));
    res.status(200).send(html)
    })

    app.listen(port, ()=>{
    console.log('Server is running on port: ' + port)
    })
    }

    const renderMarkup = (str)=>{
    return template.replace('<!--HTML_PLACEHOLDER-->', str)
    }

    server(process.env.PORT || 3000)

    server.js的基本逻辑是:首先我们需要引用react-dom/server下的renderToString方法,用这个方法去解析我们打包后的输出文件,得到组件的一些html片段,在将打包后的html文件中的<!--HTML_PLACEHOLDER-->替换成该代码片段即可,这里使用fs去读取打包后的html文件是为了,保留之前css以及js的一些引用,使页面的样式可以正常显示。