美文网首页
webpack基础——《webpack实战 入门、进阶与调优》读

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

作者: VaporSpace | 来源:发表于2020-03-03 22:20 被阅读0次

    这篇是我看《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的模块管理标准。

    • AMD(Asynchronous Module Definition 异步模块定义):通过声明回调函数异步加载模块,将其他模块以依赖注入的方式加入进当前模块,由于是异步加载所以不会阻塞当前模块加载。(参考:RequireJS和AMD规范
    // foo.js export
    define({
        method1: function() {},
        method2: function() {},
    });
       
    // import
    require(['foo'], function ( foo ) {
            foo.method1();
    });
    
    • commonJS:通过一个模块对象引出、引入其他模块,引入的值为拷贝值,相较AMD语法更简单方便;
    // export
    module.exports = {name: 123}
    
    // import 
    var name = require('./export.js').name
    
    • es6 module:通过语法静态引出、引入其他模块,是在编译期间进行,引入的值只读的变量映射(因此可以解决循环引用的问题,通过包裹一个函数的方式,参考:JavaScript 模块的循环加载),所以原始值改变会影响到引用者。因为是静态引用,所以不像前两者可以将import语句写在任意地方,如if判断内,需要在编译过程就确定是否引入。
    // export
    export const a = 123;
    
    // import 
    import {a} from './export'
    
    • 三个模块引入的方式,都在第一次引入时将该目标模块代码执行一遍。
      PS:因为前两者不是官方标准,所以都需要借助webpack等模块打包工具进行编译打包后才可以在浏览器上运行,而es6 module可以直接在浏览器上运行,只需要把script标签的type设置为module。但如果你在本地想试验一下es6 module则会发现,本地引入模块时用的是file协议,因为在浏览器引入js资源时需要域名、协议、端口一致,所以在file协议下没有域名会触发跨域限制(chrome、firefox会,ie不会),导致引入失败。
    • UMD(Universal Module Definition 通用模块标准):这不是一个模块管理方案,是一个统一所有模块管理方案的解决。webpack中则应用了这个方案。由于会通过全局是否有define函数来判断AMD环境,但在AMD的规则下是无法使用commonJS和es6 module的,所以如果项目全使用commonJS却因为某些原因出现了define函数,则可能导致全部模块失效,需要手动去修改webpack UMD模块的判断顺序。
    /*
     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详解:

    • filename的书写形式:
    // 直接写bundle名
    filename: 'bundle.js'
    
    // 相对路径,webpack会自动帮你创建src文件夹
    filename: './src/bundle.js'
    
    // 动态指定文件名,具体看下图
    filename: '[name].js'
    
    image
    图片来自《Webpack实战:入门、进阶与调优》
    
    • path:打包资源输出路径,默认为项目根目录的dist文件夹,需要配置绝对路径;
    • publicPath:资源的请求位置,html直接请求的资源如sript标签里的js,和js或css的间接请求的资源资源如引入模块和css加载背景/图,这些都属于资源请求,他们的路径都会在前面加上publicPath;
    // 相对路径则会从当前请求的文件路径开始衔接
    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
    
    • output.path建议跟devServer里面的publicPath保持一致,这样在开发和生产环境才不会搞混,具体原因可以看2、webpack-dev-server的解释;

    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,
    };
    
    • 为两种环境分别写一个配置文件,公用的部分可以通过webpack-merge来合并
    {  ...  
        "scripts": {    
            "dev": " webpack-dev-server --config=webpack.development.config.js",    
            "build": " webpack --config=webpack.production.config.js"  
        },
    }
    
    • 通过webpack中可以通过DefinePlugin插件将环境信息注入到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原理:

    • 首先浏览器端会有HMR runtime,webpack会起一个webpack-dev-server(WDS),两者依靠websocket通信;
    • 当WDS监听到文件变化,会向客户端推送更新事件,并带上构建的hash。客户端根据这个hash和之前资源的对比,判断是否需要更新;
    • 当需要更新,客户端就会向WDS请求更改的资源列表。WDS会返回需要构建的chunk name和资源版本hash。客户端再根据这些信息向WDS请求增量更新的资源;
    • 拿到更新的资源,HMR runtime就会开始决定哪些地方需要替换。webpack会暴露一个module.hot接口,用于给使用者知道热替换的时机,module.accept则是设置需要替换的模块。一般loader都会设置相应的模块热替换的补丁操作,对替换模块进行操作。如果runtime对某个模块没有检测到对HMR的update handler,则会将替换操作冒泡到父级模块,以此类推。(参考:webpack中文文档)

    相关文章

      网友评论

          本文标题:webpack基础——《webpack实战 入门、进阶与调优》读

          本文链接:https://www.haomeiwen.com/subject/cftnkhtx.html