Angular開發實踐(六):服務端渲染

来源:https://www.cnblogs.com/laixiangran/archive/2018/03/28/8664480.html
-Advertisement-
Play Games

Angular在服務端渲染方面提供一套前後端同構解決方案,它就是 Angular Universal(統一平臺),一項在服務端運行 Angular 應用的技術 ...


Angular Universal

Angular在服務端渲染方面提供一套前後端同構解決方案,它就是 Angular Universal(統一平臺),一項在服務端運行 Angular 應用的技術。

標準的 Angular 應用會執行在瀏覽器中,它會在 DOM 中渲染頁面,以響應用戶的操作。

而 Angular Universal 會在服務端通過一個被稱為服務端渲染(server-side rendering - SSR)的過程生成靜態的應用頁面。

它可以生成這些頁面,併在瀏覽器請求時直接用它們給出響應。 它也可以把頁面預先生成為 HTML 文件,然後把它們作為靜態文件供服務端使用。

工作原理

要製作一個 Universal 應用,就要安裝 platform-server 包。 platform-server 包提供了服務端的 DOM 實現、XMLHttpRequest 和其它底層特性,但不再依賴瀏覽器。

你要使用 platform-server 模塊而不是 platform-browser 模塊來編譯這個客戶端應用,並且在一個 Web 伺服器上運行這個 Universal 應用。

伺服器(下麵的示例中使用的是 Node Express 伺服器)會把客戶端對應用頁面的請求傳給 renderModuleFactory 函數。

renderModuleFactory 函數接受一個模板 HTML 頁面(通常是 index.html)、一個包含組件的 Angular 模塊和一個用於決定該顯示哪些組件的路由作為輸入。

該路由從客戶端的請求中傳給伺服器。 每次請求都會給出所請求路由的一個適當的視圖。

renderModuleFactory 在模板中的 <app> 標記中渲染出哪個視圖,併為客戶端創建一個完成的 HTML 頁面。

最後,伺服器就會把渲染好的頁面返回給客戶端。

為什麼要服務端渲染

三個主要原因:

  1. 幫助網路爬蟲(SEO)

  2. 提升在手機和低功耗設備上的性能

  3. 迅速顯示出第首頁

幫助網路爬蟲(SEO)

Google、Bing、百度、Facebook、Twitter 和其它搜索引擎或社交媒體網站都依賴網路爬蟲去索引你的應用內容,並且讓它的內容可以通過網路搜索到。

這些網路爬蟲可能不會像人類那樣導航到你的具有高度交互性的 Angular 應用,併為其建立索引。

Angular Universal 可以為你生成應用的靜態版本,它易搜索、可鏈接,瀏覽時也不必藉助 JavaScript。它也讓站點可以被預覽,因為每個 URL 返回的都是一個完全渲染好的頁面。

啟用網路爬蟲通常被稱為搜索引擎優化 (SEO)

提升手機和低功耗設備上的性能

有些設備不支持 JavaScript 或 JavaScript 執行得很差,導致用戶體驗不可接受。 對於這些情況,你可能會需要該應用的服務端渲染、無 JavaScript 的版本。 雖然有一些限制,不過這個版本可能是那些完全沒辦法使用該應用的人的唯一選擇。

快速顯示首頁

快速顯示首頁對於吸引用戶是至關重要的。

如果頁面載入超過了三秒中,那麼 53% 的移動網站會被放棄。 你的應用需要啟動的更快一點,以便在用戶決定做別的事情之前吸引他們的註意力。

使用 Angular Universal,你可以為應用生成“著陸頁”,它們看起來就和完整的應用一樣。 這些著陸頁是純 HTML,並且即使 JavaScript 被禁用了也能顯示。 這些頁面不會處理瀏覽器事件,不過它們可以用 routerLink 在這個網站中導航。

在實踐中,你可能要使用一個著陸頁的靜態版本來保持用戶的註意力。 同時,你也會在幕後載入完整的 Angular 應用。 用戶會認為著陸頁幾乎是立即出現的,而當完整的應用載入完之後,又可以獲得完全的交互體驗。

示例解析

下麵將基於我在GitHub上的示例項目 angular-universal-starter 來進行講解。

這個項目與第一篇的示例項目一樣,都是基於 Angular CLI進行開發構建的,因此它們的區別隻在於服務端渲染所需的那些配置上。

安裝工具

