2

一、使用

关于webpack的使用,在这借鉴官方文档,做一些简单总结。

webpack4以后会有默认的配置体系,简单来说项目根目录下默认会有个webpack.config.js的配置文件,其中入口文件为src/index.js,打包出口文件夹为dist,一般来说我们平时项目也是按这个规则来的。

会有首先看一下一个简单的webpack基本配置结构

{
    entry:'./src/index.js',
    output:{
        path: __dirname+'/dist',
        filename: '[name].js'
    },
    mode:'production',
    module:{},
    plugins:[]
}

1.1 入口

entry,指定入口文件,地址是以项目为根节点的相对路径的字符(以单页面为例,只有一个入口)

1.2 输出

output,指定输出,参数是一个对象,其中path指定输出的文件夹,地址需要使用绝对路径

1.3 模式

mode,一般是两个值'developement'和'production',作用用来标记对应开发和生产环境,同时webpack会根据值不同进行一些默认配置,比如production会压缩代码;同时,在开发或者打包过程中,会在Node的模块process中把值存进变量process.env.NODE_ENV中供使用。

1.4 模块

module,因为webpack是基于Node,本身能运行识别js文件,所以其它模块,如css、图片等通过配置module中的loader来加载这些文件,同时借助loader也可以编译处理ES6、TS等上层语法、模板。

1.5 插件

plugins,如文档所说,插件是webpack的支柱功能,目的在于解决loader无法实现的事。比如常见的UglifyJsPlugin压缩代码、CopyWebpackPlugin复制静态资源、HtmlWebpackPlugin使用html模板等等

二、优化

前面介绍的webpack的简单使用,官方文档很详细就一带而过了。接下来总结下webpack优化相关的东西,优化这部分,我把它分为三个部分:代码风格、构建效率、加载性能

2.1 代码风格

开发单页面时,都会配置一套开发环境和一套打包操作,然后你有一个朋友就是我系列一般会有三个配置文件;

  • webpack.base.js:基础配置
  • webpack.dev.js:开发环境配置
  • webpack.pro.js:打包配置

webpack.base.js公共配置部分,然后生产和打包配置通过merge的方式扩展。

package.json的scripts中配置命令行;

  • "dev":"webpack --config webpack.dev.js",
  • "build":"webpack --config webpack.pro.js"

这样的确也很清晰,也没问题。但是后来我使用React脚手架create-react-app,看了一下配置文件觉得更加合理。

首先它并没有在命令行中使用webpack,这样就省去安装webpack-cli

create-react-app中的package.json的scripts中配置命令行;

  • "start": "node scripts/start.js",
  • "build": "node scripts/build.js"

项目中写好脚本直接使用node执行,配置文件比较多就模拟主要内容

// webpack.config.js
module.exports = function(webpackEnv){
    const isEnvDevelopment = webpackEnv === 'development';
    const isEnvProduction = webpackEnv === 'production';
    return {
        // ...
        mode: webpackEnv,
        // ...
        plugins:[
            new pulicPlugin(),
            isEnvDevelopment && new Plugin1(),
            isEnvProduction && new Plugin2(),
        ].filter(Boolean)
        // ....
    }
}

webpack.config.js导出一个函数,指定环境作为参数,然后通过参数进行定制,返回一个webpack的配置参数,start.jsbuild.js主体内容也大概可以猜想到了

// start.js
const configFactory = require('webpack.config');
const webpack = require('webpack');
const config = configFactory('development');
const complier = webpack(config);
return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      let messages;
      if (err) {
        if (!err.message) {
          return reject(err);
        }
        // ....
      }
    // .....
    })

首先通过传参方式可以省去安装webpack-merge,而且原来使用的方式,只执行到webpack编译,而脚手架还补充了编译后的统计分析。

2.2 构建效率

  • 1、dllPlugin

dll意思是动态链接库,在webpack的主要思想就是将一些第三方库打包分离出来,只要第三方库不需要升级,每次项目启动或者打包时就不需要重复打包这些库了。

具体用法以react项目为例,在config新增一个webpack.dll.config.js配置

const webpack = require('webpack');
const path = require('path');
module.exports =  {
    entry: {
        vendor:[
            'react',
            'react-dom',
            'react-router-dom'
        ]
    },
    output: {
        path: path.resolve(__dirname,'../dist'),
        filename: '[name].js',
        library: '[name]_[hash]',
    },
    plugins: [
        new webpack.DllPlugin({
            context: __dirname,
            name: '[name]_[hash]', //需要与output.library相同
            path: path.resolve(__dirname, '../dist/[name]-manifest.json'),
        })
    ]
}

package.json增加命令行"build:dll": "webpack --config config/webpack.dll.config.js --mode production"

执行命令后会在dist文件夹下生成vendor-manifest.jsonvendor.js两个文件,vendor.js也就是分离出的静态资源,而vendor-manifest.json一个清单文件来描述资源的来源路径。

分离完成后就是通过DllReferencePlugin供项目使用了,在webpack.config.js中添加配置

