故仔背景
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
Toolkit
可以看到页面拿出来是没内容的,body里面只有
关于windows下如何使用Curl命令:参考文章
Prerender:@ng-toolkit/universal
关于如何做SSR, 官方提供了详细的教程。
除此之外,我们可以借助第三方的插件@ng-toolkit/universal ,一个命令就可以增加 SSR & Prerender 支持。
接下来我们使用@ng-toolkit/universal 来实现Prerender.
ng add @ng-toolkit/universal
+ @ng-toolkit/[email protected]
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
Toolkit
Welcome to toolkit!
Here are some links to help you start:
现在看起来内容不是个空壳了,很棒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
= new BehaviorSubject (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去掉了,问题最终完美解决~
一起学习一起进步!
有问题欢迎随时指正~