故仔背景
Angular7+ 开发的Web项目上线后,我们用google search console做了下检测,关键词零个,哈哈^_^。PO要求我们项目要在google上能尽量靠前,好了,工作有了,我们需要做SEO啦。
SPA项目在SEO这一块一直都是短板,所幸Angular,React,Vue等主流框架都提供了SSR 方案。 因为项目原因,后端不由我们Team控制,所以我们放弃了SSR,考虑做Prerender
Angular的SEO问题
关于Angular的SEO问题,可以看看这篇文章。
angular项目在显示首页时,需要先加载js,然后解析js,再渲染页面。在index文件里面可以看到body只有一个root component,没有其它内容。 但网络爬虫是检测网站html一load出来时的内容的,它没有时间和资源去等待成千上万个网站加载解析完js。
使用curl命令来看下browser一开始拿到的页面内容:
- 新建项目
ng new toolkit
cd toolkit
ng serve
- 运行curl命令
curl localhost:4200
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Toolkit</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
<script type="text/javascript" src="runtime.js"></script><script type="text/javascript" src="es2015-polyfills.js" nomodule></script><script type="text/javascript" src="polyfills.js"></script><script type="text/javascript" src="styles.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script></body>
</html>
可以看到页面拿出来是没内容的,body里面只有<app-root></app-root>,显然这很不利于搜索引擎和爬虫。
关于windows下如何使用Curl命令:参考文章
Prerender:@ng-toolkit/universal
关于如何做SSR, 官方提供了详细的教程。
除此之外,我们可以借助第三方的插件@ng-toolkit/universal ,一个命令就可以增加 SSR & Prerender 支持。
接下来我们使用@ng-toolkit/universal 来实现Prerender.
ng add @ng-toolkit/universal
+ @ng-toolkit/universal@7.1.2
added 10 packages from 4 contributors and audited 52074 packages in 25.57s
found 0 vulnerabilities
Installed packages for tooling via npm.
CREATE local.js (215 bytes)
CREATE prerender.ts (9079 bytes)
CREATE static.js (165 bytes)
CREATE static.paths.ts (70 bytes)
CREATE src/main.server.ts (220 bytes)
CREATE src/app/app.server.module.ts (485 bytes)
CREATE src/tsconfig.server.json (219 bytes)
CREATE webpack.server.config.js (1419 bytes)
CREATE server.ts (1346 bytes)
CREATE src/app/app.browser.module.ts (473 bytes)
CREATE ng-toolkit.json (493 bytes)
UPDATE package.json (2143 bytes)
UPDATE angular.json (4467 bytes)
UPDATE src/main.ts (500 bytes)
UPDATE src/app/app.module.ts (759 bytes)
可以看到这个命令帮我们安装了必须的依赖,创建了一些配置文件和更新了几个文件。
在package.json中可以看到新加了几个跑SSR和Prerender的命令
"compile:server": "webpack --config webpack.server.config.js --progress --colors",
"serve:ssr": "node local.js",
"build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
"build:client-and-server-bundles": "ng build --prod && ng run toolkit:server:production",
"server": "node local.js",
"build:prod": "npm run build:ssr",
"serve:prerender": "node static.js",
"build:prerender": "npm run build:prod && node dist/prerender.js"
- 运行Prerender命令
npm run build:prerender
该命令执行成功后,我们可以看到在dist文件夹下面多了browser,server,static文件夹。对于prerender,打包好的文件都在包含在static文件夹里面。打开index.html文件,可以看到首页的页面内容已经组装好了。
- 开启本地服务
npm run serve:prerender
Note:static.js设置默认服务端口为8080,如果端口被占用,会抛出"listen EACCES:permission denied 0.0.0.0:8080"的错误,可自行修改端口
使用curl命令看下现在的页面内容:
curl localhost:4200
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8">
<title>Toolkit</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.3ff695c00d717f2d2a11.css"><style ng-transition="serverApp">
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzcmMvYXBwL2FwcC5jb21wb25lbnQuc2NzcyJ9 */</style></head>
<body>
<app-root _nghost-sc0="" ng-version="7.2.15"><div _ngcontent-sc0="" style="text-align:center"><h1 _ngcontent-sc0=""> Welcome to toolkit! </h1><img _ngcontent-sc0="" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==" width="300"></div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2><ul _ngcontent-sc0=""><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener" target="_blank">Tour of Heroes</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/cli" rel="noopener" target="_blank">CLI Documentation</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2></li></ul><router-outlet _ngcontent-sc0=""></router-outlet></app-root>
<script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="es2015-polyfills.e17997aea7f522b8d49d.js" nomodule=""></script><script type="text/javascript" src="polyfills.8bbb231b43165d65d357.js"></script><script type="text/javascript" src="main.a17b3343b8cb832b6b94.js"></script>
<script id="serverApp-state" type="application/json">{}</script></body></html>
现在看起来内容不是个空壳了,很棒O(∩_∩)O
实际项目做Prerender遇到的问题
到此我们新建项目做Prerender和SSR都很顺利,但在真实的项目中会遇到各种各样的问题。下面举一些我遇到的问题以及对应的解决方法:
-
Build SSR ERRORs
- error: Unexpected end of file
最后检查发现是anular.json文件的格式有错误,修正就好了。 -
error: Can not recognize the custom path in tsconfig.json
在开发时为了方便,通常会自定义一些公用组件或方法的路径,例如在项目的tsconfig.app.json定义path:@test/common{ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "types": [], "paths": { "@angular/*": [ "node_modules/@angular/*" ], "@test/common": [ "src/app/common/index" ] } } }
在Component中引用
import { TestService } from '@test/common';
原因:tsconfig.server.json的baseUrl属性修改了设置的相对路径。
解决方法:删除tsconfig.server.json的baseUrl属性即可。
- error: Unexpected end of file
-
Build Prerender error: EPERM:operation not permitted, copy file './dist/browser/assets' ->'./dist/static/assets'
原因:从prerender.ts的160行的代码可以看到在复制资源文件的时候,默认assets下是文件,没有folder的。但在我的项目中assets目录下还有分文件夹,所以报错了,源码为:filesBrowser.forEach(file => { if (file !== 'index.html') { fs.copyFileSync(`./dist/browser/${file}`, `./dist/static/${file}`); } });
解决方法: 自己重写一个copy文件的方法覆盖默认的:
/** ** Copy static files ** Using custom copyFile function instead **/ function copyFile(dir) { let subFilesBrowser = fs.readdirSync(dir); subFilesBrowser.forEach(file => { if (file !== 'index.html') { let fullName = path.join(dir, file); let stats = fs.statSync(fullName); if (stats.isDirectory()) { //recurse fs.mkdirSync(fullName.replace('\\browser\\', '\\static\\')); copyFile(fullName) } else { let filePath = fullName.split('\\dist\\browser\\')[1]; fs.copyFileSync(`./dist/browser/${filePath}`, `./dist/static/${filePath}`); } } }) } function removeStaticFile(filePath){ if (fs.existsSync(filePath)) { fs.readdirSync(filePath).forEach(function(file, index){ let curPath = path.join(filePath, file); if (fs.lstatSync(curPath).isDirectory()) { //recurse removeStaticFile(curPath); } else { // delete file fs.unlinkSync(curPath); } }); fs.rmdirSync(filePath); } } function beforeCopyFile() { let staticDir=`${process.cwd()}/dist/static`; removeStaticFile(staticDir); fs.mkdirSync(staticDir); copyFile(`${process.cwd()}/dist/browser`); } beforeCopyFile();
-
Build Prerender error: window is not defined.
@ng-toolkit/universal 提供了window和localstorage的支持,按照文档解决即可:import { WINDOW } from '@ng-toolkit/universal'; constructor(@Inject(WINDOW) private wdw: Window) { } //import NgtUniversalModule in your app module import { NgtUniversalModule } from '@ng-toolkit/universal'; @NgModule({ declarations: [...] imports: [ NgtUniversalModule ] })
-
Prerender error: document.cookie NotYetImplemented.
screen is not defined.
诸如此类需要在真实浏览器中才能获取的数据或运行的逻辑,我使用了平台检测方法,具体实现如下:
//in common service import { Injectable, PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { Observable, Subject, BehaviorSubject } from 'rxjs'; isBrowser: boolean; /** **BehaviorSubject can stores the latest value. **Once a new Observer subscribes, it will emitted current value to its consumers **/ browserSub: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); checkPlatform() { this.isBrowser = isPlatformBrowser(this.platformId); this.isBrowser && this.browserSub.next(this.isBrowser); }
-
Remove sourcemap in prerender
SSR & Prerender要去掉sourcemap,在angular.json的"server"属性的config设置下sourcemap即可,如:"server": { "builder": "@angular-devkit/build-angular:server", "options": { "outputPath": "dist/server", "main": "src/main.server.ts", "tsConfig": "src/tsconfig.server.json" }, "configurations": { "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "sourceMap": false } }
- Lazyload router module flickering (first page loads twice)
首页会闪动的问题猜测是因为浏览器第一次快速显示的是index的内容,等router对应的js load回来时,angular会再次进行解析,跳到对应的路由,所以看起来页面会闪动一下。这个问题follow stackflow和github issue都没得到解决,最后想想都是第一次路由跳转惹的祸,就把首页的lazyload router module去掉了,问题最终完美解决~
一起学习一起进步!
有问题欢迎随时指正~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。