webpack基础——《webpack实战 入门、进阶与调优》读

2020-03-03  本文已影响0人  VaporSpace

这篇是我看《webpack实战 入门、进阶与调优》这本书的一个笔记,也相应扩充了部分内容,可以算是给没读过的人做个引子。这本书比较系统地介绍了webpack的基础,阅读量也不大,让我弄清楚了很多以前模糊的点。

1、安装webpack

安装webpack建议本地安装(不使用全局),因为全局安装的话项目在不同机器下可能出现版本不一(本地安装能保证团队的版本一致),并且使用时可能出现本地和全局webpack版本混乱的情况。所以干脆就本地安装。

安装:npm i webpack webpack-cli --save-dev

webpack为核心库,webpack-cli是命令行工具。

由于是安装于本地,所以可以使用 npx webpack 来使用。(npx是nodejs自带的自动执行本地模块的一个命令,具体可以参考npx 使用教程

2、JS的模块管理

在es6 module未成为标准前,有2个比较多人使用的模块管理方案:AMD和commonJS。这两者都是通过编译后生成runtime,在代码运行过程中动态引入。目前commonJS是nodejs的模块管理标准。

// foo.js export
define({
    method1: function() {},
    method2: function() {},
});
   
// import
require(['foo'], function ( foo ) {
        foo.method1();
});
// export
module.exports = {name: 123}

// import 
var name = require('./export.js').name
// export
export const a = 123;

// import 
import {a} from './export'
/*
 UMD判断模块管理方案的源码
 */
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['b'], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require('b'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.b);
    }
}(this, function (b) {
    //use b in some fashion.
    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));

3、chunk、entry、bundle的基本概念

chunk:打包的模块
entry:打包的入口文件
bundle:每个模块打包好后的文件1

image
图片来自《Webpack实战:入门、进阶与调优》

4、配置资源入口

// webpack.config.js
module.exports = {
    /*
    * context配置entry的路径前缀,可以理解为入口的文件上下文,所以是绝对路径,这样在多个entry的时候写起来比较方便
    */
    context: path.join(__dirname, './src'),
    
    /*
    * 打包入口文件
    * 入口可为多个,entry的值可以是数组、字典,用函数或promise返回这两种数据结构也可以
    */
    entry: './index.js'
}

5、提取公共模块

像loadsh、jquery这些第三方的库,如果都跟业务代码一起打进一个bundle文件就会很大,并且每次代码更新都需要更新整个文件。这时候可将一些公共模块抽出来,就不用跟业务代码混杂在一起了。

module.exports = {
    entry: {
        app: './index.js', // 主入口
        vender: ['react', 'lodash', 'jquery'] // vender是‘提供商’的意思,这里理解为第三方模块
    }
}

6、配置资源出口

资源出口的配置在output对象种配置。

module.exports = {    
    entry: './src/app.js',    
    output: {        
        // bundle文件名
        filename: 'bundle.js',       
        // bundle导出路径
        path: path.join(__dirname, 'assets'), 
        // 资源访问上下文       
        publicPath: '/dist/',    
    },
};

output详解:

// 直接写bundle名
filename: 'bundle.js'

// 相对路径,webpack会自动帮你创建src文件夹
filename: './src/bundle.js'

// 动态指定文件名,具体看下图
filename: '[name].js'
image
图片来自《Webpack实战:入门、进阶与调优》
// 相对路径则会从当前请求的文件路径开始衔接
publicPath: './js'
// 在app目录下的html直接请求的资源index.js => www.example.com/app/js/index.js 

// 以 / 开头,则直接从域名后开始衔接
publicPath: '/js'
// 请求资源index.js => www.example.com/js/index.js

// 绝对路径,一般用CDN的场景
publicPath: 'www.cdn.com/js'
// 请求资源index.js => www.cdn.com/js/index.js

7、webpack模块打包的简单原理(理解地比较粗浅)

通过声明一个installedModules字典来存储每个模块,给每个模块设置一个唯一key,全部传入一个立即执行的匿名函数,有个入口模块,在里面执行所有模块并存储进installedModules,已经执行过的模块会直接拿缓存。

webpack编译打包后的代码,在浏览器中是这么运行的:1、初始化环境和一些数据结构;2、执行入口模块代码;3、执行模块代码,记录export和导出import(递归);4、所有模块代码执行完毕,控制权回到入口模块;

8、loader

loader可以译为装载机,在webpack中一切皆模块,这也是为什么引入css需要在js中import,因为webpack只能识别js,而一个组件或者页面的js+css就是一个模块,所以通过在js中引用css的方式来将其绑定成一个模块。

