gwuhaolin / blog

浩的技术博客
https://wuhaolin.cn
2.22k stars 280 forks source link

webpack原理与实战 #4

Open gwuhaolin opened 7 years ago

gwuhaolin commented 7 years ago

webpack是一个js打包工具,不一个完整的前端构建工具。它的流行得益于模块化和单页应用的流行。webpack提供扩展机制,在庞大的社区支持下各种场景基本它都可找到解决方案。本文的目的是教会你用webpack解决实战中常见的问题。

webpack原理

在深入实战前先要知道webpack的运行原理

webpack核心概念

webpack构建流程

从启动webpack构建到输出结果经历了一系列过程,它们是:

  1. 解析webpack配置参数,合并从shell传入和webpack.config.js文件里配置的参数,生产最后的配置结果。
  2. 注册所有配置的插件,好让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
  3. 从配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去。
  4. 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
  5. 递归完后得到每个文件的最终结果,根据entry配置生成代码块chunk
  6. 输出所有chunk到文件系统。

需要注意的是,在构建生命周期中有一系列插件在合适的时机做了合适的事情,比如UglifyJsPlugin会在loader转换递归完后对结果再使用UglifyJs压缩覆盖之前的结果。

场景和方案

通过各种场景和对应的解决方案让你深入掌握webpack

单页应用

demo redemo 一个单页应用需要配置一个entry指明执行入口,webpack会为entry生成一个包含这个入口所有依赖文件的chunk,但要让它在浏览器里跑起来还需要一个HTML文件来加载chunk生成的js文件,如果提取出了css还需要让HTML文件引入提取出的css。web-webpack-plugin里的WebPlugin可以自动的完成这些工作。

webpack配置文件

const { WebPlugin } = require('web-webpack-plugin');
module.exports = {
  entry: {
    app: './src/doc/index.js',
  },
  plugins: [
    // 一个WebPlugin对应生成一个html文件
    new WebPlugin({
      //输出的html文件名称
      filename: 'index.html',
      //这个html依赖的`entry`
      requires: ['app'],
    }),
  ],
};

requires: ['doc']指明这个HTML依赖哪些entryentry生成的js和css会自动注入到HTML里。 你还可以配置这些资源的注入方式,支持如下属性:

要设置这些属性可以通过在js里配置

new WebPlugin({
    filename: 'index.html',
    requires: {
         app:{
              _dist:true,
              _inline:false,
         }
    },
}),

或者在模版里设置,使用模版的好处是灵活的控制资源注入点。

new WebPlugin({
      filename: 'index.html',
      template: './template.html',
}),
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <link rel="stylesheet" href="app?_inline">
    <script src="ie-polyfill?_ie"></script>
</head>
<body>
<div id="react-body"></div>
<script src="app"></script>
</body>
</html>

WebPlugin插件借鉴了fis3的思想,补足了webpack缺失的以HTML为入口的功能。想了解WebPlugin的更多功能,见文档

一个项目里管理多个单页应用

一般项目里会包含多个单页应用,虽然多个单页应用也可以合并成一个但是这样做会导致用户没访问的部分也加载了。如果项目里有很多个单页应用,为每个单页应用配置一个entryWebPlugin?如果项目又新增了一个单页应用,又去新增webpack配置?这样做太麻烦了,web-webpack-plugin里的AutoWebPlugin可以方便的解决这些问题。

module.exports = {
    plugins: [
        // 所有页面的入口目录
        new AutoWebPlugin('./src/'),
    ]
};

AutoWebPlugin会把./src/目录下所有每个文件夹作为一个单页页面的入口,自动为所有的页面入口配置一个WebPlugin输出对应的html。要新增一个页面就在./src/下新建一个文件夹包含这个单页应用所依赖的代码,AutoWebPlugin自动生成一个名叫文件夹名称的html文件。AutoWebPlugin的更多功能见文档

代码分割优化

一个好的代码分割对浏览器首屏效果提升很大。比如对于最常见的react体系你可以

  1. 先抽出基础库react react-dom redux react-redux到一个单独的文件而不是和其它文件放在一起打包为一个文件,这样做的好处是只要你不升级他们的版本这个文件永远不会被刷新。如果你把这些基础库和业务代码打包在一个文件里每次改动业务代码都会导致文件hash值变化从而导致缓存失效浏览器重复下载这些包含基础库的代码。以上的配置为:
    // vender.js 文件抽离基础库到单独的一个文件里防止跟随业务代码被刷新
    // 所有页面都依赖的第三方库
    // react基础
    import 'react';
    import 'react-dom';
    import 'react-redux';
    // redux基础
    import 'redux';
    import 'redux-thunk';
    // webpack配置
    {
    entry: {
    vendor: './path/to/vendor.js',
    },
    }
  2. 再通过CommonsChunkPlugin可以提取出多个代码块都依赖的代码形成一个单独的chunk。在应用有多个页面的场景下提取出所有页面公共的代码减少单个页面的代码,在不同页面之间切换时所有页面公共的代码之前被加载过而不必重新加载。

构建npm包

demo remd 除了构建可运行的web应用,webpack也可用来构建发布到npm上去的给别人调用的js库。

