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

2021-02-10  本文已影响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模块化

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'
                ]
            }
        ]
    }
}

常用加载器分类

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只是打包工具,加载器可以用来编译转化代码

加载资源的方式

webpack的核心工作原理

核心工作原理:

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

webpack Loader的工作原理

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

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

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-cheap-module-source-map,原因:

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

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注意事项:

生产环境优化

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

模式(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的优化配置

const webpack = require('webpack')

plugins: [
    new HtmlWebpackPlugin({
      filename: `index.html`
    }),
    new webpack.DefinePlugin({
      API_BASE_URL: JSON.stringify('http://api.example.com')
    })
  ]`
optimization: {
    usedExports: true,
    minimize: true
  }
optimization: {
   usedExports: true,
   minimize: true,
   concatenateModules: true
 }
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'
    }
  }

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'
  },
上一篇 下一篇

猜你喜欢

热点阅读