// app.js
import './style.css';

// style.css
body {    
    text-align: center;    
    padding: 100px;    
    color: #fff;    
    background-color: #09c;
}

loader其实一个函数,它的输入和输出是源码或上一个loader的输出(字符串、source map、AST),所以loader的调用是链式的,像一个流水线一样将模块打包出去。这也意味着loader的声明是需要注意顺序的。
ps:source map是一个json文件,用来解决代码编译前后的映射问题

loader配置:

module.exports = {      
    module: {        
        rules: [{         
            // 正则匹配需要进入loader的文件   
            test: /\.css$/,    
            
            // 用到的loader数组(loader的执行顺序从后到前,所以这里是css-loader先执行)         
            use: [
                'style-loader',  
                
                // loader除了上面'style-loader'这种直接声明字符串
                // 还可以像下面'css-loader'这样声明一些配置项
                {
                    loader: 'css-loader', 
                    options: {
                          // css-loader 配置项
                      }
                }
            ],  
            
            // loader处理文件的排除范围
            exclude: /node_modules/,  // 正则 
            
            // 处理范围,exclude优先于include,意味着如果两个配置有重叠,include是不能覆盖exclude的
            include: /src/,  // 正则 
            
            /*
            * 在Webpack中,我们认为被加载模块是resource,而加载者是issuer。
            * 比如在这个例子里,css文件是加载模块(resource),js文件则是加载者(issuer)
            * 所以下面是配置js文件,则是配置加载者
            * 前面的loader则是配置加载模块
            */
            issuer: {
                test: /\.js$/,
                include: /src/pages/  // 正则 
            },
            
            // loader执行顺序:
            // normal(默认,按排列顺序)、pre(在所有正常loader前)、post(在所有正常lodaer后)
            enforce: 'normal',
        }],    
    },
};

9、写一个最简单的loader

上面说了loader其实就是一个有输入输出的函数,所以最简单的loader其实只要写一个函数就行。

  1. 用 npm init 初始化一个项目;
  2. 创建一个index.js写入以下代码;
// 这个loader可以在js文件头部加上 “这是我加上去的代码” 这句注释
// content 则是loader的输入即源码或上一个loader的输出字符串
module.exports = function(content) {     
    var useStrictPrefix = `
        // 这是我加上去的代码
    `;

    return useStrictPrefix + content;
}
  1. 在另一个项目通过 npm install <绝对路径> 来安装loader;
  2. 在webpack配置文件中写入loader配置;
  3. 执行编译;

10、webpack-dev-server

开启一个热更新的服务,可以修改代码后通过websocket通知浏览器更新。devServer会对代码进行编译打包,但不会生成文件,打包后的代码会放进内存访问,当浏览器对这个服务发起请求,它会先校验请求的url是不是配置文件里devServer的publicPath。

安装:npm install --save-dev webpack-dev-server

配置:

devServer: {
    /*
        devServer.contentBase
        
        决定了 webpackDevServer 启动时服务器资源的根目录,默认是项目的根目录。
        
        在有静态文件需要 serve 的时候必填,contentBase 不会影响 path 和 publicPath,
        它唯一的作用就是指定服务器的根目录来引用静态文件。
        
        可以这么理解 contentBase 与 publicPath 的关系:contentBase 是服务于引用静态文件的路径,
        而 publicPath 是服务于打包出来的文件访问的路径,两者是不互相影响的。
    */
    contentBase: './dist',
    
    /*
        devServer.publicPath
        
        在开启 webpackDevServer 时浏览器中可通过这个路径访问 bundled 文件,
        静态文件会加上这个路径前缀,若是devServer里面的publicPath没有设置,
        则会认为是output里面设置的publicPath的值。
        (如果有使用htmlWebpackPlugin,建议devServer.publicPath不填或者跟output.publicPath一致,
        因为在开启devServer后,htmlWebpackPlugin插入js会使用devServer.publicPath)
        
        和 output.publicPath 非常相似,都是为浏览器制定访问路径的前缀。
        但是不同的是 devServer.publicPath 只影响于 webpackDevServer(一般来说就是 html),
        但各种 loader 打出来的路径还是根据 output.publicPath。
    */
    publicPath: './dist'
}

参考:https://github.com/fi3ework/blog/issues/39

11、代码分片

