asp.net core 系列之Response caching(1)

這篇文章簡單的講解了response caching:

講解了cache-control,及對其中的頭和值的作用,及設置來控制response caching;

簡單的羅列了其他的緩存技術:In-memory caching , Distributed Cache , Cache Tag Helper , Distributed Cache Tag Helper ;

講解了使用ResponseCache attribute來控制response caching生成合適的頭

 主要翻譯於官網,水平有限,見諒

Overview

響應緩存減少了客戶端或者代理到web服務器的請求的次數響應緩存也可以減少web服務器的生成響應的執行的工作量。響應緩存被頭部控制,頭部指出了你想要客戶端,代理和中間件怎樣緩存響應。

ResponseCache attribute 參加設置響應緩存頭部,which clients may honor when caching responses. (當緩存響應時,客戶端會受這些屬性影響)Response Caching Middleware 可以被用來在服務器上緩存響應。 中間件可以使用ResponseCacheAttribute屬性來影響服務端緩存行為。 

HTTP-based response caching 

HTTP 1.1 Caching specification(規格,詳述,說明書)描述了網絡緩存應該怎樣表現(Internet caches should behave.) 主要的用於緩存的HTTP頭,是Cache-Control, 它被用於指定緩存指令。這個指令控制緩存行為,當請求從客戶端到服務端的時候,並且當響應從服務端返回客戶端的時候。

公共的Cache-Control 指令在下錶中被展示了:

其他緩存頭在緩存中扮演的角色,羅列在下面了:

注意:Cache-Control,是用在從請求中的HTTP頭,可以用來控制服務器中緩存行為。

HTTP-based caching respects request Cache-Control directives

HTTP 1.1 Caching specification for the Cache-Control header (HTTP 1.1 緩存規格對於Cache-Control)要求一個緩存遵守一個有效的Cache-Control ,這個Cache-Control頭是被客戶端發送的。一個客戶端可以發送一個帶no-cacheheader,並且強制要求服務器為每個請求生成一個新的響應。

總是遵守客戶端Cache-Control請求頭是有意義的,如果你考慮HTTP緩存的目標。在官方的說明書下,

緩存意味着減少潛在因素和網絡管理,對滿足請求跨客戶端,代理和服務器網絡。它不是一種控制原服務器上的加載的必須的方式。

當使用Response Caching 中間件時,開發者是沒法對緩存行為控制的。因為中間件附着於官方緩存說明書。當決定提供一個緩存響應時,對這个中間件的計劃豐富(planned enhancements to the middleware)對於達到配置中間件來忽視請求的Cache-Control頭的目的,是一個機會(Planned enhancements to middleware are an opportunity to middleware to ignore a request’s Cache-Control header when deciding to serve a cached response.)。計劃的豐富提供了一個機會來更好的控制服務器加載。

Other caching technology in ASP.NET Core ASP.NET Core上的其他緩存技術

  • In-memory caching 內存緩存

    In-memory caching 使用服務器內存來存儲緩存數據。這種類型的緩存適合使用sticky sessionsticky:不動的)的一個或者多個服務器Sticky sessions 意味着從客戶端發出的請求總是路由到同一台服務器處理。

    更多信息:Cache in-memory in ASP.NET Core.

  • Distributed Cache 分佈式緩存

    使用一個分佈式緩存來存儲數據在內存中,當應用部署在雲上或者服務器集群上時。緩存是在這些處理請求的服務器之間共享的。客戶端可以提交一個請求,請求可以被組群里的任意服務器處理,如果緩存數據對於客戶端是可用的。ASP.NET Core提供了SQL ServerRedis分佈式緩存

    更多信息:Distributed caching in ASP.NET Core.

  • Cache Tag Helper

    使用Cache Tagmvc頁面或者Razor Page中緩存內容Cache Tag Helper用內存緩存數據

    更多信息:Cache Tag Helper in ASP.NET Core MVC

  • Distributed Cache Tag Helper

    在分佈式雲或者web集群場景中使用Distributed Cache Tag Helper 來緩存Mvc view或者Razor Page中的內容The Distributed Cache Tag Helper SQL Server或者Redis來緩存數據

    更多信息:Distributed Cache Tag Helper in ASP.NET Core.

ResponseCache attribute

為了在response caching (響應緩存)上設置合適的頭,ResponseCacheAttribute 指出了必須的參數。(即,可以通過ResponseCacheAttribute,設置response caching上的頭的值)

