美文网首页JSVue
模块打包器-Webpack

模块打包器-Webpack

作者: 洲行 | 来源:发表于2020-10-13 20:20 被阅读0次

    引入模块化后,解决了大体量项目的开发问题,但是又带来了一些新问题。比如:
    ES Module还存在兼容性问题
    模块文件过多,网络请求频繁
    所有前端资源都需要模块化,不仅仅是JS,还需要css,html图片等静态资源

    所以需要工具满足我们以下设想:


    es6=>es5
    能将散落的模块化文件再次打包到一起,因为生产阶段不需要分散的模块
    还能支持多种类的前端资源类型

    所以出现了前端模块化打包工具

    webpack

    常见工具:webpack,RollUp,Parcel
    webpack,模块打包器,在打包过程中,可以通过模块加载器(Loader)对新特性进行编译转换,它还具备代码拆分(code splitting)的能力,将模块按我们的需要去分组打包,用渐进式加载解决文件太碎或文件太大,这两个极端的问题,还支持资源模块(Asset Module),支持载入任意资源类型的文件,比如import一个css文件。
    打包工具解决的是前端整体的模块化,并不单指JS的模块化

    打包结果运行原理:
    webpack打包过后的代码并不会特别复杂,只是把所有模块翻到了同一文件当中,还提供了基础代码,让模块与模块间相互依赖的关系还可以保持原有状态。

    几个关键概念

    // webpack.config.js
    const path = require('path')
    
    module.exports = {
      mode: 'none',
      entry: './src/main.js',
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
      },
      module: {
        rules: [
          {
            test: /.css$/,
            use: [
              'style-loader', // 把样式转化成style标签插进html
              'css-loader' // 把css转换成js模块
            ]
          }
        ]
      }
    }
    
    • webpack.config.js 是配置webpack的文件,运行在node上,所以我们需要遵循commonJS
    • entry: 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会递归找出有哪些模块和库是入口起点(直接和间接)依赖的
    • output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件
    • mode:通过选择 development 或 production 或 none;none不会开启任何默认插件。
    • loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
      注意,loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能。
      以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:
      “嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先使用 raw-loader 转换一下。”

    webpack只是打包工具,loader用来转换编译代码。

    loader

    常见的资源加载器,有file-loader,url-loader(Data Urls形式)
    最佳实践为:小文件用Data Urls,减小请求次数,大文件还是file-loader,提高加载速度。

    {
            test: /.png$/,
            use: {
              loader: 'url-loader',
              options: {
                limit: 10 * 1024 // 10 KB  大于10自动调用 file-loader
              }
            }
          }
    

    常用Loader分类:
    编译转换类型:css-loader,babel-loader
    文件操作类型:file-loader,
    代码检查类:eslint-loader

    babel-loader

    es6=>es5的编译要安装三个东西
    babel-loader @babel/core @babel/present-env


    image.png

    babel-loader是一个开关,只是触发@babel/core去工作,它是转换的核心,但是他的转换需要插件来指定,比如babel/classes是针对es6中class新语法的,@babel/present-env就是插件的集合,把所有新特性都支持了

    webpack加载资源的方式

    webpack加载资源的方式

    几乎所有代码中所要引用的资源,都会被webpack找出来,根据我们的配置交给不同的loadel去处理,最后将处理结果整体打包到输出目录。

    webpack核心工作原理

    官网首页

    官网首页其实就很好的解释了工作原理。
    从入口文件开始,webpack递归的找到所有依赖,形成了一个依赖树,根据依赖树和配置文件中的rules属性,找到其对应的加载器(loader),交给对应的加载器去处理,最后将处理结果整体打包到输出文件中。其中Loader机制是webpack的核心。

    开发一个Loader

    我们来实现一个markDown-loader

    // m-loader.js
    // 首先loader要运行在node上要遵循commonJS
    // Loader就是一个函数,函数的参数source就是拿到的源代码
    // Loader工作的结果必须是js代码
    module.exports = source => {
      console.log(source)
      // return "hello ~" // 返回的不是js 不行
      return 'console.log("hello ~")' // 可以
    }
    

    或者可以交给其他loader处理


    image.png
    // m-loader.js
    const marked = require('marked')
    module.exports = source => {
      const html = marked(source)
      // 返回 html 字符串交给下一个 loader 处理
      return html
    }
    // webpack.config.js
    {
            test: /.md$/,
            use: [
              'html-loader',   // 交给你
              './m-loader'
            ]
          }
    

    plugin插件

    loader专注资源的加载,plugin解决其他自动化工作。
    比如:压缩代码,清除dist目录,拷贝静态文件到输出目录等
    webpack+plugin 实现了大多数前端工程化的工作,所以让大家以为webpack就是前端工程化。

    常用插件

    plugins: [
        new webpack.ProgressPlugin(),
        new CleanWebpackPlugin(), // 清除dist
     // HtmlWebpackPlugin 自动生成使用打包结果bundle.js的html文件
        // 用于生成index.html
        new HtmlWebpackPlugin({
            template: './src/index.html' // 以谁为模版
        })
        // 多页面应用,再生成个其他的html
        new HtmlWebpackPlugin({
          filename: 'about.html'
        })
        // 静态文件拷贝(其实上线前才会用,开发阶段不要用这个,开销大)
        new CopyWebpackPlugin(['public']),
      ]
    

    想找插件就去 github上搜索 关键字[ plugin webpack image mini ]

    自己实现一个插件

    相比loader,plugin有着更宽的一个能力范围,plugin通过钩子机制实现

    class MyPlugin {
    // 必须有apply方法
      apply (compiler) {
        console.log('MyPlugin 启动')
    // 自己上官网找钩子时机
        compiler.hooks.emit.tap('MyPlugin', compilation => {
          // compilation => 可以理解为此次打包的上下文
          for (const name in compilation.assets) {
             // ......
          }
        })
      }
    }
    

    webpack的开发体验

    如果只是完成上线任务,那么上面的功能足够用了,但是实际中,你本地的开发环节时间是远大于你上线的那几分钟的,好的开发体验才能事半功倍,我希望我的开发环境能:

    • 项目可以以http serve来运行,才能使用ajax,如果以文件形式运行那是不支持的。
    • 边开发,边自动编译,边自动刷新
    • 提供SourceMap支持,便能在浏览器调试,定位问题源码

    那么对于上述功能,webpack都以实现

    自动编译

    watch工作模式
    webpack --watch 用于观察依赖文件的变化,一旦有变化,则可以重新执行构建流程

    自动刷新浏览器

    npm install browser-sync
    browser-sync dist --files"*/"
    browser-sync会监听dist文件下的变化,有变化就刷新。
    这样就实现了自动编译+自动刷新,但是效率还是低,因为要用到两个工具,还需要磁盘的写入后读取,还有更好的解决办法。

    Webpack Dev Server

    Webpack官方提供的工具,它提供了http server,集成了自动编译自动刷新浏览器功能等对开发友好的功能

    npm install webpack-dev-server --dev
    webpack-dev-server // 启动啦
    

    webpack-dev-server为了提高效率,并没有使用磁盘,它将打包结果暂时存放在内存中,http server也是从内存中把这些文件读出来发给浏览器。

    静态资源访问
    dev-server默认会将构建结果输出的文件,全部作为开发服务器的资源文件,也就是说只要能够作为webpack打包输出的文件,都可以被正常访问到,如果有些静态资源也需要被访问的话(假如图片没有经过webpack构建),就需要额外告诉webpack

    // webpack.config.js
    // contentBase额外为httpserver指定查找资源目录
    module.exports = {
        devServer: {
          contentBase: ['./public'] // 指定了public文件为额外的资源路径
        },
    }
    

    代理API
    开发阶段的接口跨域问题

    解决问题最好的方法
    通过开发服务器代理,因为服务器与服务器间的请求是不跨域的。
    dev server支持配置代理
    我们实现代理github的一个接口
      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:8080 作为请求 GitHub 的主机名
            changeOrigin: true
          }
        }
      },
    

    Source Map

    在开发阶段,调试和报错我们都希望基于开发阶段的代码而不是编译过后的。Source Map(源代码地图)就是最好的一种方法,它是一种映射源代码与编译后代码的关系地图,转换后的代码通过source map逆向解析,就可得到源代码。
    source map固定格式
    在编译后文件最后一行添加 //# sourceMappingURL=xx.js.map

    webpack配置source map

    module.exports = {
         devtool: 'source-map',
    }
    

    webpack支持12种sourcemap风格,每种方式的效果和速度各不同。官网有表格对比差异
    选择合适自己的sourcemap,一般来说
    开发环境下:eval-cheap-module-source-map (loader转换前,能定位到行)
    生产环境下:nosources-source-map

    webpack自动刷新问题

    自动刷新带来了一个问题就是,会把输入框中你输入的文字刷新掉,还是麻烦一点,最好保留着。
    问题核心:自动刷新导致页面状态丢失。最好的解决办法是,在页面不刷新的前提下,模块也可以及时更新。

    HMR(模块热更新)

    热更新:应用程序运行的过程中,实时的替换掉应用中的某个模块,应用运行状态并不受影响。
    HMR是webpack最强大的特性之一,极大提高了开发效率。
    开启,

    // HMR已经集成在了dev-server中
    const webpack = require('webpack')
    module.exports = {
      devServer: {
        hot: true
        // hotOnly: true // 只使用 HMR,不会热替换失败又刷新浏览器
      },
      plugins: [
        new webpack.HotModuleReplacementPlugin()
      ]
    }
    

    我们需要手动处理JS模块更新后的事情,CSS可以不用我们手动处理是因为css有规律可言,js没有,那react可以是因为react也是有规律可言的。

    module.hot.accept('./editor', () => {// 处理逻辑})
    

    webpack不同环境下的配置

    开发环境注重开发效率,生产环境注重代码运行效率
    如何解决呢:
    配置文件根据环境不同导出不同配置 或 一个环境对应一个配置文件

    // 配置文件根据环境不同导出不同配置
    // webpack.config.js
    module.exports = function(webpackEnv) {
        const isEnvDevelopment = webpackEnv === 'development';
        const isEnvProduction = webpackEnv === 'production';
        const config = {  ...   }
        return config
    }
    

    // 一个环境对应一个配置文件


    image.png

    DefinePlugin

    mode = production模式下面,内部默认开启了很多功能。
    比如DefinePlugin,为我们的代码注入全局成员,例如
    process.env.NODE_ENV这样一个常量,很多第三方模块都会判断这么一个常量从而做不同操作

    plugins: [
        new webpack.DefinePlugin({
          // 值要求的是一个代码片段
          API_BASE_URL: JSON.stringify('https://api.example.com')
        })
      ]
    

    Tree Shaking

    摇树,用来去除未引用代码,在生产模式下自动开启,我们来尝试在mode=none下开启Tree Shaking

    module.exports = {
    // optimization是集中去配置webpack内部优化功能的
      optimization: {
        // 模块只导出被使用的成员
        usedExports: true, 
        // 压缩输出结果
        minimize: true
        // 尽可能合并每一个模块到一个函数中
        concatenateModules: true,
      }
    }
    

    代码分割

    默认webpack会把所有代码都打到一个包中,如果代码太多,bundle就会很大,并不是每个模块在启动时都是必要的,最好是分离到多个bundle中,根据应用按需加载。
    代码分割有2种方式:

    • 多入口打包
    • 动态导入

    多入口打包

    适用于多页面应用程序,一个页面对应一个打包入口,公共部分单独提取

    module.exports = {
      mode: 'none',
      entry: {
        index: './src/index.js',
        album: './src/album.js'
      },
      output: {
        filename: '[name].bundle.js' // name会取自输入文件name
      },
    // 不同的打包接口中会有相同的代码出现,于是我们需要提取公共模块
      optimization: {
        splitChunks: {
          // 自动提取所有公共模块到单独 bundle
          chunks: 'all'
        }
      },
      plugins: [
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index.html',
          filename: 'index.html',
          chunks: ['index'] // 指定需要的bundle
        }),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/album.html',
          filename: 'album.html',
          chunks: ['album']
        })
      ]
    }
    

    动态导入

    动态导入的模块会被自动分包,动态导入更灵活,它可以根据代码逻辑实现是否需要导入
    原理是利用import() 和 魔法注释,webpack会自动分包。
    注释的格式如下,是固定的,相同的名字就会打包到一起。
    在react和vue中,就可以在路由分发模块中利用动态导入实现按需加载

    const render = () => {
      const hash = window.location.hash || '#posts'
      const mainElement = document.querySelector('.main')
      mainElement.innerHTML = ''
      if (hash === '#posts') {
        // mainElement.appendChild(posts())
        import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
          mainElement.appendChild(posts())
        })
      } else if (hash === '#album') {
        // mainElement.appendChild(album())
        import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
          mainElement.appendChild(album())
        })
      }
    }
    
    render()
    
    window.addEventListener('hashchange', render)
    

    MiniCssExtractPlugin

    提取css到单个文件

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              // 'style-loader', // 将样式通过 style 标签注入
              MiniCssExtractPlugin.loader, // 个人经验超过150k再单独提取
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new MiniCssExtractPlugin()
      ]
    }
    

    OptimizeCssAssetsWebpackPlugin

    压缩Css,webpack内置的压缩只能压缩JS,其他的压缩需要其他插件支持。

    module.exports = {
      optimization: {
        minimizer: [
          new TerserWebpackPlugin(),
          new OptimizeCssAssetsWebpackPlugin()
        ]
      },
    }
    

    minimizer,允许你通过提供一个或多个定制过的压缩插件,覆盖内置的minimize,所以webpack建议把压缩插件写在这里而不是plugin里

    输出文件名Hash

    生产模式下,文件名使用Hash,解决上线后缓存的问题
    webpack及大多数插件的filename都支持用占位符实现hash。

    '[name].[chunkhash:8].js'
    

    hash:hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值,每一次构建后生成的哈希值都不一样,即使文件内容压根没有改变。

    chunkhash:它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值,由于采用chunkhash,所以项目主入口文件main.js及其对应的依赖文件main.css由于被打包在同一个模块,所以共用相同的chunkhash。
    这样就会有个问题,只要对应css或则js改变,与其关联的文件hash值也会改变,但其内容并没有改变,所以没有达到缓存意义,所以js可以用chunkhash。

    contenthash:contenthash表示由文件内容产生的hash值,内容不同产生的contenthash值也不一样,css文件最好使用contenthash。

    Webpack vs Gulp

    Webpack vs Gulp 谁会被拍死在沙滩上

    相关文章

      网友评论

        本文标题:模块打包器-Webpack

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