webpack由浅入深系列(二)

1,955 阅读5分钟

解析es6

在webpack解析es6需要借助babel-loader:

npm i babel-loader @babel/core --D

module:{
    rules:[
        {
             test: /\.js$/,
             loader: 'babel-loader',
             exclude: /node_modules/, //不转译node_modules目录的源代码。
        },
    ]
},

babel-loader是依赖babel的,需要在项目中使用babel的配置文件.babelrc。

在项目根目录配置.babelrc文件。.babelrc是一个json格式的文件,其中主要是对presets和plugins进行配置。plugins是告诉babel要使用那些插件,这些插件可以控制如何转换代码;presets 是某一类 plugin 的集合,包含了某一类插件的所有功能。

{
  "presets": [
    [
      "@babel/preset-env", //转换es6语法
      {
        "useBuiltIns": "usage",//据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill
        "corejs": "2",
        "modules": false, //不会转换模块
        "targets": {
          "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] //配置项目所支持的浏览器的配置
        }
      }
    ],
  ],
  "plugins":[
      "@babel/plugin-transform-runtime"
  ]
}

更详细的解读配置参照 www.cnblogs.com/tugenhua070…

css自动加前缀

Css3的某些属性需要加上前缀来适配不同的浏览器内核,主要有以下四种:

在webpack中,可以用autoprefixer插件,它会根据浏览器内核和版本自动补齐css3前缀。它的使用是基于postcss-loader,具体如下:

npm i postcss-loader autoprefixer --D


//webpack.config.js
module:{
    rules:[
    	{
            test: /\.css$/,
            use:  ['style-loader','css-loader','postcss-loader']
        },
        {
            test: /\.less$/,
            use:  ['style-loader','css-loader','postcss-loader','less-loader']
        },
    ]
}

//.posscssrc.js(位于项目根目录下,项目会自动去读取这些配置类文件)
module.exports = {
  "plugins": {
    "autoprefixer": {}
   }
}

//package.json(browserslist字段说明了要兼容的浏览器版本,根据各浏览器版本来决定要加的前缀)
{
  //...
  "browserslist": [
      "defaults",
      "not ie < 8",
      "last 2 versions",
      "> 1%",
      "iOS 7",
      "last 3 iOS versions"
    ]
}

文件监听和热更新

文件监听是在发现源码发生变化时,自动重新构建出新的输出文件。

开启文件监听有两种方式:

  • 在命令行加上 --watch
  • 在webpack配置文件中设置watch:true

在配置文件中可以根据需要设置监听的参数:

module.export = {
	watch: true,
	//只有watch值为true时,watchOptions才有意义
	watchOptions:{
		ignored: ['src/test/**', 'node_modules/**'], //不监听的文件或者文件夹,支持正则匹配
		aggregateTimeout: 500, //监听到变化发生后会等500ms再去执行
		poll:1000 //每秒询问一次文件变化
	}
}

文件监听的原理是轮询判断文件的最后编辑时间是否发生变化,一开始有个文件的修改时间,先存储起来这个修改时间,下次再有修改就会和上次修改时间比对,发现不一致的时候不会立即告诉监听者,而是把文件修改缓存起来,等待一段时间,等待期间内如果有其他发生变化,会把变化列表一起构建,并生成到bundle文件。

文件监听在重新构建后需要刷新浏览器才能看到效果,而热更新使我们在修改代码后不必刷新整个页面即可看到更改后的效果。比如在一个表单页面中,我们已经填写了很多表单信息,这时修改了代码逻辑,如果没有热更新,那么在刷新浏览器页面后之前填写的表单信息就没有了,需要重新再填写,而热更新则能避免这个问题。在开发环境下,通常使用热更新来提升开发效率。

热更新的配置:

  • 安装webpack-dev-server:npm i webpack-dev-server --D
  • webpack.config.js
module.exports = {
	mode: 'development',
	entry: "./src/index.js",
	...
	devServer: {
    		contentBase: './dist',
        	hot: true//会自动引入webpack.HotModuleReplacementPlugin
    	}
}
  • ./src/index.js
if(module.hot){//如果没有这段代码,当修改文件时,会刷新整个页面。
    module.hot.accept()
}
  • package.json
"scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server"
  },

