学习笔记(十)——Webpack打包

Webpack打包

webpack是一款目前主流的模块化打包工具,提供了对前端开发过程中涉及的所有资源的模块化打包方案

模块化打包工具由来

  • 解决开发阶段代码在实际生产运行环境中的兼容性问题
image-20200917221904481
  • 将零散的模块文件打包到统一的文件中,避免由于模块文件过多造成频繁的网络请求
image-20200917221921877
  • 实现所有前端资源的模块化,而不仅仅是JS模块化
image-20200917221942279

快速上手(基本使用)

  • 在项目目录中,执行yarn add webpack webpack-cli --dev安装webpack依赖
  • 执行yarn webpack命令,对项目目录中的文件进行打包,打包后的文件默认输出到dist目录

配置文件及基础配置

  • webpack4以上版本支持按照约定零配置直接打包,以src/index.js为入口文件 -> 以dist/main.js为输出文件

  • 使用webpack.config.js配置文件可以对webpack进行自定义打包配置

    • entry:指定入口文件
    • output:指定输出文件
      • filename:输出文件名
      • path:输出文件路径(必须为绝对路径,可以使用path.join(__dirname, 'output')获取绝对路径)
      • publicPath:资源目录,默认值为空字符串
    • mode:指定工作模式
    • module:
      • rules:配置loader加载规则,一个对象数组
        • test:正则表达式匹配使用loader的文件
        • use:指定使用的loader,可以接收loader名字符串(或指定loader文件路径)、配置对象,或者由多个loader名字符串、配置对象组成的数组,数组中配置的多个loader会按照从后往前的顺序依次执行
          • loader:loader名
          • options:配置选项,用于指定loader的配置参数
    • plugins:配置插件,接收一个数组
    // webpack.config.js
    const path = require('path');
    
    module.exports = {
        mode: 'none',
        entry: './src/style.css',
        output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'output')
        },
        module: {
            rules: [
                {
                    test: /.css$/,
                    use: ['style-loader', 'css-loader']
                }
            ]
        },
        plugins: [
    
        ]
    }
    

工作模式

webpakc可以通过命令行参数--mode=指定工作模式,默认不指定时执行production模式,也可以在配置文件中通过mode配置项进行指定

  • production
    • 生产模式,会启用内置的插件对打包的代码进行压缩优化等操作
  • development
    • 开发模式,更注重打包效率,不对代码进行压缩
  • none
    • 纯打包模式,以最原始的状态打包代码

打包结果运行原理

以none模式打包代码,查看打包后的输出文件,并进行分析

  • 入口函数

    • 定义了一个对象来缓存加载的模块 installedModules

      image-20200921203134994
    • 定义了一个函数__webpack_require__来加载模块,返回exports

      image-20200921203154670
    • 在定义的__webpack_require__函数上挂载了一些数据与工具函数

      image-20200921203223450
    • 在最后调用__webpack_require__函数来加载入口模块,并返回exports

      image-20200921203237447
  • 入函数参数传入的是一个由模块函数组成的数组,每个模块被包裹在一个相同结构的函数当中 function(module, __webpack_exports__, __webpack_require__){},用来形成模块私有作用域

    image-20200921203940524
  • 每个模块函数中都会执行__webpack_require__.r(__webpack_exports__),用来为exports对象定义一个__esModule标记

    image-20200921204053586
image-20200921204313513
  • 模块中使用__webpack_require__加载其他依赖的模块,并执行模块中相应的代码

资源模块加载

webpack使用loader实现对模块的加载,默认loader只对js文件进行加载与解析,对于css等其他资源模块的加载,则可以通过配置额外的loader来实现

loader是webpack实现前端模块的核心,通过不同的loader可以实现加载任何类型的资源

