gregberge / loadable-components

The recommended Code Splitting library for React ✂️✨
https://loadable-components.com
MIT License
7.65k stars 380 forks source link

Script tags append again when they have already appended, And when you jump to a page, publicpath doesn't work #555

Closed Yanchenyu closed 4 years ago

Yanchenyu commented 4 years ago

🐛 Bug Report

Hi, i have create a React SSR Project, Here are the version numbers of some tools

"react": "^16.13.1",
 "react-dom": "^16.13.1",
 "react-router-dom": "^5.1.2",
"webpack": "^4.42.1",
"egg": "^2.11.2",
 "egg-view-nunjucks": "^2.2.0",
"@loadable/babel-plugin": "^5.12.0",
"@loadable/component": "^5.12.0",
"@loadable/server": "^5.12.0",
"@loadable/webpack-plugin": "^5.12.0",

here are some code snippets

node part: (i used egg, just like koa2)

const path = require('path');
const React = require('react');
const Controller = require('../core/baseController');
const { StaticRouter } = require('react-router-dom');
const ReactDOMServer = require('react-dom/server');
const { ChunkExtractor } = require('@loadable/server');

const nodeStats = path.resolve(__dirname, '../../dist/server/loadable-stats.json');
const webStats = path.resolve(__dirname, '../../dist/client/loadable-stats.json');

class TestController extends Controller {
    async index() {

        const arr = this.ctx.url.split('/m/flights/');
        let location;
        if (arr.length > 1) {
            location = arr[1].split('/')[0];
        }

        const nodeExtractor = new ChunkExtractor({ 
            statsFile: nodeStats,
            entrypoints: ["index", location],
            publicPath: 'https://webresource.english.c-ctrip.com/ares2/ibu/booking-flight-h5/1.0.1/default/'
        });
        const { default: App } = nodeExtractor.requireEntrypoint();

        const webExtractor = new ChunkExtractor({ 
            statsFile: webStats,
            entrypoints: ["index", location],
            publicPath: 'https://webresource.english.c-ctrip.com/ares2/ibu/booking-flight-h5/1.0.1/default/'
        });

        const staticRouterComponent = React.createElement(
            StaticRouter,
            {
                location: this.ctx.url,
                context: this.ctx.context || {}
            },
            React.createElement(App)
        );

        const jsx = webExtractor.collectChunks(staticRouterComponent);

        const resources = {
            scripts: webExtractor.getScriptTags(),
            styles: webExtractor.getStyleTags(),
            links: webExtractor.getLinkTags()
        };

        await this.ctx.render(location, {
            locale: 'zh',
            context: this.ctx.context || {},
            resources,
            renderHtml: ReactDOMServer.renderToString(jsx)
        });
    }

}

module.exports = TestController;

nunjucks templete

<!DOCTYPE html>
<html lang="{{locale}}">
<head>
    <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>Document</title>
    {{ resources.links | safe }}
    {% block preload %}{% endblock %}
    {{ resources.styles | safe }}
    {% block styles %}{% endblock %}
</head>
<body>
    {% block title %}{% endblock %}
    <div id="root">{{ renderHtml | safe }}</div>
    <script type="text/javascript">
        window['__ROUTE_DATA__'] = {{ context | dump | safe }};
    </script>
    {{ resources.scripts | safe }}
    {% block script %}{% endblock %}
</body>
</html>

config/routes

import loadable from '@loadable/component';

export const routes = [
    {
        path: '/m/flights/test1',
        exact: true,
        component: loadable(() => import(/* webpackChunkName: "test1" */ '../pages/Test1'))
    },
    {
        path: '/m/flights/test2',
        component: loadable(() => import(/* webpackChunkName: "test2" */ '../pages/Test2'))
    }
];

Test1.jsx and Test2.jsx are very simple jsx file, just like a function component

server-side root pages/index.jsx

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { routes } from '../config/routes';

export default function App() {
    return (
        <Switch>
            {
                routes.map((route, index) => (
                    <Route key={`${index}_${route.path}`}  {...route} />
                ))
            }
        </Switch>
    );
}

client-side root index.jsx

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { loadableReady } from '@loadable/component';
import App from './pages';

loadableReady(() => {
    ReactDOM.hydrate(
        <BrowserRouter>
            <App />
        </BrowserRouter>,
        document.getElementById('root')
    );
});

here are some webpack config

webpack.config.client

const path = require('path');
const LoadablePlugin = require('@loadable/webpack-plugin');
const alias = require('./alias');
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

console.log('NODE_ENV: ', process.env.NODE_ENV);

const entryFileConfig = {
    'index': 'index'
};

const outputFileConfig = {
    path: path.resolve(__dirname, '../dist/client'),
    filename: '[name].js',
    publicPath: IS_DEVELOPMENT ? '/m/flights/static/' : '',
    // chunkFilename: '[name].chunk.js'
};

