Angular7+ Prerender && SSR

故仔背景

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!

Angular Logo

Here are some links to help you start:

现在看起来内容不是个空壳了,很棒O(∩_∩)O

实际项目做Prerender遇到的问题

到此我们新建项目做Prerender和SSR都很顺利,但在真实的项目中会遇到各种各样的问题。下面举一些我遇到的问题以及对应的解决方法:

  1. 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属性即可。

  2. 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();
  3. 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
       ]
    })
  4. 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);
    }
    
  5. 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
         }
     }
  6. Lazyload router module flickering (first page loads twice)
    首页会闪动的问题猜测是因为浏览器第一次快速显示的是index的内容,等router对应的js load回来时,angular会再次进行解析,跳到对应的路由,所以看起来页面会闪动一下。这个问题follow stackflow和github issue都没得到解决,最后想想都是第一次路由跳转惹的祸,就把首页的lazyload router module去掉了,问题最终完美解决~

一起学习一起进步!
有问题欢迎随时指正~

你可能感兴趣的:(angular,prerender,typescript)