Webpack中hash的用法

webpack的配置項中,可能會見到hash這樣的字符。javascript

當存在hash配置的時候,webpack的輸出將能夠獲得形如這樣的文件:css

page1_bundle_54e8c56e.js

這種帶哈希值的文件名,能夠幫助實現靜態資源的長期緩存,在生產環境中很是有用。關於這一點的詳細內容,能夠參考這篇久遠的大公司裏怎樣開發和部署前端代碼html

在webpack中配置hash

下面是一個帶hash輸出的webpack配置的例子(webpack v3.0.0):前端

var env = {
    src: path.resolve(__dirname, './src'),
    output: path.resolve(__dirname, './dist'),
    publicPath: '/'
};

module.exports = {
    entry: {
        'page1': './page1',
        'page2': './page2'
    },
    context: env.src,
    output: {
        path: env.output,
        filename: './[name]/bundle_[chunkhash:8].js',
        publicPath: env.publicPath
    },
    devtool: false,
    module: {
        rules: [{
            test: /\.(png|jpg)$/,
            use: 'url-loader?limit=8192&name=[path][name]_[hash:8].[ext]'
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: 'style-loader',
                use: 'css-loader'
            })
        }]
    },
    plugins: [
        new ExtractTextPlugin({
            filename: './[name]/style_[contenthash:8].css'
        })
    ]
};

能夠看到,有多個地方都出現了hash這個詞,但形式不太同樣。java

output的狀況

output的filename能夠指定hash。有兩個值能夠選擇:webpack

  • [hash]。hash值是特定於整個構建過程的。git

  • [chunkhash]。hash值是特定於每個文件的內容的。github

咱們理想的緩存設計是,在一次版本更新(從新構建)後,只有當一個文件的內容確實發生了變化,它才須要被從新下載,不然應使用緩存。web

所以,以上兩個值中更推薦的是[chunkhash]。你也能夠閱讀這篇官方的緩存指南瞭解更多細節。spring

file-loader的狀況

url-loaderfile-loader是同一家,參照file-loader文檔可知,文件名name可使用標識符[hash]來啓用hash。此外,你還能夠按照[<hashType>:hash:<digestType>:<length>]的格式更詳細地定製hash結果。

[hash:8]中的:8則和前面output的同樣,指定了hash結果的截取長度。

extract-text-webpack-plugin的狀況

被引用的css經過extract-text-webpack-plugin來獲得帶hash的文件。參照extract-text-webpack-plugin文檔,在指定生成文件的文件名filename時可使用標識符[contenthash](能夠看到,和以前的並不相同)。

引用帶hash的文件

當靜態資源的文件名變成這樣的帶哈希值的版本後,引用這些靜態資源就須要稍多花一點工夫。

純前端的狀況

若是沒有任何服務端,只是純html、css、js的前端應用的話,通常使用html-webpack-plugin

例如,新建一個index.ejs模板文件以下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>App Example</title>
</head>

<body>
    <main id="root"></main>
</body>

</html>

而後增長html-webpack-plugin到webpack:

{
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.ejs'
    })
  ]
}

執行一次webpack構建,獲得生成的index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>App Example</title>
    <link href="/page1/style_626f7c3f.css" rel="stylesheet">
</head>

<body>
    <main id="root"></main>
    <script type="text/javascript" src="/page1/bundle_0f33bdc8.js"></script>
</body>

</html>

能夠看到,html-webpack-plugin在模板文件內容的基礎上,就添加好了須要引用的bundle js。若是還有生成的css文件(經過extract-text-webpack-plugin),也會被添加到適當的位置。

純前端、多頁的狀況

若是webpack有多個entry文件,例如本文最前面給出的例子:

{
    entry: {
        'page1': './page1',
        'page2': './page2'
    }
}

在這種狀況下,html-webpack-plugin會把所有entry的輸出都集中到一個.html裏。因此,這可能並非咱們想要的。

咱們更但願的是爲每個entry生成一個.html。這時候,可使用的是multipage-webpack-plugin。這個插件實際也依賴了html-webpack-plugin。

例如,有這樣的目錄結構:

.
├─ package.json
├─ src
│   ├─ page1
│   │   ├─ index.css
│   │   ├─ index.ejs
│   │   ├─ index.js
│   │   └─ potofu.jpg
│   └─ page2
│       ├─ index.css
│       ├─ index.ejs
│       └─ index.js
└─ webpack.config.js

而後在webpack配置文件中加入multipage-webpack-plugin:

{
  plugins: [
    new MultipageWebpackPlugin({
        htmlTemplatePath: '[name]/index.ejs',   // 源模板文件的位置
        bootstrapFilename: 'manifest.js',
        templatePath: '[name]'  // 輸出html文件的路徑
    }),
  ]
}

[name]標識符對應的是每個entry的名稱(注意,在本文的時間點,須要使用multipage-webpack-plugin的master分支,也就是最新版,才支持此標識符)。在這個例子中,只有兩個取值:page1page2

bootstrapFilename如字面意義,是指保存webpack的bootstrap代碼的文件命名。而webpack的bootstrap代碼被這樣單獨放到一個文件裏,是由於multipage-webpack-plugin在內部(強行)爲你啓用了CommonsChunkPlugin

執行一次webpack構建,獲得的輸出結果:

dist
├─ manifest.js
├─ page1
│   ├─ bundle_29862ad6.js
│   ├─ index.html
│   ├─ potofu_26766d43.jpg
│   └─ style_0b5ab6ef.css
├─ page2
│   ├─ bundle_6a9c6f12.js
│   ├─ index.html
│   └─ style_914dffd0.css
└─ shared
    └─ bundle_9fa1a762.js