webpack作用到的任何文件(index.html、bundle.js依赖树中的文件、CopyWebpackPlugin作用到的文件)更新时,都会重新编译,但是只有bundle.js依赖树中的文件的变化会进行热更新到浏览器。若使用mini-css-extract-plugin,则样式的变化不会热更新到浏览器。

文件指纹

文件指纹是一串哈希符,通常是加载文件名后面,用于标识文件,做版本的管理。比如项目上线后,需要修改一些东西,只需要发布修改过的文件,对于没有修改过的文件,用户在访问的时候,依旧可以使用浏览器缓存,加速页面渲染。

webpack中的文件指纹有三种:

  • hash:和整个项目的构建相关,所有文件共用一个hash值,任何文件有修改,项目的hash值就会更改,没有实现缓存的效果。
  • chunkhash:根据不同的入口文件(entry)进行依赖文件解析、构建对应的chunk,生成对应的chunkhash值
  • contenthash: 由文件内容产生的hash值,内容不同产生的contenthash值也不一样。

设置文件指纹的通常是静态资源文件,以下是几种文件的文件指纹设置方法:

js:设置output的filename,使用[chunkhash]

output: {
   path: path.join(__dirname,"/dist"),
   filename: "js/bundle.[chunkhash:6].js"
}

css:只有将css抽离成单独的文件时才需要设置指纹,借助mini-css-extract-plugin将css提取成独立的文件,设置mini-css-extract-plugin的filename。使用[contenthash],如果只修改js就可以不用重新上传css文件。

new MiniCssExtractPlugin({
    filename: 'css/[name].[contenthash:7].css'
})

图片:设置file-loader或url-loader的name,使用[hash],与上面整个项目构建的hash不同,图片等媒体文件的hash是根据文件的内容所生成的,换成[contenthash]也可以。

{
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    loader: 'file-loader', 
    options: {
    	esModule: false,
        name: 'img/[name].[hash:7].[ext]'
    }
}

使用热更新的情况下,不能用chunkhash或contenthash,会报错,只能使用hash。所以在开发环境下一般不设置文件指纹,在生产环境下设置。

代码分割

在项目中,基础库文件变更频率较低,比如vue、react等,而我们的业务代码时常变化,这时候我们应该合理的划分代码,将变更频率低的这些模块移到单独的文件中,这样浏览器可以单独缓存它们,保证每次应用中的代码变更也不用重新下载它们。另一方面,合理地分割代码可以利用浏览器并行加载。又比如多页面通常共用基础库文件,此时非常有必要将库文件单独打包,避免重复库文件增加业务代码主文件的大小,同样也是可以利用浏览器缓存。

还有一些非首屏内容,但是文件尺寸较大的,比如一页面中有多个Tab,非首屏的tab是一个图表统计,这部分引入了echarts,而这个统计并不是用户特别关心的那一类,点击率不高。如果为了这么一个用户不常用功能而导致打包进来一个size较大的库,从而导致首屏加载速度变慢,不太可取。用import()异步动态加载,可以减少了首屏代码体积。

webpack中,代码分割有三种方式:

  • entry配置:通过多入口打包来实现,除多页面应用之外,不常用。
  • 动态加载(按需加载):在代码中自动将使用import()加载的模块分离成独立的包,常用。
  • 抽取公共代码:使用splitChunks配置来抽取公共代码,分离基础库文件、公用模块等,常用。 entry配置见上文的多页面打包,这里讨论一下后两种。

(1)import()实现动态加载

通过import()引用的模块都会成为独立的chunk,我们经常在vue-router的配置中看见这样的代码:

{
    path: "/poster",
    component: () => import('@/views/poster'),
    name: "poster"
 }

打包时,会将poster组件打包成独立的js,当路由切换到/poster时,才会加载这个js。在tab切换或复杂弹窗经常用import()动态加载。

按照上面的写法,打包的后的文件以id命名,如0.bundle.531194.js、1.bundle.ddf6c4.js。要自命名文件名需要写成这样。

//router.js
{
    path: "/poster",
    component: () => import(/* webpackChunkName: 'poster' */'@/views/poster'), //魔法注释
    name: "poster"
}
//webpack.config.js
output: {
   path: path.join(__dirname,"/dist"),
   filename: "js/[name].[chunkhash:6].js" //如果不指定用[name]的话,在生产环境仍会用id命名,在开发环境会用自命名。
}

一般是在开发环境下自命名,在生产环境用id命名。

(2)splitChunks配置

