其他分享
首页 > 其他分享> > 【前端工程化】三:模块化开发之webpack4

【前端工程化】三:模块化开发之webpack4

作者:互联网

模块化开发

需求,将所有ES6的代码编译成ES5或兼容性更好的代码,并且将转换后的代码打包成一个文件,并且支持不同类型的资源模块;前面两个需求可以使用前面学习的构建系统glup等,但是最后一个需求需要我们学习新的模块化打包工具;

Webpack4

yarn add webpack webpack-cli --dev

webpack支持0配置打包,默认会将src/index.js ->dist/main.js

yarn webpack

因为模块打包需要,所以处理import和export,并不会自动去处理其他问题,所以大多数情况下我们需要自定义配置,在当前目录下新建webpack.config.js文件;

entry:入口文件;

output:出口文件,接收一个对象;

mode:工作模式,none,development,production,默认是production模式;

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'output')
    }
}

资源模块加载

webpack默认只会打包js文件,对于不同类型的资源文件需要使用相对应的Loader;

Loader是webpack的核心特性,借助于不同的Loader就可以加载任何类型的资源;

入口文件是main.css

const path = require('path')

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

推荐的做法是入口文件仍然是js文件,通过import引入css文件;

**webpack建议我们在编写代码的过程中引入当前代码所需要的任何文件;**这是webpack思想值得我们深刻学习的地方;

文件资源加载器

图片字体等文件的打包需要使用到文件资源加载器

按照webpack的思想,我们在需要引入图片的地方使用import来加载;

yarn add file-loader --dev

URL加载器

Data URLs:url就可以直接表示文件内容,不会发送http请求;

yarn add url-loader --dev

此时dist目录下就没有file-loader生成的png文件了,在打包的js文件中以base64的格式来表示图片;

建议: 小文件使用Data URLs,减少请求次数,大文件单独提取存放,提高加载速度;

const path = require('path')

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'main.js',
        path: path.join(__dirname, 'dist'),
        // publicPath: '/dist/'
    },
    mode: 'none',
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 10 * 1024 // 10kb
                    }
                }
            }
        ]
    }
}

当超过10kb的图片会以file-loader的形式去处理,小于10kb的图片转换为Data URLs嵌入代码中;

**注意:**这样使用url-loader之前必须下载file-loader;

常用加载器分类

编译转换类(css-loader),文件操作类(file-loader),代码检查类(eslint-loader);

Babel-loader

yarn add babel-loader @babel/core @babel/preset-env --dev

{
    test: /.js$/,
        use: {
            loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
        }
}

加载资源的方式

yarn add html-loader --dev

{
    test: /.html$/,
        use: {
            loader: 'html-loader',
                options: {
                    attrs: ['img:src', 'a:href']
                }
        }
},

webpack核心工作原理

webpack会根据配置找到其中一个文件作为入口,顺着入口文件的代码,根据import或者require等语句,解析推断出文件所依赖的资源模块,分别去解析每个资源模块对应的依赖,最后形成整个项目的依赖树,webpack递归这个依赖树找到每个节点依赖的资源文件,在根据配置文件中rules属性找到资源文件对应的loader,最后将加载到的结果放入指定的出口文件中,从而实现整个项目的打包;如果没有loader那么webpack只能是一个用来打包和合并js代码的工具;

webpack开发一个Loader

loader的工作过程类似一个流的工作过程,我们可以尝试手动开发一个解析md文档的loader

例:当loader返回可执行的js代码时

在这里插入图片描述
打包后的bundle.js中会出现这段可执行的js代码

在这里插入图片描述
所以需要返回一个可执行的js代码

// markdown-loader.js中
const marked = require('marked')

module.exports = source => {
  // return 'hello'
  // return 'console.log("hello ~")'
  const html = marked(source)
  // return html
  // return `module.exports = ${JSON.stringify(html)}`
  // return `export default ${JSON.stringify(html)}`

  // 返回 html 字符串交给下一个 loader 处理
  return html
}


// webpack.config中
const path = require('path')

module.exports = {
  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'
        ]
      }
    ]
  }
}

webpack的插件机制

Plugin用来解决其他自动化工作,例如清除dist目录,拷贝静态资源文件,压缩输出代码等;

webpack常用插件

绝大多数插件导出的是一个类,在plugins属性中放入实例对象;

清除插件: yarn add clean-wbpack-plugin --dev

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
    plugins: [
    	new CleanWebpackPlugin()
  	]
}

**自动生成HTML插件:**yarn add html-webpack-plugin --dev

**copy-webpack-plugin:**yarn add copy-webpack-plugin

开发阶段我们一般不会使用这个文件

静态资源文件的复制,放入静态资源的目录名 https://cloud.tencent.com/developer/section/1477556

传入一个数组,指定静态资源文件目录

const CopyWebpackPlugin = require('copy-webpack-plugin')

