fix: 换成基于 protractor 的 prerender 方式

This commit is contained in:
Zhicheng WANG 2019-01-05 17:30:28 +08:00
parent 65c7a3ed99
commit bd6bc39c05
11 changed files with 136 additions and 91 deletions

View File

@ -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"

View File

@ -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

View File

@ -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",

View File

@ -132,7 +132,7 @@ export const svgIconProviders = [
@NgModule({
imports: [
BrowserModule,
BrowserModule.withServerTransition({appId: 'ng-docs'}),
BrowserAnimationsModule,
CustomElementsModule,
HttpClientModule,

View File

@ -8,5 +8,7 @@ if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
});

View 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();
});
});
});

View File

@ -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() {

View File

@ -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

View File

@ -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();

View 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"
]
}