谈谈webpack

构建工具有很多,比如Npm Script任务执行者、Grunt也是任务执行者、Gulp基于流的自动化构建工具、Fis3百度构建工具、Webpack打包模块化JavaScript工具和Rollup模块打包工具。

这里谈谈用得很频繁的webpack构建工具。

基本概念

从最基本的概念开始了解:

入口(entry)

entry是配置模块的入口,必填。

module.exports = {
    entry: './path/to/my/entry/file.js'
}

出口(output)

output配置如何输出最终想要的代码。output是一个object,里面包含一系列的配置项。

output.filename配置输出文件的名称,为string类型。

output.path配置输出文件存放在本地的目录(路径),必须是string类型的绝对路径。

path: path.resolve(__dirname, 'dist_[hash]')

output.publicPath配置发布到线上资源的URL前缀,为string类型。默认为空字符串'',即使用相对路径。

比如需要将构建的资源上传到CDN服务上,以便加快网页的打开速度。配置代码如下:

filename: '[name]_[chunkhash:8].js'
publicPath: 'https://cdn.example.com/assets/'

发布到线上时候,HTML中引入的JavaScript文件如下:

<script src='https://cdn.example.com/assets/a_12345678.js'></script>

线上出现404错误的时候,看下路径有没有错~

还有其他配置请看文档

模块(module)

module配置如何处理模块。

module.rules配置模块的读取和解析规则,通常用来配置Loader。其类型是一个数组,数组里每一项都描述了如何去处理部分文件。应用一项rules时大致通过以下方式:

  1. 条件匹配:通过testincludeexclude三个配置项来命中Loader要应用规则的文件。
  2. 应用规则:对选中后的文件通过use配置项来应用Loader,可以只应用一个Loader或者按照从后往前的顺序应用一组Loader,同时还可以给Loader传入参数。
  3. 重置顺序:一组Loader执行顺序默认是从右往左执行,通过enforce选项可以让其中一个Loader的执行顺序放在前面或者最后。
module: {
    rules: [
        {
            // 命中scss文件
            test: /\.scss$/,
            // 处理顺序从右往左
            use: ['style-loader', 'css-loader', 'sass-loader'],
            // 排除node_modules目录下的文件
            exclude: path.resolve(__dirname, 'node_modules'),
        }
    ]
}

Loader需要传入多个参数的时候的例子:

use: [
    {
        loader:'babel-loader',
        options:{
            cacheDirectory:true,
        },
        // enforce:'post' 的含义是把该 Loader 的执行顺序放到最后
        // enforce 的值还可以是 pre,代表把 Loader 的执行顺序放到最前面
        enforce:'post'
    },
    // 省略其它 Loader
]

module.noParse配置项可以让webpack忽略对部分没采用模块化的文件的递归解析和处理,这样做有助于提高构建性能。比如:

module: {
    noParse: (content) => /jquery|lodash/.test(content)
}

module.rules.parser属性可以更细粒度的配置哪些模块需要解析,哪些不需要,和noParse配置项的区别在于parser可以精确到语法层面,而noParse只能控制哪些文件不被解析。

module: {
    rules: [
        {
            test: /\.js$/,
            use: ['babel-loader'],
            parser: {
                amd: false, // 禁用 AMD
                commonjs: false, // 禁用 CommonJS
                ...
            }
        }
    ]
}

解析(resolve)

Resolve配置webpack如何寻找模块所对应的文件。Webpack内置Javascript模块化语法解析功能,默认会采用模块化标准里面约定好的规则去寻找,你也可以按照需求修改默认规则。

resolve.alias配置项通过别名来把原导入的路径映射成一个新的导入路径。如下:

resolve: {
    alias: {
        components: './src/components/'
    }
}

当你通过import Button from 'components/button'导入时,实际上被alias等价替换了import Button from './src/components/button'

resolve.modules配置webpack去哪些目录下找第三方模块,默认只会去node_modules目录下寻找。

resolve.enforceExtension如果配置为true所有导入语句都必须带有后缀,例如开启前import './foo能正常工作,开启后就必须写成import './foo.js'

插件(plugin)

Plugin用于扩展Webpack功能,各种各样的Plugin几乎让Webpack可以做任何构建相关的事情。

举个例子:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

module.exports = {
    plugins: [
        // 所有页面都会用到的公共代码提取到 common 代码块中
        new CommonsChunkPlugin({
            name: 'common',
            chunks: ['a', 'b']
        })
    ]
}

DevServer配置

在开发环境的时候使用。要配置DevServer,除了在配置文件里面通过devServer传入参数外,还可以通过命令行参数传入。

注意:只有在通过DevServer去启动Webpack时配置项文件里devServer才会生效。

devServer.hot配置是否启用使用DevServer中提到的模块热替换功能。

devServer.host配置项用于配置 DevServer 服务监听的地址。

devServer.port配置项用于配置 DevServer 服务监听的端口,默认使用8080端口。

devServer.https配置HTTPS协议服务。某些情况下你必须使用HTTPS,HTTP2 和 Service Worker 就必须运行在 HTTPS 之上。

devServer: {
    https: true
}

webpack原理

Webpack的运行是一个串行的过程,从启动到结束会执行以下流程:

  1. 初始化参数:从配置文件和Shell语句中读取与合并参数,得到最终的参数
  2. 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译。
  3. 确定入口:根据entry找出所有文件
  4. 编译模块:从入口文件出发,调用所有配置的Loader对模块进行编译,再找到模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成编译:在第四步骤后,得到了每个模块被编译的内容和它们直接的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出内容后,这一步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入系统。