在開始之前,下列包是必須安裝的(示例項目均已配置好,只需 npm install 即可):

  • @angular/platform-server - Universal 的服務端元件。
  • @nguniversal/module-map-ngfactory-loader - 用於處理服務端渲染環境下的惰性載入。
  • @nguniversal/express-engine - Universal 應用的 Express 引擎
  • ts-loader - 用於對服務端應用進行轉譯。
  • express - Node Express 伺服器

使用下列命令安裝它們:

npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine express

項目配置

配置工作有:

  1. 創建服務端應用模塊:src/app/app.server.module.ts
  2. 修改客戶端應用模塊:src/app/app.module.ts
  3. 創建服務端應用的引導程式文件:src/main.server.ts
  4. 修改客戶端應用的引導程式文件:src/main.ts
  5. 創建 TypeScript 的服務端配置:src/tsconfig.server.json
  6. 修改 @angular/cli 的配置文件:.angular-cli.json
  7. 創建 Node Express 的服務程式:server.ts
  8. 創建服務端預渲染的程式:prerender.ts
  9. 創建 Webpack 的服務端配置:webpack.server.config.js

1、創建服務端應用模塊:src/app/app.server.module.ts

import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppBrowserModule } from './app.module';
import { AppComponent } from './app.component';

// 可以註冊那些在 Universal 環境下運行應用時特有的服務提供商
@NgModule({
    imports: [
        AppBrowserModule, // 客戶端應用的 AppModule
        ServerModule, // 服務端的 Angular 模塊
        ModuleMapLoaderModule, // 用於實現服務端的路由的惰性載入
        ServerTransferStateModule, // 在服務端導入,用於實現將狀態從伺服器傳輸到客戶端
    ],
    bootstrap: [AppComponent],
})
export class AppServerModule {
}

服務端應用模塊(習慣上叫作 AppServerModule)是一個 Angular 模塊,它包裝了應用的根模塊 AppModule,以便 Universal 可以在你的應用和伺服器之間進行協調。 AppServerModule 還會告訴 Angular 再把你的應用以 Universal 方式運行時,該如何引導它。

2、修改客戶端應用模塊:src/app/app.module.ts

import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { APP_ID, Inject, NgModule, PLATFORM_ID } from '@angular/core';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { TransferHttpCacheModule } from '@nguniversal/common';
import { isPlatformBrowser } from '@angular/common';
import { AppRoutingModule } from './app.routes';

@NgModule({
    imports: [
        AppRoutingModule,
        BrowserModule.withServerTransition({appId: 'my-app'}),
        TransferHttpCacheModule, // 用於實現伺服器到客戶端的請求傳輸緩存,防止客戶端重覆請求服務端已完成的請求
        BrowserTransferStateModule, // 在客戶端導入,用於實現將狀態從伺服器傳輸到客戶端
        HttpClientModule
    ],
    declarations: [
        AppComponent,
        HomeComponent
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppBrowserModule {
    constructor(@Inject(PLATFORM_ID) private platformId: Object,
                @Inject(APP_ID) private appId: string) {
        
        // 判斷運行環境為客戶端還是服務端
        const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
        console.log(`Running ${platform} with appId=${appId}`);
    }
}

NgModule 的元數據中 BrowserModule 的導入改成 BrowserModule.withServerTransition({appId: 'my-app'}),Angular 會把 appId 值(它可以是任何字元串)添加到服務端渲染頁面的樣式名中,以便它們在客戶端應用啟動時可以被找到並移除。

此時,我們可以通過依賴註入(@Inject(PLATFORM_ID)@Inject(APP_ID))取得關於當前平臺和 appId 的運行時信息:

constructor(@Inject(PLATFORM_ID) private platformId: Object,
            @Inject(APP_ID) private appId: string) {
    
    // 判斷運行環境為客戶端還是服務端
    const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
    console.log(`Running ${platform} with appId=${appId}`);
}

3、創建服務端應用的引導程式文件:src/main.server.ts

該文件導出服務端模塊:

export { AppServerModule } from './app/app.server.module';

4、修改客戶端應用的引導程式文件:src/main.ts

監聽 DOMContentLoaded 事件,在發生 DOMContentLoaded 事件時運行我們的代碼,以使 TransferState 正常工作

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppBrowserModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}

// 在 DOMContentLoaded 時運行我們的代碼,以使 TransferState 正常工作
document.addEventListener('DOMContentLoaded', () => {
    platformBrowserDynamic().bootstrapModule(AppBrowserModule);
});

5、創建 TypeScript 的服務端配置:src/tsconfig.server.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "commonjs",
    "types": [
      "node"
    ]
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"
  }
}

