fix: 换成基于 protractor 的 prerender 方式
This commit is contained in:
parent
65c7a3ed99
commit
bd6bc39c05
@ -9,6 +9,7 @@ addons:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
chrome: stable
|
||||
branches:
|
||||
except:
|
||||
- g3
|
||||
@ -21,5 +22,6 @@ cache:
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.10.1
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost &
|
||||
script:
|
||||
- "./aio/deploy-cn.sh"
|
||||
|
@ -9,7 +9,14 @@ commitMessage=$(git log --oneline -n 1)
|
||||
cd `dirname $0`
|
||||
|
||||
yarn build
|
||||
ts-node ./tools/translator/bin/ssr.ts
|
||||
|
||||
yarn start &
|
||||
|
||||
sleep 10
|
||||
|
||||
yarn update-webdriver
|
||||
|
||||
yarn prerender
|
||||
|
||||
if [[ ! -d "./ng-docs.github.io" ]]
|
||||
then
|
||||
|
@ -68,7 +68,8 @@
|
||||
"post~~build": "yarn build-404-page",
|
||||
"~~build-ie-polyfills": "webpack-cli src/ie-polyfills.js -o src/generated/ie-polyfills.min.js --mode production",
|
||||
"~~http-server": "http-server",
|
||||
"~~minify-lunr": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/generated/lunr.min.js --source-map"
|
||||
"~~minify-lunr": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/generated/lunr.min.js --source-map",
|
||||
"prerender": "protractor tests/e2e/protractor.conf.js --disableChecks --specs tests/e2e/prerender.e2e-spec.ts \"--grep=^Prerender \""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.9.0 <11.0.0",
|
||||
|
@ -132,7 +132,7 @@ export const svgIconProviders = [
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserModule.withServerTransition({appId: 'ng-docs'}),
|
||||
BrowserAnimationsModule,
|
||||
CustomElementsModule,
|
||||
HttpClientModule,
|
||||
|
@ -8,5 +8,7 @@ if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
});
|
||||
|
88
aio/tests/e2e/prerender.e2e-spec.ts
Normal file
88
aio/tests/e2e/prerender.e2e-spec.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { browser, promise } from 'protractor';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { concat, defer, Observable } from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import * as path from 'path';
|
||||
import * as mkdirp from 'mkdirp';
|
||||
import { minify } from 'html-minifier';
|
||||
import * as globby from 'globby';
|
||||
|
||||
const minifyOptions = {
|
||||
collapseWhitespace: true,
|
||||
ignoreCustomFragments: [/<code>[\s\S]*?<\/code>/],
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
removeComments: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
};
|
||||
|
||||
function fromPromise<T>(input: promise.Promise<T>): Observable<T> {
|
||||
return Observable.create(observer => {
|
||||
input.then((value) => {
|
||||
observer.next(value);
|
||||
observer.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function prerender(url: string): Observable<any> {
|
||||
return defer(() => fromPromise(browser.get(url))).pipe(
|
||||
tap(() => browser.executeScript('document.body.classList.add(\'no-animations\')')),
|
||||
tap(() => browser.waitForAngular()),
|
||||
switchMap(() => fromPromise(browser.executeScript('return document.documentElement.outerHTML'))),
|
||||
map(content => minify(content, minifyOptions)),
|
||||
map((content) => `<!DOCTYPE html>${content}`),
|
||||
tap((html) => {
|
||||
const filename = path.join('dist', `${url}.html`);
|
||||
mkdirp.sync(path.dirname(filename));
|
||||
writeFileSync(filename, html, 'utf-8');
|
||||
}),
|
||||
tap(() => console.log('Rendered ', url)),
|
||||
);
|
||||
}
|
||||
|
||||
function getGuideUrls(): string[] {
|
||||
const navigation = readFileSync('./content/navigation.json', 'utf-8');
|
||||
return (navigation.match(/"url": "(.*?)"/g) || [])
|
||||
.map((entry) => entry.replace(/^"url": "(.*?)".*$/, '$1'))
|
||||
.filter(url => url.slice(0, 4) !== 'http');
|
||||
}
|
||||
|
||||
function getApiUrls(): string[] {
|
||||
return globby.sync('./src/generated/docs/api/**/*.json')
|
||||
.map(file => file.replace(/^.*generated\/docs\/(.*).json$/, '$1'));
|
||||
}
|
||||
|
||||
|
||||
function renderUrlsByTemplate(urls: string[], templateUrl: string): void {
|
||||
const template = readFileSync(path.join('dist', `${templateUrl}.html`), 'utf-8');
|
||||
urls.filter(url => url !== templateUrl).forEach((url) => {
|
||||
console.log('Render by template: ', url);
|
||||
const article = JSON.parse(readFileSync(`./src/generated/docs/${url}.json`, 'utf-8'));
|
||||
const content = template
|
||||
.replace(/<article>.*<\/article>/, article.contents)
|
||||
.replace(/<title>.*<\/title>/, `${article.title} - Angular 官方文档`);
|
||||
const filename = path.join('dist', `${url}.html`);
|
||||
mkdirp.sync(path.dirname(filename));
|
||||
writeFileSync(filename, content, 'utf-8');
|
||||
});
|
||||
}
|
||||
|
||||
describe('Prerender', function () {
|
||||
it('prerender', (done) => {
|
||||
const apiUrls = getApiUrls();
|
||||
const guideUrls = getGuideUrls().filter(url => url.match(/^(guide|tutorial|cli)/));
|
||||
const otherUrls = getGuideUrls().filter(url => !url.match(/^(guide|tutorial|cli)/));
|
||||
const urls = [guideUrls[0], apiUrls[0], otherUrls[0], 'index.html'];
|
||||
const tasks = urls.map(url => prerender(url));
|
||||
concat(...tasks).subscribe(undefined, undefined, () => {
|
||||
console.log('Start render by template');
|
||||
renderUrlsByTemplate(apiUrls, apiUrls[0]);
|
||||
renderUrlsByTemplate(guideUrls, guideUrls[0]);
|
||||
renderUrlsByTemplate(otherUrls, otherUrls[0]);
|
||||
console.log('All rendered');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@ -3,8 +3,9 @@
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
const MaxInt32 = Math.pow(2, 31) - 1;
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
allScriptsTimeout: MaxInt32,
|
||||
specs: [
|
||||
'./*.e2e-spec.ts'
|
||||
],
|
||||
@ -12,7 +13,6 @@ exports.config = {
|
||||
browserName: 'chrome',
|
||||
// For Travis
|
||||
chromeOptions: {
|
||||
binary: process.env.CHROME_BIN,
|
||||
args: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
@ -21,7 +21,7 @@ exports.config = {
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
defaultTimeoutInterval: MaxInt32,
|
||||
print: function() {}
|
||||
},
|
||||
beforeLaunch: function() {
|
||||
|
@ -1,12 +0,0 @@
|
||||
<aio-shell ng-version="6.0.0"
|
||||
class="mode-stable sidenav-open page-guide-template-syntax folder-guide view-SideNav aio-notification-hide ">
|
||||
<ul role="navigation"><!---->
|
||||
<li class="ng-star-inserted"><a class="nav-link" href="/features" title="特性">特性</a></li>
|
||||
<li class="ng-star-inserted"><a class="nav-link" href="/docs" title="文档">文档</a></li>
|
||||
<li class="ng-star-inserted"><a class="nav-link" href="/resources" title="资源">资源</a></li>
|
||||
<li class="ng-star-inserted"><a class="nav-link" href="/events" title="会议">会议</a></li>
|
||||
<li class="ng-star-inserted"><a class="nav-link" href="https://blog.angular.io/" title="博客">博客</a></li>
|
||||
<li class="ng-star-inserted"><a class="nav-link" href="/translations/cn/home" title="关于中文版">关于中文版</a></li>
|
||||
<li class="ng-star-inserted"><a class="nav-link" href="https://ng-china.org" title="****2018 ngChina @ 杭州!****">****2018 ngChina @ 杭州!****</a></li>
|
||||
</ul>
|
||||
</aio-shell>
|
File diff suppressed because one or more lines are too long
@ -1,68 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as mkdirp from 'mkdirp';
|
||||
import * as path from 'path';
|
||||
import * as klawSync from 'klaw-sync';
|
||||
import { minify } from 'html-minifier';
|
||||
|
||||
const rootElementPattern = /<aio-shell\b[\s\S]*<\/aio-shell>/;
|
||||
|
||||
const indexTemplate = fs.readFileSync('./dist/index.html', 'utf-8');
|
||||
const aioShellTemplate = fs.readFileSync(__dirname + '/../assets/aio-shell-template.html');
|
||||
const pageTemplate = indexTemplate.replace(rootElementPattern, `${aioShellTemplate}`);
|
||||
|
||||
const minifyOptions = {
|
||||
collapseWhitespace: true,
|
||||
ignoreCustomFragments: [/<code>[\s\S]*?<\/code>/],
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
removeComments: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
};
|
||||
|
||||
function composePage(url) {
|
||||
console.log(`pre-rendering ${url}...`);
|
||||
const { title, contents } = JSON.parse(fs.readFileSync(`./dist/generated/docs/${url}.json`, 'utf-8'));
|
||||
|
||||
if (!contents) {
|
||||
return;
|
||||
}
|
||||
const ssrContent = contents.replace(/href="(?!http)(.{2,}?)"/gi, 'href="/$1"');
|
||||
const pageContent = pageTemplate
|
||||
.replace('<title>Angular Docs</title>', `<title>${title} - Angular 官方文档</title>`)
|
||||
.replace('<section class="sidenav-content" role="content" id="docs"></section>',
|
||||
`<section class="sidenav-content" role="content" id="docs">${ssrContent}</section>`);
|
||||
mkdirp.sync(path.dirname(`./dist/${url}`));
|
||||
fs.writeFileSync(`./dist/${url}.html`, minify(pageContent, minifyOptions), 'utf-8');
|
||||
}
|
||||
|
||||
function buildGuidePages(): void {
|
||||
const navigation = fs.readFileSync('./content/navigation.json', 'utf-8');
|
||||
(navigation.match(/"url": "(.*?)"/g) || [])
|
||||
.map((entry) => entry.replace(/^"url": "(.*?)".*$/, '$1'))
|
||||
.filter(url => url.slice(0, 4) !== 'http')
|
||||
.forEach(url => composePage(url));
|
||||
}
|
||||
|
||||
function buildApiPages(): void {
|
||||
const apiList = klawSync('./dist/generated/docs/api', { nodir: true })
|
||||
.map(file => file.path.replace(/^.*generated\/docs(\/.*).json$/, '$1'));
|
||||
apiList.forEach(url => composePage(url));
|
||||
|
||||
const links = apiList.map(url => `<a href="${url}">${url}</a>`).join('\n');
|
||||
const apiListContent = fs.readFileSync('./dist/api.html', 'utf-8')
|
||||
.replace(rootElementPattern, `<aio-shell><h3>API List</h3>${links}</aio-shell>`);
|
||||
|
||||
fs.writeFileSync(`./dist/api.html`, minify(apiListContent, minifyOptions), 'utf-8');
|
||||
}
|
||||
|
||||
function buildIndexPage(): void {
|
||||
const indexTemplate = fs.readFileSync('./dist/index.html', 'utf-8');
|
||||
const aioShellIndex = fs.readFileSync(__dirname + '/../assets/aio-shell-index.html');
|
||||
const content = indexTemplate.replace(rootElementPattern, `${aioShellIndex}`);
|
||||
fs.writeFileSync(`./dist/index.html`, minify(content, minifyOptions), 'utf-8');
|
||||
}
|
||||
|
||||
buildGuidePages();
|
||||
buildApiPages();
|
||||
buildIndexPage();
|
28
aio/tools/translator/tsconfig.json
Normal file
28
aio/tools/translator/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"outDir": "dist",
|
||||
"baseUrl": "src",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user