在webpack的配置項中,可能會見到hash
這樣的字符。javascript
當存在hash
配置的時候,webpack的輸出將能夠獲得形如這樣的文件:css
page1_bundle_54e8c56e.js
這種帶哈希值的文件名,能夠幫助實現靜態資源的長期緩存,在生產環境中很是有用。關於這一點的詳細內容,能夠參考這篇久遠的大公司裏怎樣開發和部署前端代碼。html
下面是一個帶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的filename
能夠指定hash。有兩個值能夠選擇:webpack
[hash]
。hash值是特定於整個構建過程的。git
[chunkhash]
。hash值是特定於每個文件的內容的。github
咱們理想的緩存設計是,在一次版本更新(從新構建)後,只有當一個文件的內容確實發生了變化,它才須要被從新下載,不然應使用緩存。web
所以,以上兩個值中更推薦的是[chunkhash]
。你也能夠閱讀這篇官方的緩存指南瞭解更多細節。spring
url-loader
和file-loader
是同一家,參照file-loader文檔可知,文件名name
可使用標識符[hash]
來啓用hash。此外,你還能夠按照[<hashType>:hash:<digestType>:<length>]
的格式更詳細地定製hash結果。
[hash:8]
中的:8
則和前面output的同樣,指定了hash結果的截取長度。
被引用的css經過extract-text-webpack-plugin
來獲得帶hash的文件。參照extract-text-webpack-plugin文檔,在指定生成文件的文件名filename
時可使用標識符[contenthash]
(能夠看到,和以前的並不相同)。
當靜態資源的文件名變成這樣的帶哈希值的版本後,引用這些靜態資源就須要稍多花一點工夫。
若是沒有任何服務端,只是純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分支,也就是最新版,才支持此標識符)。在這個例子中,只有兩個取值:page1
,page2
。
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-plugin或assets-webpack-plugin均可以幫助完成這一點。
請看一個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。
看完了一個傳統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...)