tsconfig.app.json 的差異在於:

  • module 屬性必須是 commonjs,這樣它才能被 require() 方法導入你的服務端應用。

  • angularCompilerOptions 部分有一些面向 AOT 編譯器的選項:

    • entryModule - 服務端應用的根模塊,其格式為 path/to/file#ClassName。

6、修改 @angular/cli 的配置文件:.angular-cli.json

apps 下添加:

{
    "platform": "server",
    "root": "src",
    "outDir": "dist/server",
    "assets": [
      "assets",
      "favicon.ico"
    ],
    "index": "index.html",
    "main": "main.server.ts",
    "test": "test.ts",
    "tsconfig": "tsconfig.server.json",
    "testTsconfig": "tsconfig.spec.json",
    "prefix": "",
    "styles": [
      "styles.scss"
    ],
    "scripts": [],
    "environmentSource": "environments/environment.ts",
    "environments": {
      "dev": "environments/environment.ts",
      "prod": "environments/environment.prod.ts"
    }
}

7、創建 Node Express 的服務程式:server.ts

import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// Our index.html we'll use as our template
const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
        provideModuleMap(LAZY_MODULE_MAP)
    ]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

/* - Example Express Rest API endpoints -
  app.get('/api/**', (req, res) => { });
*/

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser'), {
    maxAge: '1y'
}));

// ALl regular routes use the Universal engine
app.get('*', (req, res) => {
    res.render('index', {req});
});

// Start up the Node server
app.listen(PORT, () => {
    console.log(`Node Express server listening on http://localhost:${PORT}`);
});
Universal 模板引擎

這個文件中最重要的部分是 ngExpressEngine 函數:

app.engine('html', ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
        provideModuleMap(LAZY_MODULE_MAP)
    ]
}));

ngExpressEngine 是對 Universal 的 renderModuleFactory 函數的封裝。它會把客戶端請求轉換成服務端渲染的 HTML 頁面。如果你使用不同於Node的服務端技術,你需要在該服務端的模板引擎中調用這個函數。

  • 第一個參數是你以前寫過的 AppServerModule。 它是 Universal 服務端渲染器和你的應用之間的橋梁。

  • 第二個參數是 extraProviders。它是在這個伺服器上運行時才需要的一些可選的 Angular 依賴註入提供商。當你的應用需要那些只有當運行在伺服器實例中才需要的信息時,就要提供 extraProviders 參數。

ngExpressEngine 函數返回了一個會解析成渲染好的頁面的承諾(Promise)。

接下來你的引擎要決定拿這個頁面做點什麼。 現在這個引擎的回調函數中,把渲染好的頁面返回給了 Web 伺服器,然後伺服器通過 HTTP 響應把它轉發給了客戶端。

8、創建服務端預渲染的程式:prerender.ts

// Load zone.js for the server.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';

import { enableProdMode } from '@angular/core';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { renderModuleFactory } from '@angular/platform-server';
import { ROUTES } from './static.paths';

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');

const BROWSER_FOLDER = join(process.cwd(), 'browser');

// Load the index.html file containing referances to your application bundle.
const index = readFileSync(join('browser', 'index.html'), 'utf8');

let previousRender = Promise.resolve();

// Iterate each route path
ROUTES.forEach(route => {
    const fullPath = join(BROWSER_FOLDER, route);

    // Make sure the directory structure is there
    if (!existsSync(fullPath)) {
        mkdirSync(fullPath);
    }

    // Writes rendered HTML to index.html, replacing the file if it already exists.
    previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, {
        document: index,
        url: route,
        extraProviders: [
            provideModuleMap(LAZY_MODULE_MAP)
        ]
    })).then(html => writeFileSync(join(fullPath, 'index.html'), html));
});

9、創建 Webpack 的服務端配置:webpack.server.config.js

Universal 應用不需要任何額外的 Webpack 配置,Angular CLI 會幫我們處理它們。但是由於本例子的 Node Express 的服務程式是 TypeScript 應用(server.ts及prerender.ts),所以要使用 Webpack 來轉譯它。這裡不討論 Webpack 的配置,需要瞭解的移步 Webpack官網

// Work around for https://github.com/angular/angular-cli/issues/7200