new webpack.DllReferencePlugin({
  context: __dirname,
  manifest: require('../dist/vendor-manifest.json'), //指定dll描述的位置
}),

这样通过dll分离就完成了,这时候还差一步,我们运行项目发现html并没有引入vendor.js,这里需要手动更新html模板,或者使用插件add-asset-html-webpack-plugin添加。

const AddAsssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
//....
plugins:[
  // ...
  new AddAsssetHtmlWebpackPlugin({
    filepath: path.resolve(__dirname, '../dist/*.js'),
    hash:true, //附加hash到文件名后,防止缓存
    outputPath: 'static/js', //
    publicPath: '/static/js'
  })
  // ....
]

这样完成后的项目就可以正常运行了。

  • 2、HappyPack

happypack提升webpack打包速度,本质上happypack是用通过js的多进程来实现打包加速。

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
// 引入happypack,ThreadPool线程池使用系统核心数
module.exports = {
  // ....
  module: {
    rules: [
      {
        test:/\.js$/,
        use:[
          {
            // loader:'babel-loader',
            // options:{
            //  ....
            // }
            loader:'happypack/loader?id=happyBabel', //在loader中添加触发happypack,将原来的配置转移给happypack
          }
        ]
      }
    ]
  },
  plugins:[
    new HappyPack({
      id:'happyBabel', //id与loader中配置的id参数相同
      threadPool: happyThreadPool,
      verbose: false, //是否输出过程日志
      loaders:[
        {
          loader:'babel-loader', 
          options:{
            ....
          }
        }
      ]
    })
  ]
}

2.3 加载性能

  • 1、Gzip
HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。这一般是指WWW服务器中安装的一个功能,当有人来访问这个服务器中的网站时,服务器中的这个功能就将网页内容压缩后传输到来访的电脑浏览器中显示出来.一般对纯文本内容可压缩到原大小的40%.这样传输就快了,效果就是你点击网址后会很快的显示出来.当然这也会增加服务器的负载. 一般服务器中都安装有这个功能模块的。

以上是引用的一部分介绍,现在主流的浏览器都支持Gzip,简单来说Gzip在原本打包压缩后的基础上再压缩一次,Gzip的主要原理就是将重复的字符片段临时替换来减小文件大小。

如何查看?

浏览器客户端的http请求头中的有一个Accept-Encoding字段,告诉服务器接受的编码格式,它的值中包含gzip就说明浏览器支持解析Gzip的返回;如果服务器开启且返回了Gzip的响应,这时响应头会有个的content-encoding字段,内容为content-encoding:gzip。也就是响应在服务端压缩成Gzip,然后客户端再解压使用。

如何使用?

create-react-app脚手架为例,打包完成后控制台输出以下一些内容。
build.png
文件大小提示是使用Gzip压缩后的大小,我们审查真实文件大小确实是要大不少,而且Gzip文件后缀名是gz。对于静态资源的Gzip,这里需要在webpack中增加compression-webpack-plugin的配置。

const CompressWebpackPlugin = require('compression-webpack-plugin');
// 添加到plugins
new CompressWebpackPlugin({
    test: /\.(js|css)$/, // 压缩js和css
    threshold: 10240, // 达到10k才启用,资源过小没必要,而且解压也是有损耗
    minRatio: 0.6 //压缩最小比例下限
    // 其他参数使用默认
  })

完成配置后,打包后的代码静态资源满足以上条件的会多出一个以后缀为.gz的gzip文件,客户端配置也就完成了。当支持解析Gzip的客户端请求服务端的静态资源就会自动请求.gz的文件。压缩效率,以antd为例,完整的普通压缩包体积会接近2M,但Gzip压缩后体积会缩小为不到600KB的大小,结合Gzip的原理,这种复用性强的UI组件压缩效率肯定会比较高。

静态资源搞定了,服务端也可以做一些优化。服务端使用Gzip主要是对一些接口返回的数据进行压缩,以Koa为例,安装koa-compress

const compress = require('koa-compress')
const Koa = require('koa')
const app = new Koa()
app.use(compress({
  filter: function (content_type) {
      return /text/i.test(content_type)
  },
  threshold: 2048,
  flush: require('zlib').Z_SYNC_FLUSH
}))

使用demo的默认配置,完成后在接口响应值在满足配置条件下就会被压缩成Gzip返回给客户端了。

  • 2、按需加载

单页面应用中的多路由,为防止第一次加载页面加载过多资源,将路由配置成按需加载。

一篇讲述代码分割的文章

react项目为例,可以借助react-loadable,配置路由组件

import Loadable from 'react-loadable';

// 修改前配置,打包以后进入首页就会加载相应资源
import oldViewComponent from './viewComponent';
<Route path='/view' component={oldViewComponent} />

// 修改后,路由跳转/view才会加载
const newViewComponent = Loadable({
    loader: () => import('./viewComponent'),
    loading: Loading //加载完成前显示,一般用一个loading组件
})
<Route path='/view' component={newViewComponent} />

RaKL
209 声望10 粉丝