plugins: [
    new CopyWebpackPlugin([
        // 'public/**'
        'public'
    ])
]

webpack开发一个Plugin

Plugin通过钩子机制实现,webpack要求插件必须是一个函数或者是一个包含apply方法的对象;

现在我们开发一个用来清除bundle.js中的注释的插件;

class MyPlugin {
  apply (compiler) {
    console.log('MyPlugin 启动')

    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        // console.log(name)
        // console.log(compilation.assets[name].source())
        if (name.endsWith('.js')) {
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length
          }
        }
      }
    })
  }
}

webpack开发体验问题

上面我们都是通过 编写源代码 =》webpack打包 =》 运行应用 =》 刷新浏览器 的方式来开发

自动编译: 我们可以使用watch工作模式来监听文件的变动,重新编译;

watch模式会自动编译代码,然后我们需要手动刷新浏览器才可以看到最新的结果

**自动刷新:**BrowserSync插件可以帮我们实现自动刷新的功能;

webpack自动编译到我们的dist目录中,被browser-sync拦截到变化然后自动刷新

**但是!**这么做的话操作麻烦,并且需要反复读取磁盘效率低下,要是有更好的工具能替代就完美了!

Webpack Dev Server

yarn add webpack-dev-server --dev

yarn webpack-dev-server

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

映射转换之后的代码与源代码之间的关系

在源文件中的最后使用注释 //# sourceMappingURL=bundle.js.map 来引入source Map

webpack现在支持12种source Map模式,这里只给推荐;

开发环境: cheap-module-eval-source-map

生产模式: none 或者 nosources-source-map

webpack HMR体验

前面我们配置了自动刷新,但是每次自动刷新都会丢失状态,体验不友好;

问题核心: 自动刷新导致的页面状态丢失;

开启HMR: 可以通过 yarn webpack-dev-server --hot 或者在配置文件中配置

const webpack = require('webpack')

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js'
  },
  devtool: 'source-map',
  devServer: {
    hot: true,
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
}

Q1: 当我们修改css文件时,可以直接热更新,但是修改了js文件时,会自动刷新?

Q2: 项目中使用了框架,js照样可以热替换?

注意: webapck中的HMR并不是开箱即用的,需要手动处理模块热替换逻辑;

HMR API

import createEditor from './editor'
import background from './better.png'
import './global.css'

const editor = createEditor()
document.body.appendChild(editor)

const img = new Image()
img.src = background
document.body.appendChild(img)

// ================================================================
// HMR 手动处理模块热更新
// 不用担心这些代码在生产环境冗余的问题,因为通过 webpack 打包后,
// 这些代码全部会被移除,这些只是开发阶段用到
if (module.hot) {
  let hotEditor = editor
  module.hot.accept('./editor.js', () => {
    // 当 editor.js 更新,自动执行此函数
    // 临时记录编辑器内容
    const value = hotEditor.innerHTML
    // 移除更新前的元素
    document.body.removeChild(hotEditor)
    // 创建新的编辑器
    // 此时 createEditor 已经是更新过后的函数了
    hotEditor = createEditor()
    // 还原编辑器内容
    hotEditor.innerHTML = value
    // 追加到页面
    document.body.appendChild(hotEditor)
  })

  module.hot.accept('./better.png', () => {
    // 当 better.png 更新后执行
    // 重写设置 src 会触发图片元素重新加载,从而局部更新图片
    img.src = background
  })

  // style-loader 内部自动处理更新样式,所以不需要手动处理样式模块
}

webpack生产环境优化

生产环境与开发环境的差别大,开发环境注重开发效率,而生产环境注重运行效率;

webpack4提供了mode模式,在生产环境下为我们提供了很多默认的配置;

要使用不同环境下的配置有以下方法

  1. 配置文件根据环境不同导出不同配置;

    配置文件导出的函数中,会传入两个参数,其中env就是环境变量,可以根据env对不同环境做不同配置;

    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 = {
        ....// 开发环境下的配置
      }
    
      if (env === 'production') {
        config.mode = 'production'
        config.devtool = false
        config.plugins = [
          ...config.plugins,
          new CleanWebpackPlugin(),
          new CopyWebpackPlugin(['public'])
        ]
      }
    
      return config
    }
    
    
  2. 一个环境对应一个配置文件,通常会配置三个文件,公共配置、开发环境配置、生产环境配置;

    对于配置文件的合并,我们一般会用到webpack-merge插件;

    const webpack = require('webpack')
    const merge = require('webpack-merge')
    const common = require('./webpack.common')
    
    module.exports = merge(common, {
      mode: 'development',
      devtool: 'cheap-eval-module-source-map',
      devServer: {
        hot: true,
        contentBase: 'public'
      },
      plugins: [
        new webpack.HotModuleReplacementPlugin()
      ]
    })
    

DefinePlugin