const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        server: './server.ts', // This is our Express server for Dynamic universal
        prerender: './prerender.ts' // This is an example of Static prerendering (generative)
    },
    target: 'node',
    resolve: {extensions: ['.ts', '.js']},
    externals: [/(node_modules|main\..*\.js)/,], // Make sure we include all node_modules etc
    output: {
        path: path.join(__dirname, 'dist'), // Puts the output at the root of the dist folder
        filename: '[name].js'
    },
    module: {
        rules: [
            {test: /\.ts$/, loader: 'ts-loader'}
        ]
    },
    plugins: [
        new webpack.ContextReplacementPlugin(
            /(.+)?angular(\\|\/)core(.+)?/, // fixes WARNING Critical dependency: the request of a dependency is an expression
            path.join(__dirname, 'src'), // location of your src
            {} // a map of your routes
        ),
        new webpack.ContextReplacementPlugin(
            /(.+)?express(\\|\/)(.+)?/, // fixes WARNING Critical dependency: the request of a dependency is an expression
            path.join(__dirname, 'src'),
            {}
        )
    ]
};

測試配置

通過上面的配置,我們就製作完成一個可在服務端渲染的 Angular Universal 應用。

在 package.json 的 scripts 區配置 build 和 serve 有關的命令:

{
    "scripts": {
        "ng": "ng",
        "start": "ng serve -o",
        "ssr": "npm run build:ssr && npm run serve:ssr",
        "prerender": "npm run build:prerender && npm run serve:prerender",
        "build": "ng build",
        "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
        "build:prerender": "npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:prerender",
        "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
        "generate:prerender": "cd dist && node prerender",
        "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
        "serve:prerender": "cd dist/browser && http-server",
        "serve:ssr": "node dist/server"
    }
}

開發只需運行 npm run start

執行 npm run ssr 編譯應用程式,並啟動一個Node Express來為應用程式提供服務 http://localhost:4000

dist目錄:

執行npm run prerender - 編譯應用程式並預渲染應用程式文件,啟動一個演示http伺服器,以便您可以查看它 http://localhost:8080

註意: 要將靜態網站部署到靜態托管平臺,您必須部署dist/browser文件夾, 而不是dist文件夾

dist目錄:

根據項目實際的路由信息併在根目錄的 static.paths.ts 中配置,提供給 prerender.ts 解析使用。

export const ROUTES = [
    '/',
    '/lazy'
];

因此,從dist目錄可以看到,服務端預渲染會根據配置好的路由在 browser 生成對應的靜態index.html。如 / 對應 /index.html/lazy 對應 /lazy/index.html

服務端的模塊懶載入

在前面的介紹中,我們在 app.server.module.ts 中導入了 ModuleMapLoaderModule,在 app.module.ts

ModuleMapLoaderModule 模塊可以使得懶載入的模塊也可以在服務端進行渲染,而你要做也只是在 app.server.module.ts 中導入。

服務端到客戶端的狀態傳輸

在前面的介紹中,我們在 app.server.module.ts 中導入了 ServerTransferStateModule,在 app.module.ts 中導入了 BrowserTransferStateModuleTransferHttpCacheModule

這三個模塊都與服務端到客戶端的狀態傳輸有關:

  • ServerTransferStateModule:在服務端導入,用於實現將狀態從服務端傳輸到客戶端
  • BrowserTransferStateModule:在客戶端導入,用於實現將狀態從服務端傳輸到客戶端
  • TransferHttpCacheModule:用於實現服務端到客戶端的請求傳輸緩存,防止客戶端重覆請求服務端已完成的請求

使用這幾個模塊,可以解決 http請求在服務端和客戶端分別請求一次 的問題。

