您的位置:时时app平台注册网站 > web前端 > 入坑Webpack

入坑Webpack

2019-11-02 08:52

2. js与css共用相同chunkhash的解决方案

webpack的理念是把所有类型的文件都以js为汇聚点,不支持js文件以外的文件为编译入口。所以如果我们要编译style文件,唯一的办法是在js文件中引入style文件。如下:

import 'style/style.scss';

webpack默认将js/style文件统统编译到一个js文件中,可以借助extract-text-webpack-plugin将style文件单独编译输出。从这点可以看出,webpack将style文件视为js的一部分。

这样的模式下有个很严重的问题,当我们希望将css单独编译输出并且打上hash指纹,按照前文所述的使用chunkhash配置输出文件名时,编译的结果是js和css文件的hash指纹完全相同。不论是单独修改了js代码还是style代码,编译输出的js/css文件都会打上全新的相同的hash指纹。这种状况下我们无法有效的进行版本管理和部署上线。

为什么会产生这种问题呢?

chunkFilename

即非入口文件打包后的名称,未被列在entry中,却又需要被打包出来的文件命名配置。一般情况下是不需要这个配置的。比如我们在做异步加载模块时就需要用到了:

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 require 语法告诉 webpack
  // 自动将编译后的代码分割成不同的块,
  // 这些块将通过 Ajax 请求自动下载。
  require(['./my-async-component'], resolve)
})

2.3 chunk-hash

此小节内容只适用于webpack1,webpack2已经修复了hash相关的计算规则。

chunk-hash并不是webpack中另一种hash值,而是compilation执行生命周期中的一个钩子。chunk-hash钩子代表的是哪个阶段呢?请看webpack的Compilation.js源码中以下部分:

for(i = 0; i < chunks.length; i  ) {
        chunk = chunks[i];
        var chunkHash = require("crypto").createHash(hashFunction);
        if(outputOptions.hashSalt)
            hash.update(outputOptions.hashSalt);
        chunk.updateHash(chunkHash);
        if(chunk.entry) {
            this.mainTemplate.updateHashForChunk(chunkHash, chunk);
        } else {
            this.chunkTemplate.updateHashForChunk(chunkHash);
        }
        this.applyPlugins("chunk-hash", chunk, chunkHash);
        chunk.hash = chunkHash.digest(hashDigest);
        hash.update(chunk.hash);
        chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
}

webpack使用NodeJS内置的crypto模块计算chunkhash,具体使用哪种算法与我们讨论的内容无关,我们只需要关注上述代码中this.applyPlugins("chunk-hash", chunk, chunkHash);的执行时机。

chunk-hash是在chunhash计算完毕之后执行的,这就意味着如果我们在chunk-hash钩子中可以用新的chunkhash替换已存在的值。如下伪代码:

compilation.plugin("chunk-hash", function(chunk, chunkHash) {
        var new_hash = md5(chunk);
        chunkHash.digest = function () {
        return new_hash;
    };
});

webpack之所以如果流行的原因之一就是拥有庞大的社区和不计其数的开发者们,实际上,我们遇到的问题已经有先驱者帮我们解决了。插件webpack-md5-hash便是上述伪代码的具体实现,我们需要做的只是将这个插件加入到webpack的配置中:

var WebpackMd5Hash = require('webpack-md5-hash');

module.exports = {
    output: {
        //...
        chunkFilename: "[chunkhash].chunk.js"
    },
    plugins: [
        new WebpackMd5Hash()
    ]
};

webpack-dev-server

它将在localhost:8080启动一个express静态资源web服务器,并且会以监听模式自动运行webpack,在浏览器打开http://localhost:8080/或http://localhost:8080/webpack-dev-server/可以浏览项目中的页面和编译后的资源输出,并且通过一个socket.io服务实时监听它们的变化并自动刷新页面。
在终端中执行

webpack-dev-server --inline --hot

当我们修改了模块的内容后,webpack-dev-server会自动执行打包(打包后的结果会缓存到内存中,所以不能在本地文件中看到打包后的文件)。