splitChunks是webpack4的内置插件,在没有手动配置的情况是使用默认配置的。

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'async', // "initial" | "all" | "async" (默认)
            minSize: 30000, // 最小尺寸,30K,development 下是10k,越大那么单个文件越大(不管再大也不会把动态加载的模块合并到初始化模块中)
            maxSize: 0, // 文件的最大尺寸,0为不限制
            minChunks: 1, // 默认1,被提取的一个模块至少需要在几个 chunk 中被引用
            maxAsyncRequests: 5, //按需加载代码块时,同时发送的请求最大数量不应该超过的数量,默认是5
            maxInitialRequests: 3, //页面初始化时,同时发送的请求最大数量不应该超过的数量,默认是3
            automaticNameDelimiter: '~', // 打包文件名分隔符
            name: true, // 拆分出来文件的名字,默认为 true,表示自动生成文件名,如果设置为固定的字符串那么所有的 chunk 都会被合并成一个
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/, // 正则规则,如果符合就提取 chunk
                    priority: -10 // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级
                },
                default: {
                    minChunks: 2,
                    priority: -20, // 优先级
                    reuseExistingChunk: true // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
                }
            }
        }
    }
};

chunks的三个选项:"initial" | "all" | "async"的区别。 我们建这样几个文件,模拟vue单页应用。每个页面的页面的代码如下:

  • sayName.js
export default function sayName(){
    console.log('This is Tom. Nice to meet you')
}
  • home.vue
<template>
    <div>
        <p>This is home</p>
    </div>
</template>
<script>
import sayName from "../common/sayName.js"
export default {
    mounted(){
        sayName()
    }
}
</script>
  • poster.vue
<template>
    <div>
        This is poster
    </div>
</template>
<script>
import sayName from "../common/sayName.js"
export default {
    mounted(){
        sayName()
    }
}
</script>
  • app.vue
<template>
    <div>
       <router-view />
    </div>
</template>
<script>
export default {}
</script>
  • index.js
import Vue from "vue"
import App from "./App.vue"
import router from './router.js'
new Vue({
    el'#app',
    router,
    renderh => h(App)
})
  • router/index.js
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
const Routes = [
  {
    path: "/",
    component: () => import(/*webpackChunkName: 'home'*/"../views/home.vue"),
    name: "home"
  },
  {
    path: "/poster",
    component: () => import(/*webpackChunkName: 'poster'*/"../views/poster.vue"),
    name: "poster"
  }
];
export default new Router({
  routes: Routes
});
  • webpack.dev.js
module.exports = {
    mode: 'development',
    entry: "./src/index.js",
    output: {
        path: path.join(__dirname,"/dist"),
        filename: "[name].js"
    },
    module:{
    	rules:[
        	...
        	{
                test: /\.vue$/,
                use: 'vue-loader'
            }
        ]
    },
    plugins:[
    	new VueLoaderPlugin(),
        new HtmlWebpackPlugin({
            template: 'index.html',    
            filename: 'index.html',  
            inject: true
        }),
        new CleanWebpackPlugin()
    ],
    optimization: {
    	//上面的默认配置
    }
}

我们希望的打包结果是:(1)node_modules里的文件不常更新,单独打包成一个包。(2)home和poster各自打包,根据路由按需加载。(3)自己写的公用代码更新频率介于库文件和业务代码之间,单独打成一个包。

首先修改minSize: 0(我们的测试模块都很小,改成0可以不计模块大小的原因); maxInitialRequests: 6(同时发送的请求最大数量不应该超过的数量为3,则打包出来的首屏文件不会超过3,也会影响打包结果)

默认chunks:'async': 同步引入的模块不会被打包出来。所以第(1)部分没有打包出来,包含在main.js中。第(3)部分的内容被抽离到了default ~ home~poster.js中,home.js和poster.js中没有第(3)部分的内容。

修改chunks:'initial', (1)被单独打包了,(3)没有,home.js和poster.js中同时包含了(3)的内容。在initial的模式下,不会抽离通过动态import生成的chunk中的公共代码。

修改chunks:'all' (1)和(3)都被打包出来了。所以日常开发中,通常设置chunks为all。

chunk的命名是在cacheGroups中设置

cacheGroups: {
     vendors: {
     	  name: "libs",
          test: /[\\/]node_modules[\\/]/, 
          priority: -10 
     },
     default: {
          name: "common",
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true 
    }
}