考虑到缓存和减少请求时间等原因,需要将公共代码分块。不同于之前使用的CommonsChunk-Plugin插件,webpack4有了改进版的代码分片配置optimization.SplitChunks。

不像CommonsChunk-Plugin需要去将特定的模块提取出来,使用SplitChunks只需要配置提取条件,webpack就会将符合条件的模块打包出来。下面是默认配置:

optimization: {        
    splitChunks: {     
        // chunks: async(默认,只提取异步模块) | initial(只提取入口) | all(前两者都提取)
        chunks: 'all',    
        
        // 按cacheGroups的提取规则,并以automaticNameDelimiter为分隔符命名chunks
        // eg: vendors~a~b~c.js意思是该chunk为vendors规则所提取,并且该chunk是由a、b、c三个入口chunk所产生的。
        name: true,
        // chunk命名的分隔符
        automaticNameDelimiter: '~', 
        
        /* 
        * 根据chunk资源本身情况配置规则
        */
        // 提取后的Javascript chunk体积大于30kB(压缩和gzip之前),CSS chunk体积大于50kB
         minSize: {      
             javascript: 30000,      
             style: 50000,    
         },         
         // 在按需加载过程中,并行请求的资源最大值小于等于5
         maxAsyncRequests: 5,  
         // 在首次加载时,并行请求的资源数最大值小于等于3  
         maxInitialRequests: 3,
         // 备注:设置maxAsyncRequests和maxInitialRequests是因为不希望浏览器一次发出过多请求,
         // 所以希望把一次加载的模块限定规定次数;
        
        /* 
        * 根据chunk来源配置提取规则
        */
        cacheGroups: {        
            // 模块来自node_modules目录,vendors只是chunk命名,可灵活调整;
            vendors: {            
                test: /[\\/]node_modules[\\/]/,            
                priority: -10, // 优先级,这里vendors优先        
            },
            // chunk被至少两个模块引用则重用
            default: {            
                minChunks: 2,    
                reuseExistingChunk: true,   
                priority: -20,          
            },
        }
    },    
}

// 正常只需要像下面这样声明即可
optimization: {        
    splitChunks: {     
        chunks: 'all'
    }
}

12、webpack异步模块加载

// 异步地将b.js加载进来
import('./b.js').then((b) => {
    ...
})

这个异步import,webpack是通过动态插入script标签来实现的,因为之前提过,通过script加载进来的属于间接资源请求,这个资源位置需要通过output.publicPath来确定,所以需要配置号output.publicPath;

13、环境区分

在开发过程中,需要区分开发环境和生产环境,开发环境一般完成基本的编译打包工作,让代码能在浏览器运行就好,而生产环境为了更小的包体通常还会进行压缩、tree-shaking等操作。将这两种环境的操作区分开来,一般有两种方案:

// package.json
{  ...  
    "scripts": {    
        "dev": "ENV=development webpack-dev-server",    
        "build": "ENV=production webpack"  
    },
}
        
// webpack.config.js
const ENV = process.env.ENV;
const isProd = ENV === 'production';
module.exports = {  
    output: {    
        filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js',  
    },  
    // mode模式如果为production,webpack会默认添加一些配置,帮助压缩代码
    mode: ENV,
};
{  ...  
    "scripts": {    
        "dev": " webpack-dev-server --config=webpack.development.config.js",    
        "build": " webpack --config=webpack.production.config.js"  
    },
}
// webpack.config.js
plugins: [        
     new webpack.DefinePlugin({            
         ENV: JSON.stringify('production'),        
     })    
 ]
 
 // app.js
 document.write(ENV);

14、tree-shaking

tree-shaking能使webpack在打包过程中,将一些没引用到的多余包体剔除,但有两点需要注意,一是tree-shaking只在es6 moudle下生效(依靠es6 moudle静态引用实现),意味着commonjs的模块管理是不可行的;二是tree-shaking只是做多余包体的标记工作,实际剔除代码还是需要借助压缩插件如terser-web-pack-plugin,但在webpack4只需要将mode设置为production即可。

针对上面第一点,需要注意在babel-loader中设置module=false,禁止bable将模块转为commonjs。

15、模块热替换(hot module replace)

监听文件变化,不同于live reload(刷新页面,全量更新),热替换是增量修改,不刷新网页,只更改局部。

开启HMR:

const webpack = require('webpack');

module.exports = {  // ...  
    plugins: [    
        new webpack.HotModuleReplacementPlugin()  
    ],  
    devServer: {    hot: true,  },
};

HMR原理:

上一篇下一篇

猜你喜欢

热点阅读