inline选项为整个页面添加了"Live Reloading"功能,而hot选项开启了"Hot Module Reloading"功能,这样就会尝试着重载发生变化的组件,而不是整个页面。这样就实现了修改文件,界面就会自动更新了。
我们可以在package.json中输入以下内容:

"scripts": {
   "dev": "webpack-dev-server --colors --hot --inline",
   "build": "webpack --colors --watch"
},

这样我们只需要键入npm run dev命令就能执行上面的命令了。

在这之前,先看看项目的结构以及一个简单的webpack config:

|——hello-webpack
   |——src  # 项目源码
      |——assets # 资源文件
         |——img # 图片
         |——css # 样式
      |——component  # 页面组件
      main.js  # 入口文件
   |——static # 静态资源文件
   index.html
   package.json
   webpack.config.js

webpack config.js

var path = require('path');
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    publicPath: '',
    filename: '[name].js'
  },
  resolve: {
    extensions: ['', '.js', '.jsx', '.json'],
    alias: {
      'src': path.resolve(__dirname, './src'),
      'assets': path.resolve(__dirname, './src/assets'),
      'components': path.resolve(__dirname, './src/components')
    }
  },
  module: {
    loaders: [
      {
        test: /.js|jsx?$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'react']
        }
      },
      {
        test: /.css$/,
        loader: 'style!css',
        exclude: /node_modules/
      },
      {
        test: /.(png|jpe?g|gif|svg)(?.*)?$/,
        loader: 'url',
        query: {
          limit: 10000,
          name: path.join('static', 'img/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
  ]
}

2.1 chunkhash的计算模式

前文提到了webpack的编译理念,webpack将style视为js的一部分,所以在计算chunkhash时,会把所有的js代码和style代码混合在一起计算。比如main.js引用了main.scss:

import 'main.scss';
alert('I am main.js');

main.scss的内容如下:

body{
    color: #000;
}

webpack计算chunkhash时,以main.js文件为编译入口,整个chunk的内容会将main.scss的内容也计算在内:

body{
    color: #000;
}
alert('I am main.js');

所以,不论是修改了js代码还是scss代码,整个chunk的内容都改变了,计算所得的chunkhash自然就不同了。

那么如何解决这种问题呢?

图片 1

1. hash与chunkhash

首先我们先看一下官方文档对于两者的定义:

[hash] is replaced by the hash of the compilation.

hash代表的是compilation的hash值。

[chunkhash] is replaced by the hash of the chunk.

chunkhash代表的是chunk的hash值。

chunkhash很好理解,chunk在Webpack中的含义我们都清楚,简单讲,chunk就是模块chunkhash也就是根据模块内容计算出的hash值。

那么该如何理解hash是compilation的hash值这句话呢?

首先先讲解一下Webpack中compilation的含义。

js与css共用相同chunkhash的解决方案

前文提到了webpack的编译理念,webpack将style视为js的一部分,所以在计算chunkhash时,会把所有的js代码和style代码混合在一起计算。所以,不论是修改了js代码还是css代码,整个chunk的内容都改变了,计算所得的chunkhash自然就一样了。

那么如何解决这种问题呢?

文件的hash指纹通常作为前端静态资源实现增量更新的方案之一,Webpack是目前最流行的开源编译工具之一,其强大的功能也带来很多坑(当然,大部分麻烦其实都可以在官方文档中找到答案)。

hash与chunkhash

按照官方的定义hash就是webpack的每一次编译(compilation)所产生的hash值,chunkhash从字面上理解就是每一个chunk的hash值。那么什么时候会产生编译以及chunk又是什么东西?

1.2 hash应用场景

接上文所述,webpack的hash字段是根据每次编译compilation的内容计算所得,也可以理解为项目总体文件的hash值,而不是针对每个具体文件的。

webpack针对compilation提供了两个hash相关的生命周期钩子:before-hashafter-hash。源码如下:

this.applyPlugins("before-hash");
this.createHash();
this.applyPlugins("after-hash");

hash可以作为版本控制的一环,将其作为编译输出文件夹的名称统一管理,如下:

output: {
    filename: '/dest/[hash]/[name].js'
}

我们不讨论这种方式的合理性和效率,这只是hash的一种应用场景。当然,hash还有其他的应用场景,不过笔者目前未接触过,欢迎大家补充。

起步

在开始学习Webpack之前,请先确保安装了Node.js,建议安装最新版的Node.js。然后就可以使用npm安装Webpack了。你可以将Webpack安装到全局,不过我们通常会把它安装到项目依赖中。

现在进入项目目录,并使用npm init -y初始化一个默认的package.json。打开终端,键入命令:

//全局安装
npm install webpack --g
//安装到项目依赖中
npm install webpack --save-dev

安装好Webpack依赖后,新建一个webpack.config.js文件,用来配置webpack。不过在配置webpack之前,先安装webpack-dev-server:

//全局安装
npm install webpack-dev-server --g
//安装到项目依赖中
npm install webpack-dev-server --save-dev

比如,在Webpack编译输出文件的配置过程中,如果需要为文件加入hash指纹,Webpack提供了两个配置项可供使用:hashchunkhash。那么两者有何区别呢?其各自典型的应用场景又是什么?本文结合笔者工作中遇到的问题,简单记录一下以上问题的解决方案。

contenthash

webpack默认将js/style文件统统编译到一个js文件中,可以借助extract-text-webpack-plugin将style文件单独编译输出。所以我们可以这样配置:

new ExtractTextPlugin('./dist/css/[name].[contenthash].css')

contenthash代表的是文本文件内容的hash值,也就是只有style文件的hash值。这样编译输出的js和css文件将会有其独立的hash值。

3. 结语

静态资源的版本管理是前端工程化中非常重要的一环,使用webpack作为构建工具时需要谨慎使用hashchunkhash,并且还需要注意webpack将一切视为js模块这种理念带来的一些不便。

webpack可以说是目前最流行的构建工具了,但是其官方文档太过笼统,许多细节并未列出,需要研究源码才会了解。好在我们并非独立战斗,庞大的社区资源也是促进webpack流行的重要因素之一。

行文至此,常规的前端项目中关于静态资源hash指纹的问题基本得到了解决,但是前端的环境是复杂的,各种新技术新框架层出不穷。最后留一点悬念给大家:像vue这种将template/js/style统统写在一个js文件中,如何保证在只修改了style时不影响编译输出的js文件hash指纹?

Webpack中的hash与chunkhash

2.2 contenthash

前文提到了使用extract-text-webpack-plugin单独编译输出css文件,造成上一节js/css共用hash指纹的配置为:

new ExtractTextPlugin('[name].[chunkhash].css');

extract-text-webpack-plugin提供了另外一种hash值:contenthash。顾名思义,contenthash代表的是文本文件内容的hash值,也就是只有style文件的hash值。这个hash值就是解决上述问题的银弹。修改配置如下:

new ExtractTextPlugin('[name].[contenthash].css');

编译输出的js和css文件将会有其独立的hash指纹。

到这里是不是就找到完美的解决方案了呢?

远远没有!

结合上文提到的种种,考虑一下这个问题:如果只修改了main.scss文件,未修改main.js文件,那么编译输出的js文件的hash指纹会改变吗?

答案是肯定的。

修改了main.scss编译输出的css文件hash指纹理所当然要更新,但是我们并未修改main.js,可是js文件的hash指纹也更新了。这是因为上文提到的:

webpack计算chunkhash时,以main.js文件为编译入口,整个chunk的内容会将main.scss的内容也计算在内。

那么怎么解决这个问题呢?

很简单,既然我们知道了webpack计算chunkhash的方式,那我们就从这一点出发,尝试修改chunkhash的计算方式。

loader链

多个loader可以链式调用,作用于同一种文件类型。工作链的调用顺序是从右向左,各个loader之间使用"!"分开。
以处理css文件为例,我们需要css-loader来处理css文件,然后使用style-loader将css样式插入到html的style标签中。

1.1 compilation

Webpack官方文档中How to write a plugin章节有对compilation的详解。

A compilation object represents a single build of versioned assets. While running Webpack development middleware, a new compilation will be created each time a file change is detected, thus generating a new set of compiled assets. A compilation surfaces information about the present state of module resources, compiled assets, changed files, and watched dependencies.

compilation对象代表某个版本的资源对应的编译进程。当使用Webpack的development中间件时,每次检测到项目文件有改动就会创建一个compilation,进而能够针对改动生产全新的编译文件。compilation对象包含当前模块资源、待编译文件、有改动的文件和监听依赖的所有信息。

与compilation对应的有个compiler对象,通过对比,可以帮助大家对compilation有更深入的理解。

The compiler object represents the fully configured Webpack environment. This object is built once upon starting Webpack, and is configured with all operational settings including options, loaders, and plugins.

compiler对象代表的是配置完备的Webpack环境。 compiler对象只在Webpack启动时构建一次,由Webpack组合所有的配置项构建生成。

简单的讲,compiler对象代表的是不变的webpack环境,是针对webpack的;而compilation对象针对的是随时可变的项目文件,只要文件有改动,compilation就会被重新创建。

理解了compilation之后,再回头看hash的定义:

[hash] is replaced by the hash of the compilation.

compilation在项目中任何一个文件改动后就会被重新创建,然后webpack计算新的compilation的hash值,这个hash值便是hash

如果使用hash作为编译输出文件的hash指纹的话,如下:

output: {
    filename: '[name].[hash:8].js',
    path: __dirname   '/built'
}

hash是compilation对象计算所得,而不是具体的项目文件计算所得。所以以上配置的编译输出文件,所有的文件名都会使用相同的hash指纹。如下:
图片 2

这样带来的问题是,三个js文件任何一个改动都会影响另外两个文件的最终文件名。上线后,另外两个文件的浏览器缓存也全部失效。这肯定不是我们想要的结果。

那么如何避免这个问题呢?

答案就是chunkhash

根据chunkhash的定义知道,chunkhash是根据具体模块文件的内容计算所得的hash值,所以某个文件的改动只会影响它本身的hash指纹,不会影响其他文件。配置webpack的output如下:

output: {
    filename: '[name].[chunkhash:8].js',
    path: __dirname   '/built'
}

编译输出的文件为:
图片 3

每个文件的hash指纹都不相同,上线后无改动的文件不会失去缓存。

说来说去,好像chunkhash可以完全取代hash,那么hash就毫无用处吗?

publicPath

文件的引用路径,可用来被一些Webpack插件用来处理CSS,HTML文件中的URL,在开发模式下建议使用相对路径,在生产模式中,如果你的资源文件放在别的服务器上,可以使用服务器的地址。当然你也可以不用配置publicPath,。
在项目中我使用了url-loader加载图片,

{
    test: /.(png|jpe?g|gif|svg)(?.*)?$/,
    loader: 'url',
    query: {
      limit: 10000,
      name: path.join('static', 'img/[name].[hash:7].[ext]') # 图片最终的输出路径
    }
}

main.js中使用了图片

import Girl from 'assets/img/girl.jpg'

那么最终浏览器访问的图片路径就是:

static/img/girl.7672e53.jpg 

所以可以根据开发环境和生产环境配置不同的publicPath
在生产环境中,由于我的资源文件放在项目目录下,所以可以这样配置output:

output: {
  path: path.resolve(__dirname, './dist'),
  publicPath: './',
  filename: 'js/[name].[chunkhash].js',
  chunkFilename: `js/[id].[chunkhash].js`
}

那么最终打包后的输出目录结构就是这样的:

|——dist
   |——static
      |——img
         girl.7672e53.jpg
      |——js
         app.js
    index.html

所以通过static/img/girl.7672e53.jpg可以访问到图片。在开发环境下,经过测试,将publicPath设置为'./'界面是无法加载出来的,所以在开发环境下可以不用设置。

filename

入口文件打包后的名称,[name]对应着入口文件的key值,例如:app.js,这对多入口文件是很有用的,应为入口文件可以有多个,但是filename只能有一个,所以对于上面的多入口,最后就是:home.js,'profile.js',当然为了体现文件层级关系可以这么写:

filename: 'js/[name].js'

最后的结果就是:

|——hello-webpack
   |——dist
   |——js
       home.js
       profile.js
    webpack.config.js

compilation

compilation对象代表某个版本的资源对应的编译进程。当使用Webpack的development中间件时,每次检测到项目文件有改动就会创建一个compilation,进而能够针对改动生产全新的编译文件。以及在每次执行webpack命令时都会创建一个compilation。也就是说当创建了一个compilation,我们所有需要打包的文件(js,css,img,font等)都会产生相同的hash。

如果在项目中我们使用hash作为编译输出文件的hash的话,像这样:

entry: {
    home: './src/home.js',
    profile: './src/profile.js'
},
output: {
    path: './dist',
    filename: 'js/[name].[hash].js'
}

那么在编译后所有的文件名都会使用相同的hash值,这样带来的问题是,上面两个js文件任何一个改动都会影响另外文件的最终文件名。上线后,另外文件的浏览器缓存也全部失效。这肯定不是我们想要的结果。

那么如何避免这样的问题呢?

答案就是使用chunkhash
按照上面所说,chunkhash是每一个chunk的hash值,chunk就是模块(webpack中一切皆模块),chunkhash也就是根据模块内容计算出的hash值。所以某个文件的改动只会影响它本身的hash值,不会影响其他文件。
所以可以将上面的filename改为:

 filename: 'js/[name].[chunkhash].js'

这样的话每个文件的hash值都不相同,上线后无改动的文件不会失去缓存。

不过使用chunkhash也不能解决所有问题,比如打包css文件。

output

output是输出配置。

output: {
  path: path.resolve(__dirname, './dist'),
  publicPath: '/',
  filename: '[name].js',
  chunkFilename: '[id].[hash].js'
}

path是文件输出到本地的路径,publicPath是文件的引用路径,可用来被一些Webpack插件用来处理CSS,HTML文件中的URL,一般用于生产模式,filename是打包后的入口文件名,chunkFilename是每个模块编译后的文件名,其中[hash]是用来唯一标识文件,主要用来防止缓存。

resolve

  resolve: {
    extensions: ['', '.js', '.jsx', '.json'],
    alias: {
      'src': path.resolve(__dirname, './src'),
      'assets': path.resolve(__dirname, './src/assets'),
      'components': path.resolve(__dirname, './src/components')
    }
  }

resolve.extensions是对模块后缀名的简写,配置后,原本是require('./components/app.jsx') 可以简写为require('./components/app')

resolve.alias是别名,配置后,比如原本是require('./src/components/nav.jsx')可以简写为require('components/nav.jsx')

logo

path

仅仅用来告诉Webpack在哪里存放结果文件,上面例子中,最终的打包文件会放到与当前脚本文件同级目录的dist目录下。即:

hello-webpack
   dist
  -webpack.config.js

Webpack配置

webpack.config.js为Webpack的默认配置,我们可以为开发环境和生产环境分别做不同的配置。下面一一介绍每个配置的作用。

entry

entry是入口配置项,可以是string,Array或者一个Object:

entry: {
  app: './src/main.js'
},

entry: './src/main.js'

如果页面有多个入口可以这样写:

entry: ['./src/home.js', '.src/profile.js']
//或
entry: {
  home: './src/home.js',
  profile: './src/profile.js'
}

loader

module: {
    loaders: [
      {
        test: /.js|jsx?$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'react']
        }
      },
      {
        test: /.css$/,
        loader: 'style!css',
        exclude: /node_modules/
      },
      {
        test: /.(png|jpe?g|gif|svg)(?.*)?$/,
        loader: 'url',
        query: {
          limit: 10000,
          name: path.join('static', 'img/[name].[hash:7].[ext]')
        }
      }
    ]
  }