取其中一個page1/index.html,內容是:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>page1</title>
    <link href="/page1/style_0b5ab6ef.css" rel="stylesheet">
</head>

<body>
    <div class="page-box">page1</div>
    <script type="text/javascript" src="/manifest.js"></script>
    <script type="text/javascript" src="/shared/bundle_9fa1a762.js"></script>
    <script type="text/javascript" src="/page1/bundle_29862ad6.js"></script>
</body>

</html>

能夠看到,關聯的css、js靜態資源,都已被正確添加。

帶服務端的狀況

若是是帶服務端的應用,引用帶hash的資源文件將是另外一個思路。

常見的作法是,爲全部的靜態資源生成一個.json清單文件,而後在服務端讀取這個.json,而後把清單信息提供給模板文件,由此來正確地引用所需的靜態資源

插件webpack-manifest-pluginassets-webpack-plugin均可以幫助完成這一點。

服務端例子 - Spring Boot & Thymeleaf

請看一個Spring Boot(1.5.3.RELEASE) & Thymeleaf(2.1)的例子。這裏選擇webpack-manifest-plugin。

首先,在webpack的配置中加入這個插件:

{
  plugins: [
     new ManifestPlugin()
  ]
}

執行webpack構建,即生成一個資源清單文件manifest.json(位置取決於webpack的output配置,這裏是src/main/resources/static),它的內容是這樣:

{
  "account/login.css": "account/login_style_f549ea0a.css",
  "account/login.js": "account/login_bundle_279af402.js"
}

接下來,建立一個幫助類ResourceFormatter(名稱自擬):

public class ResourceFormatter{

    private JsonNode resourceMap;

    public ResourceFormatter(){
        ObjectMapper mapper = new ObjectMapper();
        Resource resource = new ClassPathResource("static/manifest.json");

        try {
            resourceMap = mapper.readValue(resource.getFile(), JsonNode.class);
        } catch (IOException e) {
            resourceMap = null;
        }
    }

    public String format(String originPath){

        if(resourceMap != null && resourceMap.has(originPath)){
            return "/" + resourceMap.get(originPath).asText();
        }

        return "/" + originPath;
    }
}

這個幫助類在初始化的時候就會讀取manifest.json,而在format()方法裏則會利用清單信息對路徑進行轉換。

而後,把這個幫助類添加到模板引擎Thymeleaf內,包含兩步。

第一步,建立一個Dialect類:

public class ResourceDialect extends AbstractDialect implements IExpressionEnhancingDialect {

    public ResourceDialect() {
        super();
    }

    @Override
    public String getPrefix() {
        return "resource";
    }

    @Override
    public Map<String, Object> getAdditionalExpressionObjects(IProcessingContext processingContext) {
        Map<String, Object> expressions = new HashMap<>();
        expressions.put("resourceFormatter", new ResourceFormatter());
        return expressions;
    }
}

能夠看到ResourceFormatter在這裏被實例化並添加。

第二步,在Spring應用中註冊這個Dialect類:

@Configuration
public class ThymeleafConfig {
    @Bean
    public ResourceDialect resourceDialect() {
        return new ResourceDialect();
    }
}

到此,就能夠在Thymeleaf視圖模板文件中使用了。修改視圖文件以下(只包含修改的部分):

<link rel="stylesheet" th:href="@{${#resourceFormatter.format('account/login.css')}}" th:unless="${@environment.acceptsProfiles('dev')}" />
<!-- ... -->
<script th:src="@{${#resourceFormatter.format('account/login.js')}}"></script>

最後,啓動服務,訪問該頁,能夠看到最終的輸出信息:

<link rel="stylesheet" href="/account/login_style_f549ea0a.css">
<!-- ... -->
<script src="/account/login_bundle_279af402.js"></script>

這就是咱們要的帶hash的文件了。

此外,關於如何在Spring Boot中引入webpack,能夠參考這個spring-boot-angular2-seed

服務端例子 - Koa

看完了一個傳統Java應用的例子,再來看看現代的Node應用。[Koa]Koa是簡潔的Node服務端框架,在它的基礎上引用帶hash的資源文件,也是一樣的思路。

首先,一樣是在webpack配置中加入webpack-manifest-plugin。

運行webpack構建生成manifest.json,內容大概會像這樣:

{
  "page1.css": "page1/style_0b5ab6ef.css",
  "page1.js": "page1/bundle_0f33bdc8.js",
  "page1\\potofu.jpg": "page1/potofu_26766d43.jpg"
}

而後,讀取這個json,爲Koa(經過ctx.state)添加一個資源路徑轉換的幫助方法:

import manifest from './public/manifest.json';

app.use(async(ctx, next) => {
    ctx.state.resourceFormat = (originPath) => {

        if (originPath in manifest) {
            return "/" + manifest[originPath];
        }

        return "/" + originPath;
    };
    await next();
});

最後,在視圖模板(這裏的模板引擎是ejs)內,引用所需的靜態資源:

<link rel="stylesheet" href="<%= resourceFormat('page1.css') %>">
<!-- ... -->
<script src="<%= resourceFormat('page1.js') %>"></script>

到此,Koa的例子就完成了。

結語

帶hash的文件是如今web啓用緩存來提高性能比較建議的形式,若是你也有相似的生產環境優化的須要,很推薦你也試試。

(從新編輯自個人博客,原文地址:http://acgtofe.com/posts/2017...

相關文章
相關標籤/搜索