比如在 home.component.ts 中有如下代碼:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Component({
    selector: 'app-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit, OnDestroy {
    constructor(public http: HttpClient) {
    }
    
    ngOnInit() {
        this.poiSearch(this.keyword, '北京市').subscribe((data: any) => {
            console.log(data);
        });
    }
    
    ngOnDestroy() {
    }
    
    poiSearch(text: string, city?: string): Observable<any> {
        return this.http.get(encodeURI(`http://restapi.amap.com/v3/place/text?keywords=${text}&city=${city}&offset=20&key=55f909211b9950837fba2c71d0488db9&extensions=all`));
    }
}

代碼運行之後,

服務端請求並列印:

客戶端再一次請求並列印:

方法1:使用 TransferHttpCacheModule

使用 TransferHttpCacheModule 很簡單,代碼不需要改動。在 app.module.ts 中導入之後,Angular自動會將服務端請求緩存到客戶端,換句話說就是服務端請求到數據會自動傳輸到客戶端,客戶端接收到數據之後就不會再發送請求了。

方法2:使用 BrowserTransferStateModule

該方法稍微複雜一些,需要改動一些代碼。

調整 home.component.ts 代碼如下:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

const KFCLIST_KEY = makeStateKey('kfcList');

@Component({
    selector: 'app-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit, OnDestroy {
    constructor(public http: HttpClient,
                private state: TransferState) {
    }
    
    ngOnInit() {
    
        // 採用一個標記來區分服務端是否已經拿到了數據,如果沒拿到數據就在客戶端請求,如果已經拿到數據就不發請求
        const kfcList:any[] = this.state.get(KFCLIST_KEY, null as any);

        if (!this.kfcList) {
            this.poiSearch(this.keyword, '北京市').subscribe((data: any) => {
                console.log(data);
                this.state.set(KFCLIST_KEY, data as any); // 存儲數據
            });
        }
    }
    
    ngOnDestroy() {
        if (typeof window === 'object') {
            this.state.set(KFCLIST_KEY, null as any); // 刪除數據
        }
    }
    
    poiSearch(text: string, city?: string): Observable<any> {
        return this.http.get(encodeURI(`http://restapi.amap.com/v3/place/text?keywords=${text}&city=${city}&offset=20&key=55f909211b9950837fba2c71d0488db9&extensions=all`));
    }
}
  • 使用 const KFCLIST_KEY = makeStateKey('kfcList') 創建儲存傳輸數據的 StateKey
  • HomeComponent 的構造函數中註入 TransferState
  • ngOnInit 中根據 this.state.get(KFCLIST_KEY, null as any) 判斷數據是否存在(不管是服務端還是客戶端),存在就不再請求,不存在則請求數據並通過 this.state.set(KFCLIST_KEY, data as any) 存儲傳輸數據
  • ngOnDestroy 中根據當前是否客戶端來決定是否將存儲的數據進行刪除

客戶端與服務端渲染對比

最後,我們分別通過這三個原因來進行對比:

  1. 幫助網路爬蟲(SEO)

  2. 提升在手機和低功耗設備上的性能

  3. 迅速顯示出首頁

幫助網路爬蟲(SEO)

客戶端渲染:

服務端渲染:

從上面可以看到,服務端提前將信息渲染到返回的頁面上,這樣網路爬蟲就能直接獲取到信息了(網路爬蟲基本不會解析javascript的)。

提升在手機和低功耗設備上的性能

這個原因通過上面就可以看出,對於一些低端的設備,直接顯示頁面總比要解析javascript性能高的多。

迅速顯示出首頁

同樣在 Fast 3G 網路條件下進行測試

客戶端渲染:

服務端渲染:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大多數情況下容易忘記的快捷鍵,在此整理了一下。 編輯快捷鍵:cmd+L:選擇行(重覆按下將下一行加入選擇);cmd+D:選擇詞(重覆按下時多重選擇相同的詞進行多重編輯);cmd+shift+D 複製游標所在整行,插入到下一行control+shift+M:選擇括弧的內容; cmd+shift+ent ...
  • H5中新增的表單屬性:autofocus(獲得焦點)、placeholder(給出提示信息)、list(類似下拉列表)、autocomplete(有記錄的話會自動出現記錄信息)input元素:color(顏色選擇器) date(日期選擇器) time(時間選擇器) datetime-local(本地 ...
  • background-clip屬性的通俗作用就是指定元素背景所在的區域,有四種取值 1、border-box border-box是預設值,表示元素的背景從border區域(包括border)以內開始保留背景。 簡單代碼如下: 效果如下: 從上圖我們可以看出,元素背景預設是存在於邊框及以內的區域,但 ...
  • 在製作網頁動態效果時,一定會遇到某些需求,要求某段程式等待多時時間後再開始執行,就像在我們的生活中一樣,待會兒再開始做一件事。在JavaScript中主要通過定時器實現此類需求,本文將對定時器做一個概括,正對setTimeout()做一個詳細用法總結。 一.setInterval與setTimeou ...
  • [轉載] meta的標簽的使用是我在前端學習中曾經困惑過一段時間的問題。一方面不是很瞭解meta標簽的用途,另一方面是對於meta標簽里的屬性和值不是懂,也不知道從哪裡冒出來的,所以這篇文章專門整理下meta標簽,一個必用且關鍵的標簽。 什麼是meta標簽?引自下W3school的定義說明一下。元數 ...
  • 1 2 3 4 5 拖放API 6 31 32 33 34 請拖放 35 36 37 94 95 ...
  • 一.水平居中 1.文字水平居中 <div class="one"> 測試居中 </div> <style> .one{ width: 200px; height: 100px; border:1px solid red; text-align: center; } </style> 2.盒子居中 < ...
  • 1.效果圖 2.源碼 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...