由于Webpack本身只能处理JavaScript 模块,如果要处理其他类型的文件,就需要使用loader 进行转换。Loader可以理解为是模块和资源的转换器,它本身是一个函数,接受源文件作为参数,返回转换的结果。不同的loader可以将各种类型的文件转换为浏览器能够接受的格式如JS,Stylesheets等等。

下面一一对这些子参数进行说明:

  • test参数用来指示当前配置项针对哪些资源,当参数匹配时,就会使用相应的loader。
  • exclude参数用来剔除掉需要忽略的资源。
  • include参数用来表示本loader配置仅针对哪些目录/文件,从名称上就可以认为跟exclude作用相反。
  • loader/loaders参数,用来指示用哪个/哪些loader来处理目标资源,这俩表达的其实是一个意思,只是写法不一样,我个人喜欢将loader写成一行,多个loader间使用!分割,这种形式类似于管道的概念,例如loader: 'css?!postcss!less',可以很明显地看出,目标资源先经less-loader处理过后将结果交给postcss-loader作进一步处理,然后最后再交给css-loader。

loader本身也是可以配置的,传入不同的参数可以实现不同的功能。以url-loader为例,我们配置url-loader使小于10000字节的图片使用DataURL,大于10000字节的图片使用URL,name属性配置输出图片的图片名称,例如:

require('a.png') => static/a.3445645.png

不同的loader配置参数不一样,具体配置参数可以去官网查看。

示例代码

在看文章的同时,搭配示例项目会更直观哦,赶紧动起手来,开始入坑Webpack吧:)。

克隆后,请执行 npm install

//启动运行环境
npm run dev 
//执行打包
npm run build

plugin

插件可以完成更多loader不能完成的功能。
插件的使用一般是在webpack的配置信息plugins选项中指定。
loader是在打包前或打包的过程中作用于单个文件。plugin通常在打包过程结束后,作用于包或者chunk级别。

以下是一些常用的插件:

  1. extract-text-webpack-plugin

ExtractTextPlugin的作用是把各个chunk加载的css代码合并成一个css文件并在页面加载的时候以<link>的形式进行加载。

var ExtractTextPlugin = require('extract-text-webpack-plugin')

module: {
  loaders: [
    {
      test: /.css$/, 
      loader:ExtractTextPlugin.extract("style-loader","css-loader") }
  ]
},
plugins: [
  new ExtractTextPlugin(path.join('static', 'css/[name].[contenthash].css'))
]

注意:如果想要把CSS放到HTML的style标签中,可以不使用extract-text-webpack-plugin,只要用css-loader和style-loader就可以了。

  1. html-webpack-plugin

html-webpack-plugin,是用来生产html的,其中filename是生产的文件路径和名称,template是使用的模板,inject是指将js放在body还是head里。为true会将js放到body里

  new HtmlWebpackPlugin({
    filename: 'index.html',
    template: 'index.html',
    inject: true
  }),

这个插件是建议一定要安装的。

  1. uglifyJSPlugin

    uglifyJSPlugin是将代码进行压缩的。

    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
    
  2. CommonsChunkPlugin

CommonsChunkPlugin是将多个入口文件之间共享的块打包成一个独立的js文件。至此,你只需要在每个页面都加载这个公共代码的js文件,就可以既保持代码的完整性,又不会重复下载公共代码了。

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    filename: '[name].[chunkhash].js',
    minChunks: 4
  })

* `name`,给这个包含公共代码的chunk命个名(唯一标识)。
* `filename`,如何命名打包后生产的js文件。
* `minChunks`,公共代码的判断标准:某个js模块被多少个chunk加载了才算是公共代码。
* `chunks`,表示需要在哪些chunk(也可以理解为webpack配置中entry的每一项)里寻找公共代码进行打包。不设置此参数则默认提取范围为所有的chunk。

本文由时时app平台注册网站发布于web前端,转载请注明出处:入坑Webpack

关键词: