在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...)