const nodeExternals = require('webpack-node-externals');
module.exports = {
  entry: {
    index: './src/index.js',
  },
  externals: [nodeExternals()],
  target: 'node',
  output: {
    path: path.resolve(__dirname, '.npm'),
    filename: '[name].js',
    libraryTarget: 'commonjs2',
  },
};

这里有几个区别于web应用不同的地方:

构建服务端渲染

服务端渲染的代码要运行在nodejs环境,和浏览器不同的是,服务端渲染代码需要采用commonjs规范同时不应该包含除js之外的文件比如css。webpack配置如下:

module.exports = {
  target: 'node',
  entry: {
    'server_render': './src/server_render',
  },
  output: {
    filename: './dist/server/[name].js',
    libraryTarget: 'commonjs2',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
      },
      {
        test: /\.(scss|css|pdf)$/,
        loader: 'ignore-loader',
      },
    ]
  },
};

其中几个关键的地方在于:

从fis3迁移到webpack

fis3和webpack有相似的地方也有不同的地方。相似在于他们都采用commonjs规范,不同在于导入css这些非js资源的方式。fis3通过// @require './index.scss'而webpack通过require('./index.scss')。如果想从fis3平滑迁移到webpack可以使用comment-require-loader。比如你想在webpack构建是使用采用了fis3方式的imui模块,配置如下:

loaders:[{
     test: /\.js$/,
     loaders: ['comment-require-loader'],
     include: [path.resolve(__dirname, 'node_modules/imui'),]
}]

自定义webpack扩展

如果你在社区找不到你的应用场景的解决方案,那就需要自己动手了写loader或者plugin了。 在你编写自定义webpack扩展前你需要想明白到底是要做一个loader还是plugin呢?可以这样判断:

如果你的扩展是想对一个个单独的文件进行转换那么就编写loader剩下的都是plugin

其中对文件进行转换可以是像:

编写 webpack loader

demo comment-require-loader 编写loader非常简单,以comment-require-loader为例:

module.exports = function (content) {
    return replace(content);
};

loader的入口需要导出一个函数,这个函数要干的事情就是转换一个文件的内容。 函数接收的参数content是一个文件在转换前的字符串形式内容,需要返回一个新的字符串形式内容作为转换后的结果,所有通过模块化倒入的文件都会经过loader。从这里可以看出loader只能处理一个个单独的文件而不能处理代码块。想编写更复杂的loader可参考官方文档

编写 webpack plugin

demo end-webpack-plugin plugin应用场景广泛,所以稍微复杂点。以end-webpack-plugin为例:

class EndWebpackPlugin {

    constructor(doneCallback, failCallback) {
        this.doneCallback = doneCallback;
        this.failCallback = failCallback;
    }

    apply(compiler) {
        // 监听webpack生命周期里的事件,做相应的处理
        compiler.plugin('done', (stats) => {
            this.doneCallback(stats);
        });
        compiler.plugin('failed', (err) => {
            this.failCallback(err);
        });
    }
}

module.exports = EndWebpackPlugin;

loader的入口需要导出一个class, 在new EndWebpackPlugin()的时候通过构造函数传入这个插件需要的参数,在webpack启动的时候会先实例化plugin再调用pluginapply方法,插件需要在apply函数里监听webpack生命周期里的事件,做相应的处理。 webpack plugin 里有2个核心概念:

CompilerCompilation 都会广播一系列事件。 webpack生命周期里有非常多的事件可以在event-hooksCompilation里查到。以上只是一个最简单的demo,更复杂的可以查看 how to write a plugin或参考web-webpack-plugin

总结

webpack其实很简单,可以用一句话涵盖它的本质:

webpack是一个打包模块化js的工具,可以通过loader转换文件,通过plugin扩展功能。

如果webpack让你感到复杂,一定是各种loader和plugin的原因。 希望本文能让你明白webpack的原理与本质让你可以在实战中灵活应用webpack。

阅读原文

yueswing07 commented 6 years ago

多谢文章 基本原理和使用 关键的 loader,plugin都有介绍, web使用,npm使用,npm使用都有介绍。 webpack构建生命周期这一块不是很了解,其次多个chunk之间有什么联系啊 我看到有些多页面 提取公用 理解应该是在多个chunk中有操作。

gwuhaolin commented 6 years ago

@yueswing07 可以这样理解Chunk

Webpack 有以下几个核心的概念:

Webpack 启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有 Module。 每找到一个 Module 就会根据配置的 Loader 规则去找出对应的转换规则立即对 Module 进行转换后,再解析出当前 Module 依赖的 Module。 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后 Webpack 会把所有的 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时候执行 Plugin 里定义的逻辑。

yueswing07 commented 6 years ago

@gwuhaolin 非常感谢 你的细心回答 webpack 相关文档 资料不是很全 很细。 接触时间不长 了解不多 看到了你还写了 web-html-plugin 感觉您对了解比较深入 有机会多请教。 其次 webpack plugin 执行时机 什么的 还有类似 多个chunk 公用提取 我在多看看 有问题再请教

yueswing07 commented 6 years ago

@gwuhaolin 对于webpack 热加载 热更新 原理也不是很深入的了解 感觉 热更新 很复杂的

DevinXian commented 6 years ago

关于HMR,知乎上有不少分享

ghost commented 5 years ago

真不错

nosiyBUGS commented 4 years ago

大佬 niubility