为代码注入全局成员,在production模式下默认会启用并注入process.env.NODE_ENV

const webpack = require('webpack')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: JSON.stringify('https://api.example.com')
      // API_BASE_URL: " 'https://api.example.com' "
    })
  ]
}

Tree-shaking

a文件中导出了三个方法,b文件只引入了其中一个方法,另外两个方法就是未引用代码;

Tree-shaking就是去除未引用代码,tree-shaking并不是值某个配置选项,而是一组功能搭配使用后的效果,会在production模式下自动开启;

webpack配置中提供optimization属性,用来集中配置webpack的优化功能;

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    concatenateModules: true,
    // 压缩输出结果
    minimize: true
  }
}

**注意:**Tree-shaking前提是ES Module模式模块化,在我们使用的@babel/preset-env会将ES Module转换成Common JS模式,所以会导致Tree-shaking失效;

可以去配置中修改babel的转换方式

module: {
    rules: [
        {
            test: /\.js$/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: [
                        // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
                        // ['@babel/preset-env', { modules: 'commonjs' }]
                        // ['@babel/preset-env', { modules: false }]
                        // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
                        ['@babel/preset-env', { modules: 'auto' }]
                    ]
                }
            }
        }
    ]
},

sideEffects

副作用:模块执行时除了导出成员之外所作的事情(比如在Number的原型对象上新增一个方法),一般用于npm包标记是否有副作用;

optimization属性中开启sideEffects,开启之后webpack就会检查当前代码所属的package.json有没有sideEffects标识,以此来判断该模块是否有副作用,如果该模块没有副作用,那么webpack就不会将它打包;

webpack.config.js中

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  optimization: {
    sideEffects: true,
    // 模块只导出被使用的成员
    // usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    // concatenateModules: true,
    // 压缩输出结果
    // minimize: true,
  }
}

package.json中

{
  "name": "31-side-effects",
  "version": "0.1.0",
  "main": "index.js",
  "author": "zce <w@zce.me> (https://zce.me)",
  "license": "MIT",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "css-loader": "^3.2.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.9"
  },
  // 标记该项目中都没有副作用
  // "sideEffects": false
  "sideEffects": [
  	"*.css",
  	"./src/extend.js"
  ]
}

代码分割

项目的所有代码都会打包到了一起,当项目越来越大的时候,会导致bundle.js的体积特别大,但是并不是每个模块在启动的时候都是必要的;更为合理是把我们打包的结果按照一定的规则去分离到多个bundle.js中,根据我们定义的运行需要,按需加载;

http1.1版本的缺陷

为了解决以上的需求,webpack支持了Code Splitting(代码分包/代码分割),有以下两种实现方式:

多入口打包

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

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: 'all'
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

动态导入

需要用到某个模块时,再加载这个模块

动态导入的模块会被自动分包,import(’./posts/posts’)

魔法注释

动态导入的模块打包后的文件名都为序号,如果想要给打包后的文件命名,可以使用魔法注释;

相同的webapackChunkName会被打包到一起;

if (hash === '#posts') {
    import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }

MiniCssExtractPlugin

将Css文件单独提取一个文件中,推荐当css文件大于150kb时在考虑使用该插件,否则会多一次css的请求;

const MiniCssExtractPlugin = require('mini-css-extract-plugin')


...
module: {
    rules: [
        {
            test: /\.css$/,
            use: [
                // 'style-loader', // 将样式通过 style 标签注入
                MiniCssExtractPlugin.loader,
                'css-loader'
            ]
        }
    ]
},

optimize-css-assets-webpack-plugin

webpack默认只会压缩js文件,经过分包后的css并没有被压缩,这时候我们需要借助插件

...
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  ...,
  plugins: [
    ...,
    new OptimizeCssAssetsWebpackPlugin()
  ]
}
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  devtool: 'cheap-eval-source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  optimization: {
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin(),
      new TerserWebpackPlugin()
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin(),
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      title: 'xp item'
    }),
    new OptimizeCssAssetsWebpackPlugin()
  ]
}

输出文件名Hash

在缓存策略中,如果我们把缓存时间设置的过短,那效果可能不是很明显,一旦把时间设置的很长,那应用更新重新部署以后没有办法及时更新到后端,所以生产模式下,文件名使用Hash,当资源文件发生改变时,文件名称也会发生变化,对于后端而言就是全新的请求,这样我们可以把缓存策略的时间设置的比较长,不用担心更新不及时的问题;

配置中的所有filename等路径都支持hash模式

hash有以下三种模式:

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name]-[contenthash:8].bundle.js'
  },
  optimization: {
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import',
      template: './src/index.html',
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name]-[contenthash:8].bundle.css'
    })
  ]
}

标签:webpack4,const,模块化,loader,webpack,html,工程化,js,css
来源: https://blog.csdn.net/Whoopsina/article/details/122484606