const config = {
    context: path.resolve(__dirname, '../src'),
    entry: entryFileConfig,
    output: outputFileConfig,
    resolve: {
        alias,
        extensions: ['.js', '.jsx', '.css', '.scss'],
        modules: [
            path.resolve(__dirname, '../src'),
            'node_modules'
        ]
    },
    module: {
        rules: [
            {
                enforce: 'pre',
                test: /\.(js|jsx)$/,
                exclude: [
                    /node_modules/
                ],
                include: [
                    path.resolve(__dirname, '../src')
                ],
                use: {
                    loader: 'eslint-loader'
                }
            },
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                include: [
                    path.resolve(__dirname, '../src')
                ],
                use: [
                    {
                        loader: 'babel-loader'
                    }
                ]
            },
            {
                test: /\.(png|jpg|svg|gif|eot|otf|ttf|woff|woff2)$/,
                include: [
                    path.resolve(__dirname, '../src')
                ],
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192
                        }
                    }
                ]
            },
            {
                test: /\.(scss|css)$/,
                include: [
                    path.resolve(__dirname, '../src')
                ],
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader
                    },
                    {
                        loader: 'css-loader'
                    },
                    {
                        loader: 'postcss-loader'
                    },
                    {
                        loader: 'sass-loader'
                    }
                ]
            }
        ]
    },
    plugins: [
        new LoadablePlugin({
            writeToDisk: IS_DEVELOPMENT ? true : false
        }),
        new MiniCssExtractPlugin({
            filename: 'css/[name].min.css'
        })
    ],
    optimization: {
        splitChunks: {
            cacheGroups: {
                react: {
                    name: "chunks/react.chunk",
                    test: (module) => {
                        return /react|react-dom|react-router-dom|history/.test(module.context);
                    },
                    chunks: "all",
                    priority: 10 
                },
                common: {
                    minChunks: 2, 
                    name: 'chunks/common',
                    chunks: 'all',
                    priority: 5,
                    // test: () => {
                    //     return !/react|react-dom|history/.test(module.context);
                    // }
                }
            }
        }
    }
};

module.exports = config;

webpack.config.server

const path = require('path');
const nodeExternals = require("webpack-node-externals");
const LoadablePlugin = require('@loadable/webpack-plugin');
const alias = require('./alias');
const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';

// 入口文件
const entryFileConfig = {
    'index': 'pages/index'
};

// *输出文件
const outputFileConfig = {
    path: path.resolve(__dirname, '../dist/server'),
    filename: '[name].js',
    libraryTarget: "commonjs2",
    publicPath: IS_DEVELOPMENT ? '/m/flights/static/' : ''
};

const config = {
    mode: process.env.NODE_ENV,
    context: path.resolve(__dirname, '../src'),
    entry: entryFileConfig,
    output: outputFileConfig,
    target: 'node',
    externals: [
        '@loadable/component',
        nodeExternals()
    ],
    resolve: {
        alias,
        extensions: ['.js', '.jsx', '.css', '.scss'],
        modules: [
            // 告诉 webpack 解析模块时应该搜索的目录
            path.resolve(__dirname, '../src'),
            'node_modules'
        ]
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            caller: {
                                target: 'node'
                            }
                        }
                    }
                ]
            },
            {
                test: /\.(scss|css)$/,
                use: [
                    {
                        loader: 'ignore-loader'
                    }
                ]
            }
        ]
    },
    plugins: [
        new LoadablePlugin()
    ]
};

module.exports = config;

To Reproduce

Steps to reproduce the behavior:

and i released code to prod env, here are some strange phenomenon

image

the page has already appended script tag (common.js and test1.js) in html body, but why the html head has appended script tag (common.js and test1.js) too ? i think it should not append some tags again.

image

you can see the page from server side , there are no script tags in head tags, the repeat tags are appended in client side.

What's more, when I slow down the web page, it's normal again

image

and i refresh

image

it was correct show !

I have another problem: when I jump the route, the resource file corresponding to the next page will be loaded. I have configured the publicPath. Normally, the resource of the path will be loaded, but not, but only the resource of the relative path

image

i hope the test2.js should add the publicPath

Expected behavior

I think the correct display should be the second one above, just when i slow down the web page and refresh

image

i hope when i jump to the test2 page, the test2.js should add the publicPath in src.

That's my question. I hope I can get your help. Thank you very much

open-collective-bot[bot] commented 4 years ago

Hey @Yanchenyu :wave:, Thank you for opening an issue. We'll get back to you as soon as we can. Please, consider supporting us on Open Collective. We give a special attention to issues opened by backers. If you use Loadable at work, you can also ask your company to sponsor us :heart:.

Yanchenyu commented 4 years ago

Later, I noticed that several files that will be added repeatedly have a common feature, that is, they all have an attribute in their tag: data-chunk = "test1", and "test1" is exactly the current path

image

Yanchenyu commented 4 years ago

On the second question, I have made new progress.

I checked the source code of the package

this is index.js bundle

image

this is append css link tag part code:

image

this is append script tag part code: image

the a.p is the url prefix, and it is no value , just ""

image

i delete the ChunkExtractor publicPath, image

and only use webpack output publicPath, not '', give it some value

image

and it works, I checked the source code of the package again

image

So my guess is: can't the publicpath property of chunkextractor replace the prefixes of all chunk resource packages?

Yanchenyu commented 4 years ago

Three days later, I found an associated problem. Let's take a look at it together

https://github.com/gregberge/loadable-components/issues/557

I really like the library you made, please help me ! thank you so so much!!

theKashey commented 4 years ago

🐌- one of the most detailed issues, and left without much attention :( How it's ended?