注意:對於包含驗證信息的客戶端內容,不允許緩存。對於那些不會基於用戶的身份或者用戶是否登錄而改變的內容,才應該允許被緩存。

VaryByQueryKeys 隨着給出的query keys的集合的值,改變存儲的響應。When a single value of * is provided, the middleware varies responses by all request query string parameters. 

Response Caching Middleware 必須被允許設置VaryByQueryKeys屬性。否則,一個運行時異常會被拋出。對於VaryByQueryKeys屬性,並沒有一個對應的HTTP頭部。這個屬性是一個被Response Caching Middleware 處理的HTTP 功能。對於中間件提供一個緩存的響應,查詢字符串和查詢字符串值必須匹配之前的請求.(即,如果查詢字符串和查詢字符串值和之前的一樣時,中間件會直接返回一個緩存的響應;否則,返回一個新的響應。)例如,考慮下錶中的一系列的請求和結果:

第一個請求被服務器返回,並且緩存到中間件中。第二個請求是被中間件返回,因為查詢字符串匹配之前的請求。第三個請求不是在中間件緩存中的,因為查詢字符串值不匹配之前的請求。

ResponseCacheAttribute用於配置和創建一個ResponseCacheFilter.    

ResponseCacheFilter執行的工作,更新合適的HTTP頭和響應的功能(即,ResponseCacheAttribute的功能)The filter:

  • 移除任何存在的Vary, Cache-Control, Pragma頭部
  • 根據設置在ResponseCacheAttribute中的屬性輸出合適的頭部

  • 更新the response caching HTTP feature如果VaryByQueryKeys被設置了

Vary

這個頭部會被寫,當VaryByHeader屬性被設置了。這個屬性(VaryByHeader)設置Vary屬性的值。下面是使用VaryByHeader屬性的例子:

[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)] public class Cache1Model : PageModel
{

用這個例子,使用瀏覽器工具觀察response headers(響應頭)。 下面的響應頭隨着Cache1 page response 被發送了。

Cache-Control: public,max-age=30
Vary: User-Agent
NoStore and Location.None

NoStore重寫了大部分的其他屬性。當這個屬性被設置為true,Cache-Control頭被設置為no-store.

如果Location設置為None:

  • Cache-Control 設置為no-store, no-cache

  • Pragma設置為no-cache.

如果NoStorefalse並且LocationNoneCache-Control ,Pragma被設置為no-cache.

NoStore是典型的被設置為true,為了error pages. 示例中的Cache2 page生成響應頭,指示客戶端不要存儲響應。

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class Cache2Model : PageModel
{

這個示例應用返回Cache2 page 帶有下面的頭:

Cache-Control: no-store,no-cache
Pragma: no-cache
Location and Duration

為了可以緩存,Duration必須被設置為一個積極的值並且Location必須是任意的或者Client. 這種情況下Cache-Control頭被設置為location的值,並且跟着一個響應的max-age.

注意:

Location’s options of Any and Client轉化為Cache-Control頭的值分別為publicprivate. 正如之前提到的,設置LocationNone會設置Cache-ControlPramga頭為no-cache:

[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false)] public class Cache3Model : PageModel
{

示例應用返回的Cache3 page 帶有下面的頭:

Cache-Control: public,max-age=10
Cache profiles

取代重複的在很多controller action attributes響應緩存設置,cache profiles 可以被設置為options,當在Startup.ConfigureService中設置MVC/Razor Pages. 在引用的cache profiles中發現的值被用作默認值,隨着ResponseCacheAttribute並且被這個attribute上指定的任意properties重寫。(即很多重複的響應緩存設置可以在Startup.ConfigureService中設置,再隨着ResponseCacheAttribute設置在action上)

建立一個cache profile. 下面的例子展示了30秒的cache profile,在示例應用的Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.CacheProfiles.Add("Default30", new CacheProfile() { Duration = 30 });
    }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

這個示例應用的Cache4 page model 引用了Default30 cache profile:

[ResponseCache(CacheProfileName = "Default30")] public class Cache4Model : PageModel
{

這個ResponseCacheAttribute可以被用在

  • Razor Page handlers(classes) – 屬性可以被用到處理方法

  • MVC controllers(classes)

  • MVC actions (methods) – Method-level attributes override the settings specified in class level attributes. 方法級別的會覆蓋類級別的

Default30 profile導致的應用於Cache4 page response 的頭是:

Cache-Control: public,max-age=30

 

下一篇:Cache in-memory

 參考資料:

https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-2.2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

自定義Spring Shell

目錄

  • 概述
  • 自定義內置命令
    • 禁用內置命令
    • 覆蓋內置命令
  • 自定義命令提示符
  • 自定義命令行選項行為
  • 自定義參數轉換器

概述

官網:https://projects.spring.io/spring-shell/。

Spring Shell除了提供一些常用的內置命令之外,還允許開發者對一些默認功能進行定製。

自定義內置命令

禁用內置命令

禁用Spring Shell的內置命令非常簡單,只需要在pom.xml文件中進行簡單配置即可,如下所示:

<!-- Spring Shell -->
<dependency>
    <groupId>org.springframework.shell</groupId>
    <artifactId>spring-shell-starter</artifactId>
    <version>2.0.0.RELEASE</version>
    <exclusions>
        <!-- 禁用內置命令 -->
        <exclusion>
            <groupId>org.springframework.shell</groupId>
            <artifactId>spring-shell-standard-commands</artifactId>
        </exclusion>
    </exclusions>
</dependency>
shell:>help
No command found for 'help'
shell:>exit
No command found for 'exit'
shell:>

完全禁用了所有內置命令之後,將無法通過help命令查詢其他命令信息,也不能再使用exit命令退出應用。
因此,如果有需要的情況下,應該只是禁用某些內置命令。

如果需要禁用指定內置命令,需要在代碼中設置對應的命令屬性為false,格式為:spring.shell.command.<command>.enabled=true
例如,需要禁用help命令:

@SpringBootApplication
public class TestSpringshellApplication {
    public static void main(String[] args) {
        String[] disabledCommands = new String[]{"--spring.shell.command.help.enabled=false"};
        String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
        SpringApplication.run(TestSpringshellApplication.class, fullArgs);
    }
}
# help命令將不再能使用
shell:>help
No command found for 'help'
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
shell:>exit

如果禁用的是其他命令,如:clear,在Spring Shell應用啟動之後通過help命令不再能看被禁用的命令了。

@SpringBootApplication
public class TestSpringshellApplication {
    public static void main(String[] args) {
        // 禁用了內置的clear命令
        String[] disabledCommands = new String[]{"--spring.shell.command.clear.enabled=false"};
        String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
        SpringApplication.run(TestSpringshellApplication.class, fullArgs);
    }
}
shell:>help
AVAILABLE COMMANDS

Built-In Commands
        exit, quit: Exit the shell.
        help: Display help about available commands.
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.

顯然,在禁用了指定的內置命令之後,通過help命令將不能看到該命令了。

覆蓋內置命令

如果希望重寫內置命令的實現,可以通過實現接口org.springframework.shell.standard.commands.<Command>.Command來完成(如:需要重寫clear命令的實現,實現接口org.springframework.shell.standard.commands.Clear.Command)。
如下為重寫內置命令script的實現:

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.commands.Script;
// 實現接口org.springframework.shell.standard.commands.Script.Command
@ShellComponent
public class MyScript implements Script.Command {
    // 注意:命令名稱與內置命令保持一致
    @ShellMethod("Read and execute commands from a file.")
    public void script() {
      / // 實現自定義邏輯
        System.out.println("override default script command");
    }
}

有意思的是,此時在內置命令“Built-In Commands”分組中將不能看到script命令了,而是在自定義的分組中,

shell:>help
AVAILABLE COMMANDS

Built-In Commands  # 在內置命令分組中看不到重寫的命令了
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        stacktrace: Display the full stacktrace of the last error.

My Script          # 重寫的命令此時在自定義分組中
        scriptdo: Read and execute commands from a file.

如果希望被覆蓋的內置命令依然能夠在“Built-In Commands”分組中看到,可以通過註解@ShellMethod的group屬性指定。

// 指定被覆蓋的內置命令分組為“Built-In Commands”
@ShellMethod(value = "Read and execute commands from a file.", group = "Built-In Commands")
public void script() {
    System.out.println("override default script command");
}
shell:>help
AVAILABLE COMMANDS

Built-In Commands
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.

shell:>script
override default script command

自定義命令提示符

默認情況下,Spring Shell啟動之後显示的是一個黃色的命令提示符(shell:>)等待用戶輸入。
可以通過Spring Shell提供的接口org.springframework.shell.jline.PromptProvider對該命令提示符進行定製。

// 通過實現接口org.springframework.shell.jline.PromptProvider定製命令提示符
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.stereotype.Component;
@Component
public class MyPromptProvider implements PromptProvider {
    @Override
    public AttributedString getPrompt() {
        // 定製命令提示符為紅色的“#”
        return new AttributedString("#", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
    }
}

如下為定製的命令提示符:

自定義命令行選項行為

Spring Shell提供了2個默認的ApplicationRunner,用於實現命令行選項的行為。

1.InteractiveShellApplicationRunner用於啟動交互式界面,接收用戶輸入命令。
2.ScriptShellApplicationRunner用於在應用啟動時從程序參數中讀取指定文件中的命令並執行,具體來講:將多個命令寫在文件中,並通過參數的形式將包含了批量命令的文件路徑傳遞給程序,傳遞的文件路徑參數必須以“@”開始,如下示例:

$ java -jar /home/test/sun/workspace/test-springshell/target/test-springshell-0.0.1-SNAPSHOT.jar @/home/test/cmd

文件/home/test/cmd中的內容為:

$ cat /home/test/cmd 
help

這樣,在啟動程序時,將會自動執行/home/test/cmd文件中的命令(如果文件不存在,啟動應用時報錯)。
值得注意的是: 當在程序參數中存在“@local_file_path”這樣的參數時,應用啟動后執行完文件“local_file_path”內命令之後就退出了,不會進入交互式命令行界面(上述示例中,應用啟動后執行help命令之後就退出了)。

如果Spring Shell默認提供的上述2個ApplicationRunner無法滿足需求,可以自定義其他的命令行選項行為,直接實現接口org.springframework.boot.ApplicationRunner即可。

自定義參數轉換器

默認情況下,Spring Shell使用標準的Spring類型轉換機制將命令行的文本參數轉換為指定的類型。
實際上,Spring Shell是通過DefaultConversionService註冊Converter<S, T>GenericConverter或者ConverterFactory<S, R>類型的Bean對象來實現對命令行參數進行類型轉換的。

換句話說,如果我們需要自定義類型轉換器,只需要簡單實現接口org.springframework.core.convert.converter.Converter<S, T>就可以了。

// 自定義類型
public class Food {
    private String value = null;
    public Food(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return new StringBuilder()
                .append("Food{").append("value='").append(value).append("'}")
                .toString();
    }
}

// 自定義類型轉換器
@Component
public class MyConverter implements Converter<String, Food> {
    @Override
    public Food convert(String s) {
        // 將輸入參數轉換為Food類型實例
        return new Food(s);
    }
}

// 使用自定義轉換類型
@ShellComponent
public class ConvertionCmd {
    // 在命令方法中直接可以獲取Food對象,這是通過前面的自定義類型轉換器MyConverter實現的
    @ShellMethod("Conversion food")
    public String food(Food food) {
        return food.toString();
    }
}

在命令行指定命令food

#food apple
Food{value='apple'}

顯然,通過自定義類型轉換器可以實現對命令參數的特殊處理,非常實用。

【參考】
https://blog.csdn.net/zknxx/article/details/52196427 SpringBoot之CommandLineRunner接口和ApplicationRunner接口
https://www.jianshu.com/p/5d4ffe267596 CommandLineRunner或者ApplicationRunner接口

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

基於vue + axios + lrz.js 微信端圖片壓縮上傳

業務場景

微信端項目是基於Vux + Axios構建的,關於圖片上傳的業務場景有以下幾點需求:

1、單張圖片上傳(如個人頭像,實名認證等業務)
2、多張圖片上傳(如某類工單記錄)
3、上傳圖片時期望能按指定尺寸壓縮處理
4、上傳圖片可以從相冊中選擇或者直接拍照

遇到的坑

採用微信JSSDK上傳圖片

在之前開發的項目中(mui + jquery),有使用過微信JSSDK的接口上傳圖片,本想應該能快速遷移至此項目。事實證明編程沒有簡單的事:
1、按指定尺寸壓縮圖片
JSSDK提供的接口wx.chooseImage 是不能指定圖片壓縮尺寸的,只能在後端的接口通過localId獲取圖片時,再轉換成指定的尺寸。
2、微信JSSDK的接口權限驗證
只要是單頁面應用項目,微信JSSDK注入權限驗證都會有這個坑,而這個與路由模式(hash 或 history)也有關聯。有關此坑, 後續會再次寫文總結。參考解決方案[微信JSSDK] 解決SDK注入權限驗證 安卓正常,IOS出現config fail
經過權衡考慮網頁可能需要在微信以外的瀏覽器上也能上傳文件,顧後來放棄了採用微信JSSDK接口上傳圖片的方式。

android版微信,input onchange事件不觸發

這個坑,圈內有很多人踩過了。在PC端測試是正常的,發布之後,微信端上傳時能選擇文件,但之後沒有任何效果。日誌跟蹤,後台的api都未調用,由此判斷是input的onchange事件未被觸發。
解決方案, 更改input的 accept屬性:

<input ref="file" type="file" accept="image/jpeg,image/png" @change="selectImgs" />

將以上代碼更改為:

<input ref="file" type="file" accept="image/*" @change="selectImgs" />

如果不允許從相冊中選擇,只能拍照,增加capture=”camera”:

<input ref="file" type="file" accept="image/*" capture="camera" @change="selectImgs" />

(注:如果場景支持從相冊選擇或拍照,測試發現某些機型拍照后返回到了主頁。哈哈,也有可能是其他因素引起的問題,未做深究了)

使用Lrz.js壓縮圖片

目前手機拍照的圖片文件大小一般在3-4M,如果在上傳時不做壓縮處理會相當浪費流量並且佔用服務器的存儲空間(期望上傳原圖的另做討論)。如果能夠在前端壓縮處理,那肯定是最理想的方案。而lrz.js則提供了前端圖片文件的壓縮方案,並且可以指定尺寸壓縮。實測:3M左右的圖片文件,按寬度450px尺寸壓縮上傳后的文件大小在500kb左右,上傳時間2s以內。
其核心源碼,如下:

selectImgs () {
  let file = this.$refs.file.files[0]
  lrz(file, { width: 450, fieldName: 'file' }).then((rst) => {
    var xhr = new XMLHttpRequest()
    xhr.open('POST', 'http://xxx.com/upload')

    xhr.onload = () => {
      if (xhr.status === 200 || xhr.status === 304) {
        // 無論後端拋出何種錯誤,都會走這裏
        try {
          // 如果後端跑異常,則能解析成功, 否則解析不成功
          let resp = JSON.parse(xhr.responseText)
          console.log('response: ', resp)
        } catch (e) {
          this.imageUrl = xhr.responseText
        }
      }
    }

    // 添加參數
    rst.formData.append('folder', 'wxAvatar') // 保存的文件夾
    rst.formData.append('base64', rst.base64)
    // 觸发上傳
    xhr.send(rst.formData)

    return rst
  })
}

單個圖片上傳組件完整代碼,如下(注: icon圖標使用的是svg-icon組件):

<template>
  <div class="imgUploader">
    <section v-if="imageUrl"
             class="file-item ">
      <img :src="imageUrl"
           alt="">
      <span class="file-remove"
            @click="remove()">+</span>
    </section>
    <section v-else
             class="file-item">
      <div class="add">
        <svg-icon v-if="!text"
                  class="icon"
                  icon-class="plus" />
        <span v-if="text"
              class="text">{{text}}</span>
        <input type="file"
               accept="image/*"
               @change="selectImgs"
               ref="file">
      </div>
    </section>
  </div>
</template>

<script>
import lrz from 'lrz'
export default {
  props: {
    text: String,
    // 壓縮尺寸,默認寬度為450px
    size: {
      type: Number,
      default: 450
    }
  },
  data () {
    return {
      img: {
        name: '',
        src: ''
      },
      uploadUrl:  'http://ff-ff.xxx.cn/UploaderV2/Base64FileUpload',
      imageUrl: ''
    }
  },
  watch: {
    imageUrl (val, oldVal) {
      this.$emit('input', val)
    },
    value (val) {
      this.imageUrl = val
    }
  },
  mounted () {
    this.imageUrl = this.value
  },
  methods: {
    // 選擇圖片
    selectImgs () {
      let file = this.$refs.file.files[0]
      lrz(file, { width: this.size, fieldName: 'file' }).then((rst) => {
        var xhr = new XMLHttpRequest()
        xhr.open('POST', this.uploadUrl)

        xhr.onload = () => {
          if (xhr.status === 200 || xhr.status === 304) {
            // 無論後端拋出何種錯誤,都會走這裏
            try {
              // 如果後端跑異常,則能解析成功, 否則解析不成功
              let resp = JSON.parse(xhr.responseText)
              console.log('response: ', resp)
            } catch (e) {
              this.imageUrl = xhr.responseText
            }
          }
        }

        // 添加參數
        rst.formData.append('folder', this.folder) // 保存的文件夾
        rst.formData.append('base64', rst.base64)
        // 觸发上傳
        xhr.send(rst.formData)

        return rst
      })
    },
    // 移除圖片
    remove () {
      this.imageUrl = ''
    }
  }
}
</script>

<style lang="less" scoped>
.imgUploader {
  margin-top: 0.5rem;
  .file-item {
    float: left;
    position: relative;
    width: 100px;
    text-align: center;
    left: 2rem;
    img {
      width: 100px;
      height: 100px;
      border: 1px solid #ececec;
    }
    .file-remove {
      position: absolute;
      right: 0px;
      top: 4px;
      width: 14px;
      height: 14px;
      color: white;
      cursor: pointer;
      line-height: 12px;
      border-radius: 100%;
      transform: rotate(45deg);
      background: rgba(0, 0, 0, 0.5);
    }

    &:hover .file-remove {
      display: inline;
    }
    .file-name {
      margin: 0;
      height: 40px;
      word-break: break-all;
      font-size: 14px;
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }
  }
  .add {
    width: 100px;
    height: 100px;
    float: left;
    text-align: center;
    line-height: 100px;
    font-size: 30px;
    cursor: pointer;
    border: 1px dashed #40c2da;
    color: #40c2da;
    position: relative;
    background: #ffffff;
    .icon {
      font-size: 1.4rem;
      color: #7dd2d9;
      vertical-align: -0.25rem;
    }
    .text {
      font-size: 1.2rem;
      color: #7dd2d9;
      vertical-align: 0.25rem;
    }
  }
}
input[type="file"] {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border: 1px solid #000;
  opacity: 0;
}
</style>

後端圖片存儲處理

後端api對圖片的處理,是必不可少的環節,需要將前端提交過來的base64字符串轉換成圖片格式,並存放至指定的文件夾,接口返回圖片的Url路徑。各項目後端對圖片的處理邏輯都不一致,以下方案僅供參考(我們使用asp.net MVC 構建了獨立的文件存儲站點)。
其核心源碼,如下:

/// <summary>
/// 圖片文件base64上傳
/// </summary>
/// <param name="folder">對應文件夾位置</param>
/// <param name="base64">圖片文件base64字符串</param>
/// <returns></returns>
public ActionResult Base64FileUpload(string folder, string base64)
{
    var context = System.Web.HttpContext.Current;
    context.Response.ClearContent();
    // 因為前端調用時,需要做跨域處理
    context.Response.AddHeader("Access-Control-Allow-Origin", "*");
    context.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
    context.Response.AddHeader("Access-Control-Allow-Headers", "content-type");
    context.Response.AddHeader("Access-Control-Max-Age", "30");
    if (context.Request.HttpMethod.Equals("OPTIONS"))
    {
        return Content("");
    }

    var resultStr = base64.Substring(base64.IndexOf(",") + 1);//需要去掉頭部信息,這很重要
    byte[] bytes = Convert.FromBase64String(resultStr);
    var fileName = Guid.NewGuid().ToString() + ".png";
    if (folder.IsEmpty()) folder = "folder";
    //本地上傳
    string root = string.Format("/Resource/{0}/", folder);
    string virtualPath = root + fileName;
    string path = Server.MapPath("~" + virtualPath);
    //創建文件夾
    if (!Directory.Exists(Path.GetDirectoryName(path)))
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path));
    }
    System.IO.MemoryStream ms = new System.IO.MemoryStream(bytes);//轉換成無法調整大小的MemoryStream對象
    System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(ms);
    bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);//保存到服務器路徑
    ms.Close();//關閉當前流,並釋放所有與之關聯的資源
    return Content(Net.Url + virtualPath); //返迴文件路徑
}

結語

由於項目實際情況,上述的方案中還存在諸多未完善的點:
1、多張圖片上傳,還是採用的與單張圖片相同的接口處理, 更為完善的方案是,前端的多圖上傳組件只綁定一個關聯Id,即可通過實現上傳和將圖片列表查詢展示(注:該功能在微信端未實現)。
2、後端圖片上傳的接口,未做嚴格的安全校驗,更為完善的方案是,每個上傳的場景,都應該限制文件類型,限制文件大小,以及文件數據來源校驗(注: 如軟件需要按二級等保標準測評,則後端接口會檢測通不過)。
3、上傳組件,未显示上傳進度,體驗性稍差。
正如前文所述,出於項目實際情況考慮,只是簡單實現圖片壓縮上傳功能,如要支持更多的場景,還得細細雕琢。

參考

1、移動端H5實現圖片上傳
2、安卓版微信 input onchange事件不生效

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!