Skip to content

Commit

Permalink
fix(ssr): incomplete loading of css chunks in ssr and dynamicImport e…
Browse files Browse the repository at this point in the history
…nable for avoiding page flashing (#6637)

* fix(ssr): chunks lost in dynamicImport

* chore: types

* chore: clean code style

* chore: webpack

* chore: tsc

* chore: ts-ignore

* fix: test case
  • Loading branch information
ycjcl868 committed May 26, 2021
1 parent da26563 commit 471fb17
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 53 deletions.
3 changes: 3 additions & 0 deletions .circleci/config.yml
Expand Up @@ -22,6 +22,9 @@ jobs:
- run:
name: Run Build
command: yarn build
- run:
name: Run Type Check
command: yarn run tsc --noEmit
- run:
name: Run Tests
command: yarn test:coverage --forceExit --detectOpenHandles --runInBand
Expand Down
Expand Up @@ -4,3 +4,4 @@ export const OUTPUT_SERVER_TYPE_FILENAME = 'umi.server.d.ts';
export const TMP_PLUGIN_DIR = 'core/ssr';
export const CLIENT_EXPORTS = 'clientExports';
export const WRAPPERS_CHUNK_NAME = 'wrappers';
export const CHUNK_MANIFEST = 'asset-manifest.json';
15 changes: 13 additions & 2 deletions packages/preset-built-in/src/plugins/features/ssr/ssr.test.ts
Expand Up @@ -2,10 +2,17 @@ import { join } from 'path';
import { Service } from '@umijs/core';
import { onBuildComplete } from './ssr';
import { IApi } from '@umijs/types';
import { promises } from 'fs';

const fixtures = join(__dirname, 'fixtures');

test('onBuildComplete normal', async () => {
const writeFileSpy = jest
.spyOn(promises, 'writeFile')
// @ts-ignore
.mockImplementation(async (_, content) => {
return content;
});
const cwd = join(fixtures, 'normal');

const service = new Service({
Expand Down Expand Up @@ -33,11 +40,15 @@ test('onBuildComplete normal', async () => {
],
};

const buildComplete = onBuildComplete(api as IApi, true);
const serverContent = await buildComplete({
const buildComplete = onBuildComplete(api as IApi);
await buildComplete({
err: null,
stats,
});
const serverContent = await writeFileSpy.mock.results[0].value;
expect(writeFileSpy).toHaveBeenCalled();
expect(serverContent).toContain('/umi.6f4c357e.css');
expect(serverContent).toContain('/umi.e1837763.js');

writeFileSpy.mockClear();
});
119 changes: 83 additions & 36 deletions packages/preset-built-in/src/plugins/features/ssr/ssr.ts
Expand Up @@ -15,6 +15,8 @@ import {
} from '@umijs/utils';
import { matchRoutes, RouteConfig } from 'react-router-config';
import { webpack } from '@umijs/bundler-webpack';
// @ts-ignore
import { getCompilerHooks } from '@umijs/deps/compiled/webpack-manifest-plugin';
import ServerTypePlugin from './serverTypePlugin';
import { getHtmlGenerator } from '../../commands/htmlUtils';
import {
Expand All @@ -23,44 +25,89 @@ import {
OUTPUT_SERVER_TYPE_FILENAME,
TMP_PLUGIN_DIR,
CLIENT_EXPORTS,
CHUNK_MANIFEST,
} from './constants';

class ManifestChunksMapPlugin {
constructor(public opts: { api: IApi }) {
this.opts = opts;
}

apply(compiler: webpack.Compiler) {
let chunkGroups: any;
const { beforeEmit } = getCompilerHooks(compiler);

compiler.hooks.emit.tapPromise(
'ManifestChunksMapPlugin',
async (compilation: any) => {
chunkGroups = compilation.chunkGroups;
},
);

beforeEmit.tap('ManifestChunksMapPlugin', (manifest: object) => {
if (chunkGroups) {
const fileFilter = (file: string) =>
!file.endsWith('.map') && !file.endsWith('.hot-update.js');
const addPath = (file: string) =>
`${this.opts.api.config.publicPath}${file}`;
try {
const _chunksMap = chunkGroups.reduce((acc: any[], c: any) => {
acc[c.name] = [
...(acc[c.name] || []),
...c.chunks.reduce(
(files: any[], cc: any) => [
...files,
...cc.files.filter(fileFilter).map(addPath),
],
[],
),
];
return acc;
}, {});
return {
// IMPORTANT: hard code for `_chunkMap` field
_chunksMap,
...manifest,
};
} catch (e) {
this.opts.api.logger.error('[SSR chunkMap ERROR]', e);
}
}
return manifest;
});
}
}

/**
* onBuildComplete for test case
* replace default html template using client webpack bundle complete
* @param api
*/
export const onBuildComplete =
(api: IApi, _isTest = false) =>
async ({ err, stats }: any) => {
if (!err && stats?.stats) {
const HTML_REG = /<html.*?<\/html>/m;
const [clientStats] = stats.stats;
const html = getHtmlGenerator({ api });
const [defaultHTML] =
JSON.stringify(
await html.getContent({
route: { path: api.config.publicPath },
chunks: clientStats.compilation.chunks,
}),
).match(HTML_REG) || [];
const serverPath = path.join(
api.paths.absOutputPath!,
OUTPUT_SERVER_FILENAME,
);
if (fs.existsSync(serverPath) && defaultHTML) {
const serverContent = fs
.readFileSync(serverPath, 'utf-8')
.replace(HTML_REG, defaultHTML);
// for test case
if (_isTest) {
return serverContent;
}
await fs.promises.writeFile(serverPath, serverContent);
}
export const onBuildComplete = (api: IApi) => async ({ err, stats }: any) => {
if (!err && stats?.stats) {
const HTML_REG = /<html.*?<\/html>/m;
const [clientStats] = stats.stats;
const html = getHtmlGenerator({ api });
const [defaultHTML] =
JSON.stringify(
await html.getContent({
route: { path: api.config.publicPath },
chunks: clientStats.compilation.chunks,
}),
).match(HTML_REG) || [];
const serverPath = path.join(
api.paths.absOutputPath!,
OUTPUT_SERVER_FILENAME,
);
if (fs.existsSync(serverPath) && defaultHTML) {
const serverContent = fs
.readFileSync(serverPath, 'utf-8')
.replace(HTML_REG, defaultHTML);
await fs.promises.writeFile(serverPath, serverContent);
}
return undefined;
};
}
return undefined;
};

export default (api: IApi) => {
api.describe({
Expand Down Expand Up @@ -173,7 +220,7 @@ export default (api: IApi) => {
Basename: api.config.base,
PublicPath: api.config.publicPath,
ManifestFileName: api.config.manifest
? api.config.manifest.fileName || 'asset-manifest.json'
? api.config.manifest.fileName || CHUNK_MANIFEST
: '',
DEFAULT_HTML_PLACEHOLDER: serialize(defaultHTML),
}),
Expand Down Expand Up @@ -228,18 +275,15 @@ export default (api: IApi) => {
// force enable writeToDisk
// @ts-ignore
config.devServer.writeToDisk = (filePath: string) => {
const manifestFile =
// @ts-ignore
api.config?.manifest?.fileName || 'asset-manifest.json';
const regexp = new RegExp(
`(${OUTPUT_SERVER_FILENAME}|${OUTPUT_SERVER_TYPE_FILENAME}|${manifestFile})$`,
`(${OUTPUT_SERVER_FILENAME}|${OUTPUT_SERVER_TYPE_FILENAME})$`,
);
return regexp.test(filePath);
};
// enable manifest
if (config.dynamicImport) {
config.manifest = {
writeToFileEmit: false,
writeToFileEmit: true,
...(config.manifest || {}),
};
}
Expand Down Expand Up @@ -343,6 +387,9 @@ export default (api: IApi) => {

config.externals([]);
} else {
config
.plugin('ManifestChunksMap')
.use(ManifestChunksMapPlugin, [{ api }]);
// define client bundler config
config.plugin('define').tap(([args]) => [
{
Expand Down
Expand Up @@ -155,6 +155,12 @@ test('handleHTML dynamicImport', async () => {
'vendors~p__index.css': '/public/vendors~p__index.chunk.css',
'vendors~p__index.js': '/public/vendors~p__index.js',
'index.html': '/public/index.html',
_chunksMap: {
'umi': ['/public/umi.css', '/public/umi.js'],
'vendors~p__index': ['/public/vendors~p__index.chunk.css', '/public/vendors~p__index.js'],
'p__index': ['/public/p__index.chunk.css', '/public/p__index.js'],
'p__users': ['/public/p__users.chunk.css', '/public/p__users.js'],
}
},
mountElementId: 'root',
};
Expand Down Expand Up @@ -239,6 +245,13 @@ test('handleHTML complex', async () => {
'wrappers.css': '/wrappers.chunk.css',
'wrappers.js': '/wrappers.js',
'index.html': '/index.html',
_chunksMap: {
'umi': ['/umi.css', '/umi.js'],
'layouts__index': ['/layouts__index.chunk.css', '/layouts__index.js'],
'p__index': ['/p__index.chunk.css', '/p__index.js'],
'p__me': ['/p__me.chunk.css', '/p__me.js'],
'wrappers': ['/wrappers.chunk.css', '/wrappers.js'],
}
},
mountElementId: 'root',
};
Expand Down
Expand Up @@ -125,25 +125,17 @@ export const handleHTML = async (opts: Partial<IHandleHTMLOpts> = {}): Promise<s
const chunks: string[] = getPageChunks(
routesMatched.map((routeMatched) => routeMatched?.route),
);
// @ts-ignore
const assets = manifest?._chunksMap;
if (chunks?.length > 0) {
// only load css chunks to avoid page flashing
const cssChunkSet: string[] = [];
chunks.forEach((chunk) => {
Object.keys(manifest || {}).forEach((manifestChunk) => {
if (
manifestChunk !== 'umi.css' &&
chunk &&
// issue: https://github.com/umijs/umi/issues/6259
(manifestChunk.startsWith(chunk) ||
manifestChunk.indexOf(`~${chunk}`) > -1) &&
manifest &&
/\.css$/.test(manifest[manifestChunk])
) {
cssChunkSet.push(
`<link rel="preload" href="${manifest[manifestChunk]}"/><link rel="stylesheet" href="${manifest[manifestChunk]}" />`,
);
}
});
if(!assets || !Array.isArray(assets[chunk])) return;

assets[chunk].forEach((resource: string) => {
if (/\.css$/.test(resource)) cssChunkSet.push(`<link rel="preload" href="${resource}" as="style" /><link rel="stylesheet" href="${resource}" />`);
})
});
// avoid repeat
html = html.replace('</head>', `${cssChunkSet.join(EOL)}${EOL}</head>`);
Expand Down

0 comments on commit 471fb17

Please sign in to comment.