image-20200921205035572
  • 在webpack.confg.js中,通过module属性,配置loader加载规则

  • module

    • rules:配置loader加载规则,一个对象数组
      • test:正则表达式匹配使用loader的文件
      • use:指定使用的loader,可以接收loader名字符串,或者由多个loader名字符串组成的数组,数组中配置的多个loader会按照从后往前的顺序依次执行
    module: {
        rules: [
            {
                test: /.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    }
    

导入资源模块

通常情况下,应当使用js文件作为模块打包的入口,其他资源文件通过import的方式引入

webpack建议应当在对应的代码模块中引入当前模块所需要的资源(按需加载),而不是在全局入口引入

image-20200921210706180

常用资源加载器-loader

  • file-loader:文件资源加载器、

    image-20200921220222982
  • url-loader:类似于file-loader,区别是当文件小于指定大小(可通过options下的limit选项配置)时,可以直接返回DataURL,将图片等转换成base64编码直接存放在js文件中,将小文件通过这种方式处理,可以减少页面加载时,浏览器请求资源文件的次数,提高加载效率

    • url-loader依赖于file-loader,无法单独使用

    • 配置样例

       {
           test: /.png$/,
           use: {
               loader: 'url-loader',
                   options: {
                       limit: 10 * 1024 // 10KB
                   }
           }
       }
      
  • css-loader:编译css文件到js中,以style形式挂载到html中

    image-20200921220207106
  • eslint-loader:语法格式校验

    image-20200921220316275
  • babel-loader:ES语法转换

  • html-loader:加载处理html文件

资源加载方式

  • 遵循ES Modules标准的import声明
  • 遵循CommonJS标准的require声明
  • 遵循AMD标准的define函数和require函数
  • css文件中的@import与css属性中使用的url(例如background-image)
  • html中的src、a标签的href等

核心工作原理

  • 以指定js文件为入口,查找资源文件依赖,并生成依赖树
  • 遍历依赖树,将依赖的资源模块交给相应的loader进行处理
  • 将最终处理的结果汇总到输出的bundle.js文件中
image-20200921221837689

开发一个loader

尝试开发一个markdown文件加载器markdown-loader

  • 创建markdown-loader.js文件,使用module.exports导出一个处理函数,接收加载的模块内容作为参数

  • loader返回的是经过处理过后的结果,最后执行的loader,必须返回js脚本

    image-20200921225002907
  • 直接返回js脚本方式

    // markdown-loader.js
    const marked = require('marked');
    
    module.exports = source => {
        const html = marked(source)
    
        return `module.exports = ${JSON.stringify(html)}`
    }
    
  • 使用html-loader来进行后续处理方式

    // markdown-loader.js
    const marked = require('marked');
    
    module.exports = source => {
        const html = marked(source)
    
        // return `module.exports = ${JSON.stringify(html)}`
        return html
    }
    
    {
        test: /.md$/,
            use: [
                'html-loader',
                './markdown-loader.js'
            ]
    }
    

插件及常用插件

loader用来处理资源打包,plugin则用来处理除了打包以外的其他自动化工作,例如:清除指定目录、拷贝资源文件、压缩输出代码等

通过webpack.config.js中配置plugins属性来配置plugin

webpack loader + plugin实现了前端工程化大部分工作

  • clean-webpack-plugin:清除指定目录,默认清理输出目录

    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    module.exports = {
        ...
        plugins: [
            new CleanWebpackPlugin()
        ]
    }
    
  • html-webpack-plugin:自动生成html文件

    • filename:指定生成html文件名,默认index.html

    • title:html的title

    • meta:html的meta

    • template:指定生成html的模板文件,模板文件中的动态参数可以使用<%= htmlWebpackPlugin.options.title %>方式指定

    • 配置多个htmlWebpackPlugin实例,可以生成多个html文件

      const HtmlWebpackPlugin = require('html-webpack-plugin');
      
      module.exports = {
          ...
          plugins: [
              new HtmlWebpackPlugin({
                  title: 'hello world',
                  meta: {
                      viewport: ''
                  },
                  template: 'src/index.html'
              }),
              new HtmlWebpackPlugin({
                  filename: 'about.html'
              })
          ]
      }
      
  • copy-webpack-plugin:拷贝指定文件到输出目录,接收一个路径数组

    const CopyWebpackPlugin = require('copy-webpack-plugin');
    
    module.exports = {
        ...
        plugins: [
            // 开发阶段不建议使用,频繁的拷贝读写磁盘没有意义,同时会降低打包效率
            new CopyWebpackPlugin({
                patterns: [
                    {
                        from: 'public'
                    }
                ]
            }),
        ]
    }
    

开发一个插件

webpack的插件通过hook机制实现

webpack规定一个插件必须是一个函数,或者一个包含apply方法的对象,apply方法接收一个compiler对象参数

  • 尝试实现一个去除js文件注释的插件

    class MyPlugin {
        apply(compiler) {
            compiler.hooks.emit.tap('MyPlugin', compilation => {
                for (const name in compilation.assets) {
                    if(name.endsWith('.js')) {
                        const contents = compilation.assets[name].source()
                        const cleanContents = contents.replace(/\/\*\*+\*\//g, '')
                        compilation.assets[name] = {
                            source: () => cleanContents,
                            size: ()=> cleanContents.length,
                        }
                    }
                }
            })
        }
    }
    
    module.exports = {
        ...
        plugins: [
            new MyPlugin(),
        ]
    }
    

使用webpack提升开发体验

  • 自动编译

    • 使用--watch参数
  • 自动刷新浏览器

    • 可以使用browser-sync工具来实现浏览器自动刷新,缺点是对于webpack来说需要额外的工具来实现,且需要webpack在监听到文件改动后频繁的写入磁盘,更好的方式是使用webpack-dev-server插件
  • Dev Server

    webpack-dev-server提供了一个本地开发服务器,同时集成了自动编译、自动刷新浏览器等功能

    打包编译的结果存放在内存中,由集成的http server直接读取,避免了频繁的磁盘读写,提高了编译效率

    • 通过配置文件中的devServer属性,可以对webpack-dev-server进行相应的参数配置

      • contentBase:指定额外静态资源目录

      • proxy:代理配置

            devServer: {
                contentBase: 'public',
                proxy: {
                    '/api': {
                        // http://localhost:8080/api/users -> https://api.github.com/api/users
                        target: 'https://api.github.com',
                        // http://localhost:8080/api/users -> https://api.github.com/users
                        pathRewrite: {
                            '^/api': '', // 正则匹配替换路径
                        },
                        // 以实际代理请求的主机名访问,localhost对于外部服务器无法识别
                        changeOrigin: true,
                    }
                }
            }
        
  • Source Map

    Source Map用来帮助开发者调试压缩后的js、css文件,便于进行问题定位

    在压缩后的代码中,通过在最后添加指定格式的注释,来开启Souce Map的引用

    例如 //# sourceMappingURL=xxxx.map,xxxx.map指代对应的Source Map文件名

    • 通过配置文件中的devtool属性devtool: 'source-map'来配置在webpack中开启Source Map

    • webpack中提供了多种不同模式的Source Map,不同的模式在打包速度、打包质量上存在一定的差异

      image-20200923231232477
      • eval模式:将模块函数放到eval中执行,不生成source map文件,只能定位到代码来自哪个文件,构建速度较快
      • eval-source-map模式:将模块还数放到eval中执行,生成source map文件,可以定位原始代码位置,构建速度最慢
      • cheap-eval-source-map模式:定位到的代码文件经过loader转换,只能定位到行
      • cheap-module-eval-source-map模式:定位到的代码文件是转换前的源码,只能定位到行,构建速度慢
      • hidden-source-map模式:生成source map,但不在代码中引入,通常适合工具库开发使用
      • nosources-source-map模式:可以定位到代码位置,但是无法查看源码,在生产模式下使用,保护源码
    • 模式组合

      • eval:是否使用eval执行代码模块
      • cheap:source map是否包含行信息
      • module:是否能得到loader处理前的源码
    • 生产模式下,不建议生成source map,容易暴露源码

  • 自动刷新与HMR

    自动刷新功能当监视到代码资源文件发生改动,会自动重新编译打包,并刷新浏览器,此时会导致浏览器上正在编辑的内容以及操作状态会丢失,这样在实际开发过程中的体验并不是很好,更理想的方式是在浏览器不刷新的情况下实现代码资源改动的实时替换,即HMR(Hot Module Replacement)的功能,HMR极大的提升了开发效率

    • webpack-dev-server集成了HMR功能,有两种开启方式
      • 运行参数增加--hot
      • 配置属性devServer下增加hot: true,同时配置启用webpack.HotModuleReplacementPlugin插件
    • 开启webpack的HMR后,css样式就实现了自动热更新,而js、图片资源等需要开发者使用HMR API module.hot.accept() 去手动处理热更新,通常使用一些前端框架或者脚手架工具创建的前端项目,都已经集成了成熟完整的HRM处理方案
    • 如果HMR实现上存在异常,webpack会退而使用自动刷新页面来完成更新,通过使用hotOnly: true 配置,可以只使用HMR而不会使用自动刷新功能
  • 生产环境优化

    生产环境打包与开发环境打包通常存在较大的差异,webpack4为不同的模式提供了预设的配置,也推荐开发者为不同模式提供不同的配置文件,例如将通用配置放入webpack.common.js中,为开发环境提供webpack.dev.js,为生产环境提供webpack.prod.js,在对应模式的配置文件中,使用webpack-merge插件引入并合并配置,并在执行打包命令时使用--config参数指定要使用的配置文件,例如webpack --config webpack.prod.js

    • DefinePlugin:用于为代码注入全局成员,例如process.env.NODE_ENV,这是webpack内置的插件,可以在配置文件plugins中进行配置

    • Tree Shaking:去除未引用代码(dead-code)、去除冗余代码,Tree-Shaking并不是一项单独的配置,是一系列优化功能组合使用的结果,production生产模式下自动开启

      • 在配置文件中使用optimization属性集中配置优化选项
      • usedExports: true 只导出外部使用的成员
      • minimize: true 压缩代码
      • concatenateModules: true 尽可能将模块代码合并到一个函数中输出(默认一个模块对应一个函数),这样既提升了代码执行效率,又减少了代码的体积,这种方式又被称为Scope Hoisting(作用域提升,webpack3中添加的特性)
      • sideEffects: true:一般用于npm包标记副作用,在package.json中配置sideEffects属性来指定哪些代码包含副作用
    • Tree Shaking的实现基于ES Module,当使用了Babel插件或者babel-loader来处理转换es代码时,可能会将ES Module转换为CommonJS模块,此时Tree Shaking将不生效,最新的babel-loader中自动禁用了ES Module的转换,Tree Shaking可以生效

    • Code Splitting - 代码分割

      随着前端工程越来越复杂,打包形成的bundle.js文件会变的越来越庞大,而并不是所有模块都需要在应用启动时使用,如果将所有模块打包到一个文件中,会使得网络加载js时间较久。降低应用加载启动的速度(首页白屏时间)

      • 多入口打包:适合多页应用,每个页面作为一个打包入口,公共部分单独提取

        // 多入口配置样例
        const path = require('path');
        const { CleanWebpackPlugin } = require('clean-webpack-plugin');
        const HtmlWebpackPlugin = require('html-webpack-plugin');
        
        module.exports = {
            mode: 'none',
            entry: { //使用对象而不是数组,每个属性代表一个入口
                index: './src/index.js',
                hello: './src/hello.js',
            },
            output: {
                filename: '[name].bundle.js',
                path: path.join(__dirname, 'dist'),
            },
            module: {
                rules: [
                    {
                        test: /.css$/,
                        use: ['style-loader', 'css-loader']
                    },
                ]
            },
            plugins: [
                new CleanWebpackPlugin(),
                new HtmlWebpackPlugin({
                    filename: 'index.html',
                    template: 'src/index.html',
                    chunks: ['index'],
                }),
                new HtmlWebpackPlugin({
                    filename: 'hello.html',
                    template: 'src/hello.html',
                    chunks: ['hello'],
                }),
            ],
            optimization: {
                splitChunks: {
                    chunks: 'all' //提取公共js、css到一个单独的bundle
                }
            }
        
        }
        
      • 动态导入:按需加载,使用ES Module中的import()函数来实现;动态导入的模块会被自动分包,无需额外配置,适合单页应用对路由组件的按需加载

        • 在import()函数中使用指定注释可以用来指定打包的bundle文件名称,例如import(/*webpackChunkName: utils*/ './utils')
        • MiniCssExtractPlugin:用来实现css文件的按需加载,使用这个插件的时候,css会被提取到一个单独的文件,可以直接通过link标签引入,而不是style标签,因此不需要再使用style-loader,而转为使用MiniCssExtractPlugin.loader;当css文件的大小并不是很大的时候,意义不是很大
        • OptimizeCssAssetsWebpackPlugin:压缩css样式文件,单独的css文件不会默认被webpack压缩,需要借助额外的插件
          • 压缩的插件默认不应该放在plugins中,因为开发阶段并不需要对js与css进行压缩,应当将相应的插件放入optimization属性的minimizer配置中,这样当以production生产模式进行打包时,就会压缩相应的资源,而开发模式不会进行压缩
    • Hash文件名:输出文件名使用占位符实现输出带hash值的文件名,有三种hash模式

      • [hash]:全局hash,每次打包hash值都会发生变化
      • [chunkhash]:相同chunk使用相同的hash值,每次修改,只有对应发生修改的chunk的hash会发生变化
      • [contenthash]:不同的文件会有独立的hash值,每次修改,只有对应文件发生修改的hash会发生变化

      在占位符中使用冒号+数值,可以指定hash位数,使用[contenthash: 8] 8位contenthahs来控制缓存在实际应用中较为合适

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335