webpack优化

缩小文件搜索范围

  • 优化loader配置

Loader对文件的转换操纵很耗时,需要让尽可能少的文件被Loader处理。

在使用Loader时可以通过testincludeexclude三个配置项来命中Loader要应用规则的文件。

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: ['babel-loader?cacheDirectory'],
                include: path.resolve(__dirname, 'src')
            }
        ]
    }
}
  • 优化resolve.modules配置

resolve.modules用于配置webpack去哪些目录下寻找第三方模块。

resolve.modules的默认值是['node_modules'],含义是先去当前的目录下./node_modules目录下去找想找的模块,以此类推,如果没有找到就去上一级目录../node_modules中找,再没有去上上一级,以此类推...

如果知道安装的模块在项目的根目录下的./node_modules时候,没有必要按照默认的方式一层层找:

module.exports = {
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')]
    }
}
  • 优化resolve.alias配置

resolve.alias配置项通过别名来把原导入路径映射成一个新的导入路径。可以减少耗时的递归解析操作。

  • 优化module.noParse配置

module.noParse配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。

const path = require('path');
module.exports = {
    module: {
        // 独完整的 `react.min.js` 文件就没有采用模块化,忽略对 `react.min.js` 文件的递归解析处理
        noParse: [/react\.min\.js$/],
    }
}

把任务分解为多个子进程去并发执行

HappyPack把任务分解成多个子进程并发执行,子进程处理完后再把结果发送给主进程。减少了总的构建时间。

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'happypack/loader?id=happyBabel',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HappyPack({
            id: 'happyBabel',
            loaders: [{
                loader: 'babel-loader?cacheDirectory= true',
            }],
            // 共享进程池
            threadPool: happyThreadPool,
            // 允许happypack输出日志
            verbose: true,
        })
    ]
}

使用自动刷新

例如:

module.export = {
    watch: true,
    watchOptions: {
        // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
        // 默认为 300ms
        aggregateTimeout: 300,
        // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
        // 默认每隔1000毫秒询问一次
        poll: 1000
    }
}

由于保存文件的路径和最后编辑时间需要占用内存,定时检查周期检查需要占用CPU以及文件I/O,所以最好减少需要监听的文件数量和降低检查频率。

热替换

热替换就是当一个源码发生改变的时,只重新编译发生改变的模块,再用新输出的模块替换掉浏览器中对应的老模块。

开启热替换:

webpack-dev-server --hot

区分环境

区分开发环境和生产环境,进行不同的构建~

CDN加速

CDN又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。

CDN 其实是通过优化物理链路层传输过程中的网速有限、丢包等问题来提升网速的。

结合publicPath来处理:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const { WebPlugin } = require('web-webpack-plugin');

module.exports = {
    output: {
        filename: '[name]_[chunkhash:8].js',
        path: path.resolve(__dirname, './dist'),
        publicPath: '//js.cdn.com/id/'
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                use: ['css-loader?minimize'],
                publicPath: '//img.cdn.com/id/'
            }),
            }
        ]
    },
    plugins: [
        new WebPlugin({
          template: './template.html',
          filename: 'index.html',
          // 指定存放 CSS 文件的 CDN 目录 URL
          stylePublicPath: '//css.cdn.com/id/',
        }),
        new ExtractTextPlugin({
          // 给输出的 CSS 文件名称加上 Hash 值
          filename: `[name]_[contenthash:8].css`,
        }),
    ]
}

tree shaking优化

将多余的代码移除。

webpack --display-used-exports --optimize-minimize

提取公共代码

公共代码的提取。

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({
    // 从哪些 Chunk 中提取
    chunks: ['a', 'b'],
    // 提取出的公共部分形成一个新的 Chunk,这个新 Chunk 的名称
    name: 'common'
})

按需加载

对于采用单页应用作为前端架构的网站来说,会面临一个网页需要加载的代码量很大的问题,因为许多功能都做到了一个HTML里面,这会导致网页加载缓慢、交互卡顿、用户体验将非常糟糕。

导致这个问题的根本原因在于一次性的加载所有功能对应的代码,但其实用户每一阶段只可能使用其中一部分功能。 所以解决以上问题的方法就是用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载。

Webpack 内置了强大的分割代码的功能去实现按需加载。比如:

  • 网页首次加载时只加载main.js文件,网页会展示一个按钮main.js文件中只包含监听按钮事件和加载按需加载的代码。
  • 当按钮被点击时才去加载被分割出去的show.js文件,加载成功后再执行show.js里的函数。

main.js中:

window.document.getElementById('btn').addEventListener('click', function () {
    // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
    import(/* webpackChunkName: "show" */ './show').then((show) => {
        show('Webpack');
    })
});

show.js中:

module.exports = function (content) {
    window.alert('Hello ' + content);
};

代码中最关键的一句是import(/* webpackChunkName: "show" */ './show'),Webpack 内置了对import(*)语句的支持,当 Webpack 遇到了类似的语句时会这样处理:

  • ./show.js为入口新生成一个Chunk
  • 当代码执行到import所在语句时才会去加载由Chunk对应生成的文件。
  • import返回一个Promise,当文件加载成功时可以在Promisethen方法中获取到show.js导出的内容。

在工作中具体使用到的时候再按需要进行更改配置项啦~

参考

文章首发在github上--谈谈webpack。更多的内容,请戳我的博客进行了解,能留个star就更好了💨