美文网首页
前端模块化开发—webpack详细介绍

前端模块化开发—webpack详细介绍

作者: 我是一只小毛毛 | 来源:发表于2021-02-10 17:03 被阅读0次

    一、模块化开发

    common.js规范

    1. 一个文件就是一个模块
    2. 每个模块都有单独的作用域
    3. 通过module.exports导出成员
    4. 通过require函数载入模块

    commonJS是以同步模式加载模块

    AMD(异步的模块定义规范)

    Require.js

    ES Modules

    基本特性
    1. 自动采用严格模式
    2. 每个ESM模块都是单独的私有作用域
    3. ESM是通过CORS去请求外部JS模块的
    4. ESM的script标签会延迟执行脚本
    导入和导出
     <!--加载模块不需要提取成员-->
    import {} from './modules'
    <!--提取模块的所有成员-->
    import * as mod from './modules.js'
    <!--动态导入模块-->
    import ('./modules.js').then(modules=>{
        console.log(modules)
    })
    <!--同时导出默认成员和具名成员-->
    import title, { name, age } from './modules.js'
    
    直接导出导入成员

    1、将多个模块统一在一个文件导出,在从统一入口进行引用
    2、使用polifill解决浏览器不兼容ESmodules的问题(只适用于本地测试开发)

    <!--在不支持ESModules的文件中使用该polifill文件-->
    <script nomodule src="..."><script>
    
    在node环境下使用ES Modules编写代码

    1、将文件扩展名从js改为mjs

    <!--index.mjs-->
    import { foo, bar} from './modules.mjs'
    

    2、使用node --experimental-module执行mjs文件

    node --experimental-modules index.mjs
    

    注意事项
    1、系统内置成员可以通过ES module的提取成员方式导入,也可以默认导入

    import fs from 'fs' 
    import { writeFileSync } from 'fs'
    

    2、第三方模块不支持直接提取成员,因为第三方模块都是默认导出

    <!--不支持-->
    import { _cameCase } from 'lodash'
    <!--仅支持-->
    import _ from 'lodash'
    console.log(_comCase('ES Module'))
    
    ES modulees 与Common JS模块交互
    1. 可以在ES Module中导入commonJS模块
    <!--common.js文件-->
    <!--Comonjs模块始终只会导出一个默认成员-->
    modules.exports = {
        foo: 1111
    }
    ====>
    exports.foo = 111;
    
    <!--ES Module.mjs文件-->
     <!--导入默认成员,不能直接提取成员,注意import不是解构出对象-->
    import mod from './commonjs.js'  
    console.log(mod) // { foo :111}
    
    <!--不支持以下写法-->
    import { foo } from './commonjs.js'
    
    1. CommonJs中不能导入ES Module模块

    2. CommonJs始终只会导出一个默认成员

    3. 注意import不是解构导出对象

    4. 在node的最新版本中,在package.json中添加type字段,就表示该工程默认使用ES Module编写代码,这意味着可以不用将js文件改为mjs,不过此时如果还需要使用commonJs,需要将CommonJS模块文件改为cjs后缀名

    <!--package.json-->
    {
        type: "module"
    }
    <!--运行-->
    node --exprimental-modules index.js
    node --exprimental-modules common.cjs
    

    二、模块化打包工具

    模块化打包工具的由来

    • 新特性代码编译
    • 模块化JavaScript打包
    • 执行不同类型的资源文件

    模块化打包工具概要

    打包工具解决的是前端整体的模块化,并不是单指JavaScript模块化

    webpack

    资源文件加载

    样式文件加载

    const path = require('path');
    module.exports = {
        mode:'none',
        entry: './src/index.js',
        output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'dist')
        },
        module: {
            rules: [
                {
                    test: /.css$/,
                    use: [
                        'style-loader',
                        'css-loader'
                    ]
                }
            ]
        }
    }
    

    文件资源加载

    const path = require('path');
    module.exports = {
        mode:'none',
        entry: './src/index.js',
        output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'dist')
        },
        module: {
            rules: [
                {
                    test: /.css$/,
                    use: [
                        'style-loader',
                        'css-loader'
                    ]
                }
            ]
        }
    }
    

    常用加载器分类

    • 编译转换类,转换为JS代码,如css-loader
    • 文件操作类,将资源文件拷贝到输出目录,将文件访问路径向外导出,如:file-loader
    • 代码检查器,统一代码风格,提高代码质量,如:eslint-loader

    webpack 处理ES2015

    因为模块打包需要,所以webpack可以处理import和export,除此之外,并不能转换其他的ES6特性。如果想要处理ES6,需要安装转化ES6的编译型loader,最常用的就是babel-loader,babel-loader依赖于babel的核心模块,@babel/core和@babel/preset-env

    {
       test: /.js$/,
       use: {
         loader: 'babel-loader',
         options: {
           presets: ['@babel/preset-env']
         }
       },
        exclude: /(node_modules)/, // 这个必须配置
     }
    

    注意:Webpack只是打包工具,加载器可以用来编译转化代码

    加载资源的方式

    • 遵循ES Modules标准的import声明

    • 遵循CommonJS标准的require函数。对于ES的默认导出,要通过require('./XXX').default的形式获取

    • 遵循AMD标准的define函数和require函数

    • Loader加载的非JavaScript也会触发资源加载

      css-loader在处理css代码时,遇到url函数,会将这个资源文件 交给url-loader处理

    webpack的核心工作原理

    核心工作原理:

    1. 根据配置找到打包入口文件
    2. 顺着入口文件代码里的 import 和 require之类的语句
      解析推断文件所依赖的资源模块
    3. 分别去解析每个资源模块对应的依赖,最后形成一颗依赖树
    4. 递归依赖树,找到每个节点对应的资源文件
    5. 根据配置文件 rules 属性,找到资源模块所对应的加载器,交给对应的加载器加载对应的资源模块
    6. 最后将加载以后的结果放入到bundle.js打包结果里
      实现整个项目的打包。

    webpack Loader的工作原理

    loader机制是webpack的核心特性之一。每个 Webpack 的 Loader 都需要导出一个函数,这个函数就是我们这个 Loader 对资源的处理过程,它的输入就是加载到的资源文件内容,输出就是我们加工后的结果。我们通过 source 参数接收输入,通过返回值输出。

    对于返回的输出,有两种思路:

    • 直接在这个 Loader 的最后返回一段 JS 代码字符串

    • 再找一个合适的加载器,在后面接着处理我们这里得到的结果

    Webpack 加载资源文件的过程类似于一个工作管道,你可以在这个过程中依次使用多个 Loader,但是最终这个管道结束过后的结果必须是一段标准的 JS 代码字符串。

    // ./markdown-loader.js
    const marked = require('marked')
    
    module.exports = source => {
      const html = marked(source)
      // const code = `module.exports = ${JSON.stringify(html)}`
      const code = `export default ${JSON.stringify(html)}`
      return code 
    }
    

    插件机制

    插件机制的是webpack的另一个核心特性,目的是为了增强webpack自动化方面的能力。

    常见的插件介绍

    CleanWebpackPlugin、HtmlWebpackPlugin、CopyWebpackPlugin

    插件使用总结

    webpack开发体验问题

    自动进行编译:npx webpack --watch会监视文件的变化自动进行打包

    自动打开浏览器: npx webpack-dev-server --open

    source map

    Source Map解决了源代码与运行代码不一致所产生的问题.Webpack 支持sourceMap 12种不同的方式,每种方式的效率和效果各不相同。效果最好的速度最慢,速度最快的效果最差.下面是几种常用方式介绍:

    • eval- 是否使用eval执行代码模块
    • cheap- Source map是否包含行信息
    • module-是否能够得到Loader处理之前的源代码
    • inline- SourceMap 不是物理文件,而是以URL形式嵌入到代码中
    • hidden- 看不到SourceMap文件,但确实是生成了该文件
    • nosources- 没有源代码,但是有行列信息。为了在生产模式下保护源代码不被暴露

    开发模式推荐使用:eval-cheap-module-source-map,原因:

    • 代码每行不会太长,没有列也没问题
    • 代码经过Loader转换后的差异较大
    • 首次打包速度慢无所谓,重新打包相对较快

    生产模式推荐使用:none,原因:

    • Source Map会暴露源代码
    • 调试是开发阶段的事情
    • 对代码实在没有信心可以使用nosources-source-map

    devtool

    201802100830451.png

    webpack HRM

    HMR(Hot Module Replacement) 模块热替换,应用运行过程中,实时替换某个模块,应用运行状态不受影响。

    webpack-dev-server自动刷新导致的页面状态丢失。我们希望在页面不刷新的前提下,模块也可以即使更新。热替换只将修改的模块实时替换至应用中。

    HMR是webpack中最强大的功能之一,极大程度的提高了开发者的工作效率。

    HMR已经集成在了webpack-dev-server中,运行webpack-dev-server --hot,也可以通过配置文件开启.

    Webpack中的HMR并不是对所有文件开箱即用,样式文件支持热更新,脚本文件需要手动处理模块热替换逻辑。而通过脚手架创建的项目内部都集成了HMR方案。

    HMR注意事项:

    • 处理HMR的代码报错会导致自动刷新
    • 没启动HMR的情况下,HMR API报错
    • 代码中多了很多与业务无关的代码

    生产环境优化

    我们在生产环境中,更注重开发效率,而在生产环境中,更注重开发效率。

    模式(mode)

    webpack建议我们为不同的环境创建不同的配置,两种方案:

    • 配置文件根据环境不同导出不同配置
    const path = require('path')
    const webpack = require('webpack')
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    
    module.exports = (env, argv) => {
      const config = {
        mode: 'none',
        entry: './src/main.js',
        output: {
          filename: 'bundle.js',
          path: path.join(__dirname, 'dist'),
          // publicPath: 'dist/'
        },
        module: {
          rules: [
            {
              test: /.md$/,
              use: ['html-loader', './markdown-loader.js']
            }
          ]
        },
        plugins: [
          new CleanWebpackPlugin(),
          // 用于生成index.html 
          new HtmlWebpackPlugin({
            title: 'Webpack Plugin Sample',
            meta: {
              viewport: 'width=device-width'
            },
            template: './src/index.html'
          }),
          // 用于生成about.html 
          new HtmlWebpackPlugin({
            filename: 'about.html'
          }),
          // 开发过程最好不要使用这个插件
          // new CopyWebpackPlugin({
          //   patterns: ['public']
          // }),
          // new MyPlugin(),
          new webpack.HotModuleReplacementPlugin()
        ],
        devServer: {
          contentBase: './public',
          proxy: {
            '/api': {// 以/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, // 以实际代理的主机名去请求
            }
          },
          // hot: true
          hotOnly: true, // 如果热替换代码报错了,则不刷新
        },
        devtool: 'eval-cheap-module-source-map'
      }
      
      if (env === 'production') {
        config.mode = 'production'
        config.devtool = false
        config.plugins = [
          ...config.plugins,
          new CleanWebpackPlugin(),
          new CopyWebpackPlugin({
            patterns: ['public']
          })
        ]
      }
      return config
    }
    
    
    • 一个环境对应一个配置文件

    Webpack.common.js

    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
      entry: './src/main.js',
      output: {
        filename: `bundle.js`
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          }
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          filename: `index.html`
        })
      ]
    }
    
    

    Webpack.dev.js

    const common = require('./webpack.common')
    const merge = require('webpack-merge')
    
    module.export = merge(common, {
      mode: 'development',
    })
    

    Webpack.prod.js

    const common = require('./webpack.common')
    const merge = require('webpack-merge')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    
    module.exports = merge(common, {
      mode: 'production',
      plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin({
            patterns: ['public']
        })
      ]
    })
    

    Package.json

      "scripts": {
        "server": "npx webpack serve --config webpack.dev.js --open",
        "build": "webpack --config webpack.prod.js"
      },
    

    webpack的优化配置

    • definePlugin
      DefinePlugin 为代码注入全局成员,这个内置插件默认就会启动,往每个代码中注入一个全局变量process.env.NODE_ENV
    const webpack = require('webpack')
    
    plugins: [
        new HtmlWebpackPlugin({
          filename: `index.html`
        }),
        new webpack.DefinePlugin({
          API_BASE_URL: JSON.stringify('http://api.example.com')
        })
      ]`
    
    • Tree-Shaking 摇掉代码中未引用到的代码(dead-code),这个功能在生产模式下自动被开启。Tree-Shaking并不是webpack中的某一个配置选项,而是一组功能搭配使用后的效果。因为Tree-Shaking前提是ES Modules,由Webpack打包的代码必须使用ESM,为了转化ES中的新特性,会使用babel处理新特性,就有可能将ESM转化CommonJS,而我们使用的@babel/preset-env这个插件集合就会转化ESM为CommonJS,所以Tree-Shaking会不生效。但是在最新版babel-loader关闭了转换ESM的插件,所以使用babel-loader不会导致Tree-Shaking失效
    optimization: {
        usedExports: true,
        minimize: true
      }
    
    • 合并模块函数 concatenateModules, 又被成为Scope Hoisting,作用域提升
    optimization: {
       usedExports: true,
       minimize: true,
       concatenateModules: true
     }
    
    • sideEffects 副作用,指的是模块执行时除了导出成员之外所做的事情,sideEffects一般用于npm包标记是否有副作用。如果没有副作用,则没有用到的模块则不会被打包。
    optimization: {
        usedExports: true,
        minimize: true,
        concatenateModules: true,
        sideEffects: true
      }
    

    在package.json里面增加一个属性sideEffects,值为false,表示没有副作用,没有用到的代码则不进行打包。确保你的代码真的没有副作用,否则在webpack打包时就会误删掉有副作用的代码,比如说在原型上添加方法,则是副作用代码;还有CSS代码也属于副作用代码。

    代码分割

    webpack的一个弊端:所有的代码都会被打包到一起,如果应用复杂,bundle会非常大。而并不是每个模块在启动时都是必要的,所以需要分包、按需加载。物极必反,资源太大了不行,太碎了也不行。太大了会影响加载速度;太碎了会导致请求次数过多,因为在目前主流的HTTP1.1有很多缺陷,如同域并行请求限制、每次请求都会有一定的延迟,请求的Header浪费带宽流量。所以模块打包时有必要的。

    目前的webpack分包方式有两种:

    • 多入口打包:适用于多页应用程序,一个页面对应一个打包入口,公共部分单独抽取。
    entry: {
      index: './src/index.js',
        album: './src/album.js'
    },
    output: {
        filename: '[name].bundle.js'
    },
    // 每个打包入口形成一个独立的chunk
    plugins: [
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index.html',
          filename: 'index.html',
          chunks: ['index']
        }),
        new HtmlWebpackPlugin({
          title: 'Nulti Entry',
          template: './src/album.html',
          filename: 'album.html',
          chunks: ['album']
        })
      ],
    // 不同的打包入口肯定会有公共模块,我们需要提取公共模块:
        optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    
    
    • 动态导入:需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包。通过动态导入生成的文件只是一个序号,可以使用魔法注释指定分包产生bundle的名称。相同的chunk名会被打包到一起。
    import(/* webpackChunkName: 'posts' */'./post/posts').then({default: posts}) => {
      mainElement.appendChild(posts())
    }
    

    MiniCssExtractPlugin可以提取CSS到单个文件

    当css代码超过150kb左右才建议使用。

    const MiniCssExtracPlugin = require('mini-css-extract-plugin')
    
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            // 'style-loader',
            MiniCssExtracPlugin.loader,
            'css-loader'
          ]
        }
      ]
    },
    

    OptimizeCssAssetsWebpackPlugin 压缩输出的CSS文件

    webpack仅支持对js的压缩,其他文件的压缩需要使用插件。

    可以使用 optimize-css-assets-webpack-plugin压缩CSS代码。放到minimizer中,在生产模式下就会自动压缩

    optimization: {
      minimizer: [
        new TerseWebpackPlugin(), // 指定了minimizer说明要自定义压缩器,所以要把JS的压缩器指指明,否则无法压缩
        new OptimizeCssAssetWebpackPlugin()
      ]
    }
    

    输出文件名hash

    生产模式下,文件名使用Hash

    项目级别的hash

    output: {
          filename: '[name]-[hash].bundle.js'
      },
    

    chunk级别的hash

    output: {
          filename: '[name]-[chunkhash].bundle.js'
      },
    

    文件级别的hash,:8是指定hash长度 (推荐)

    output: {
          filename: '[name]-[contenthash:8].bundle.js'
      },
    

    相关文章

      网友评论

          本文标题:前端模块化开发—webpack详细介绍

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