重学webpack4之基础篇

1,422 阅读6分钟

往期文章:

往期文章:

安装

  • cnpm i webpack webpack-cli
  • 模块局部安装会在 node_modules/.bin/webpack 目录创建软链接

基础

entry

  • 依赖入口
// 单入口,SPA
entry: 'xx/xx.js'
// 多入口 MPA
entry: {
  app: './src/app.js',
  adminApp: './src/adminApp.js'
}

output

  • 指定打包后的输出
output: {
  path: path.resoluve(__diranme,'dist')
  filename: '[name].js' // 单入口可以写死文件名,多入口一定要使用占位符[name],来自动生成多个文件
  // filename: '[name].[chunkhash:5]]js'
  // filename: '[name].[hash]js'
}

Loaders

  • wepback开箱即用只支持JS和JSON两种文件类型,通过Loader去支持其他文件类型并把他们转化成有效的模块,并且可以添加到依赖图中
  • 本身是一个函数,接受源文件作为参数,返回转换的结果
  • 例如: babel/ts-loader、css/less/scss-loader、url/file-loader、raw-loader(将.txt文件以字符串的形式导入)、thread-loader(多进程打包js和css)
module: {
  rules: [
    {test: /.txt$/, use: 'raw-loader'},
    {test: /.css$/, use: [
      {
        loader: 'css-loader',
        options: {
          modules: {
            localIdentName: '[path][name]__[local]--[hash:base64:5]'
          }
        }
      }
    ]}
  ]
}

解析ES6

  • 使用babel-loader,babel的配置文件.babelrc
  • 安装 babel-loader、@babel/core @babel/preset-env
// webpack.config
module: {
  rules: [
    {test: /.js?x$/, use: 'babel-loader', exclude: /node_modules/}
  ]
}
// .babelrc
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "plugins": [
    // 各种插件
    "@babel/propsoal-class-properties"
  ]
}

css/less/scss-loader

  • css-loader用于加载.css文件,并且转换成 common 对象
  • style-loader 将样式通过 style 标签,插入到 head 中
// use: [loader1,loader2,loader3],loader的处理顺序是 3>2>1,从后往前
module: {
  rules: [
    {
        test: /.s?css$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              // css模块化使用
              modules: {
                localIdentName: '[path][name]__[local]--[hash:base64:5]'
              }
            }
          },
          'postcss-loader',
          'sass-loader'
        ],
        include: [],
        exclude: [
          Root('src/components')
        ]
      },
  ]
}

url/file-loader

  • 用于处理文件,图片、字体、多媒体
  • url-loader 实现较小的图片转成base64,插入到代码中,当超过限制的limit后,会自动降级到file-loader
{
  test: /\.(png|jpg|jpeg|gif|eot|woff|woff2|ttf|svg|otf)$/,
  use: [
    {
     loader: 'url-loader',
     options: {
       limit: 10 * 1024, // 10k
       name: isDev ? 'images/[name].[ext]' : 'images/[name].[hash.[ext]',
       publicPath: idDev ? '/' : 'cdn地址',
     },
    },
    // prduction,用于图片压缩
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true
      }
    }
  ]
},

postcss-loader

  • 预处理器, autoprefixer(需要安装)
rules: [
  {test: /.css$/, use: [
  {
    'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        plugins: () => {
          require('autoprefixer')({
            browsers: ['last 2 version', '>1%', 'ios7']
          })
        }
      }
    },
    'sass-loader'
  }
]
}
或者将postcss-loader的options放在根目录的postcss.config.jsrules: [
  {
    test: /.css$/, 
    use: [
      'style-loader',
      'css-loader',
      'postcss-loader'
      'less-loader',
    ]
  }
]
// postcss.config.js 和 package.json(或者.browserslistrc,推荐使用.browserslistrc)
// 安装postcss-preset-env,autoprefixer
module.exports = {
  loader: 'postcss-loader',
  plugins: {
    'postcss-preset-env': {
      stage: 0,
      features: {
        'nesting-rules': true,
        'autoprefixer': { grid: true },
        ''
      }
    }
  }
};
// package.json
  "browserslist": [
    "last 2 version",
    ">1%",
    "iOS >= 7"
  ]
// .browserslistrc
last 2 version
>1%
iOS >= 7

px自动转rem(两种方式)

  • 方式一:px2rem-loader与lib-flexible,页面渲染时计算根元素的font-size,推荐使用:
    • cnpm i -D px2rem-loader
    • cnpm i -S lib-flexible (不推荐)将lib-flexible代码拷贝到html>head>script中 使用raw-loader内联lib-flexible
rules: [
  {
    test: /.css$/, 
    use: [
    {
      'style-loader',
      'css-loader',
      'postcss-loader',
      {
        loader: 'px2rem-loader',
        options: {
          remUnit: 75, // 一个rem等于多少px
          remPrecision: 8 // px转换成rem的小数位
        }
      }
      'sass-loader',
    }
  ]
}
  • 方式二,使用postcss-loader与postcss-px2rem-exclude
module.exports = {
  loader: 'postcss-loader',
  plugins: {
    'postcss-preset-env': {
      stage: 0,
      features: {
        'nesting-rules': true,
        'autoprefixer': { grid: true },
      }
    },
    'postcss-px2rem-exclude': {
      remUnit: 200,
      exclude: /node_modules/i
    }
  }
};

plugins

  • 插件用于bundle文件的优化,资源管理和环境变量注入,作用于整个构建过程
plugins: [
  new HtmlWpeckPlugin({})
]

webpack-dev-server

  • webpack-dev-server,开启本地服务器,监听文件变化后,热更新页面
  • 不刷新浏览器而是热更新,不输出文件,而是放在内存中
  • 配合 new.webpack.HotModuleReplacementPlugin() 或 react-hot-loader 插件使用
// package.json
webpack-dev-server mode=development -open
// config
module.exports = {
    devServer: {
    host: '0.0.0.0',
    compress: true,
    port: '3000',
    contentBase: join(__dirname, '../dist'),//监听的目录,用于刷新浏览器
    hot: true,
    overlay: {
      errors: true,
      warnings: true
    },
    disableHostCheck: true,
    publicPath: '/', // 设置时,要与output.publicPath保持一致
    historyApiFallback: true,
    // historyApiFallback: {
    //   rewrites: [from: /.*/, to: path.posix.join('/',     // 'index.html')],
    //}
    proxy: {
      '/api': 'http://localhost:8081',
    }
    //proxy: {
    //  '/api/*': {
    //     target: 'https://xxx.com',
    //     changeOrigin: true,
    //     secure: false,
    //     headers: {},
    //     onProxyReq: function(proxyReq, req, res) {
    //       proxyReq.setHeader('origin', 'xxx.com');
    //       proxyReq.setHeader('referer', 'xxx.com');
    //       proxyReq.setHeader('cookie', 'xxxxx');
    //     },
    //     onProxyRes: function(proxyRes, req, res) {
    //       const cookies = proxyRes.header['set-cookie'];
    //       cookies && buildCookie(cookies)
    //     }
    //  }
   // }
  },
}

webpack-dev-middleware

  • 将webpack输出文件传输给服务器,适用于灵活的定制场景
const express = requrie('express')
const webpack = require('webpack')
const webpackDevMiddleware = requrie('webpack-dev-middleware')

const app = express()
const config = require('./webpack.config.js')
const compiler = webpack(config)

app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}))

app.listen(3000)

热更新原理

mini-css-extract-plugin和optimize-css-assets-webpack-plugin
  • 提取css,建议使用contenthash
module: {
  rules: [
    {
        test: /.s?css$/,
        use: [
          isDev ? 'style-loader' : iniCssExtractPlugin.loader
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              // css模块化使用
              modules: {
                localIdentName: '[path][name]__[local]--[hash:base64:5]'
              }
            }
          },
          'postcss-loader',
          'sass-loader'
        ]
      },
  ]
},
plugins: [
  // 提取css
  new MiniCssExtractPlugin({
    filename: 'styles/[name].[contenthash:5].css',
  }),
  // 压缩css
  new OptimizeCSSAssetsPlugin({
    assetNameRegExp: /\.css$/g,
    cssProcessor: require("cssnano"),//需要安装cssnano
    cssProcessorPluginOptions: {
      preset: [
        'default',
        {
          discardComments: {
            removeAll: true
          }
        }
      ]
    },
    canPrint: true
  }),
]

html-webpack-plugin

plugins: [
  new HtmlWebpackPlugin({
    // 自定义参数title传递到html中
    // html中使用<title><%= htmlWebpackPlugin.options.title %></title>
    // <script>let number = <%= htmlWebpackPlugin.options.number %><script>
    number: 1,
    title: '京程一灯CRM系统',
    filename: 'index.html',
    // chunks: ['index'] //用于多页面,使用哪些chunk
    template: resolve(__dirname, '/src/index.html'),
    minify: {
      minifyJS: true,
      minifyCSS: true,
      removeComments: true,
      collapseWhitespace: true,
      preserveLineBreak: false,
      removeAttributeQuotes: true,
      removeComments: false
    }
  }),
]

clean-webpack-plugin 或者使用 rimraf dist

plugins: [
  new CleanWebpackPlugin()
]

mode

  • Mode 用来指定当前构建环境 development、production 和 none
  • 设置 mode 可以使用 wepack内置函数,内部自动开启一些配置项,默认值为 production
内置功能
development:process.env.NODE_ENV为development,开启NamedChunksPlugin 和 NameModulesPlugin
这两个插件用于热更新,控制台打印路径
prodution:process.env.NODE_ENV为prodution.开启 FlagDependencyUsagePlugin、ModuleConcatenationPlugin、NoEmitOnErrorsPlugin,OccurrentceOrderPlugin、SideEffectsFlagPlugin等
none:不开启任何优化选项

watch

  • 文件监听可以在webpack命令后加上 --watch 参数,或在webpack.config中设置watch:true
  • 原理:轮询判断文件的最后编辑时间是否变化
module.exports = {
  // 默认false,不开启
  watch: true,
  // 只有开启时,watchOptions才有意义
  watchOptions: {
    // 忽略,支持正则
    ignored: /node_modules/,
    // 监听到变化后等300ms再执行,默认300ms
    aggregateTimeout: 300,
    // 怕乱文件是否变化是通过不停询问系统指定文件有没变化实现的,默认每秒1000次
    poll: 1000
  }
}

文件指纹

  • 打包后输出文件名后缀,也就是hash值
  • hash:和整个项目构建相关,只要项目中有一个文件修改,整个项目中的文件hash都会修改成统一的一个
  • chunkhash:和webpck打包的chunk有关,不同的entry会生成不同的chunkhash值(适用于js文件)
  • contenthash:根据文件内容定义hash,文件内容不变,则contenthash不变,用于批量更新(适用于css文件)

资源内联

  • 页面框架的初始化,比如flexible
  • 上报相关打点
  • css内联避免页面闪动
// raw-loader@0.5.1 内联html片段,在template中弄
<!-- 内联html -->
<%= require('raw-loader!./meta.html') %>
// raw-loader内联js
<!-- 初始化脚本,例如flexible -->
<script><%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %></script>

css内联合

  • 方式一:style-loader
module: {
  rules: [
    {
        test: /.s?css$/,
        use: [
          {
            loader: 'style-loader',
            options: {
              injectType: 'singletonStyleTag', // 将所有style标签合并成一个
            }
          }
          'css-loader'
          'postcss-loader',
          'sass-loader'
        ]
      },
  ]
},
  • 方式二:html-inline-css-webpack-plugin 首先使用 mini-css-extract-plugin(而非 style-loader)将 css 提取打包成一个独立的 css chunk 文件 然后使用 html-webpack-plugin 生成 html 页面 最后使用 html-inline-css-webpack-plugin 读取打包好的 css chunk 内容注入到页面,原本 html-webpack-plugin 只会引入 css 资源地址,现在实现了 css 内联
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
 
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    }),
    new HtmlWebpackPlugin(),
    new HTMLInlineCSSWebpackPlugin(),
  ],
}

style-loader vs html-inline-css-webpack-plugin

  • style-loader是css-in-js,需要加载js后才能写入到style中,有一定的延迟性
  • html-inline-css-webpack-plugin是将css提取出来,再写入到html中,html网页源代码中已经内联好css了,没有延迟性了
  • 好不好,谁用谁知道

请求成面:减少HTTP网络请求数

  • 小图片或者字体内联(url-loader)

多页面应用 MPA

  • 每一次页面跳转,后台都会返回一个新的html,多页应用
  • 优势:SEO友好、每个页面是解耦的
  • 每个页面对应一个entry,一个html-webpack-plugin,(这种太麻烦了,每次新增都需要再配置一次)
  • 解决方案:
// 例如 ./src/index/index.js 与 ./src/search/index.js
// path: './src/*/index.js'
const setMPA = filenames => {
  const entry = {}, htmlWebpackPlugins = [];
  const entryFiles = glob.sync(path.join(__dirname, filenames))
  for(let item of entryFiles){
    // (/\/([a-z\_\$]+)\/index.js$/)
    const match = item.match(/src\(.*)/index\.js$/)
    const pageName = match && match[1];
    entry[pageName] = item

    htmlWebpackPlugins.push(
      new HtmlWebpackPlugin({
        template: `src/${pageName}/index.html`,
        filename: `${pageName}.html`,
        chunks: ["runtime", "common", pageName],
        minify: {
          // ..
        }
      })
    )
  }
  return {
    entry,
    htmlWebpackPlugins
  }
}
entry: entry
plugin: [//.....].concat(htmlWebpackPlugins)

source-map

  • 通过source-map 定位到源代码

  • 开发环境开启,线上环境关闭

  • eval:使用eval包裹模块代码

  • cheap:不包含列信息

  • inline:将.map作为DataURI嵌入,不单独生成.map文件

  • module:包含loader的source

  • 开发环境:建议使用

首先在源代码的列信息是没有意义的,只要有行信息就能完整的建立打包前后代码之间的依赖关系。因此不管是开发环境还是生产环境,我们都会选择增加cheap基本类型来忽略模块打包前后的列信息关联。

其次,不管在生产环境还是开发环境,我们都需要定位debug到最最原始的资源,比如定位错误到jsx,coffeeScript的原始代码处,而不是编译成js的代码处,因此,不能忽略module属性

再次我们希望通过生成.map文件的形式,因此要增加source-map属性

module.expors = {
  // 开发,因为eval的rebuild速度快,因此我们可以在本地环境中增加eval属性
  devtool: 'cheap-module-eval-source-map'
  // 生产
  devtool: 'cheap-module-source-map'
}

代码拆分

基础库分离

将react、react-dom基础包通过cdn引入,不打入到bundle中

  • 方式一: externals
// 1
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    'react-router-dom': 'ReactRouterDOM',
    mobx: 'mobx'
  },
}
2. 在html模版中 script标签引入对应的cdn地址
  • 方式二:html-webpack-externals-plugin(推荐使用)
1.在html模版中 script标签引入对应的cdn地址
2.plugins: [
  new HtmlWebpackPlugin(),
  new htmlWebpackExternalsPlugin({
    externals: [
      {
        module: 'react',
        entry: react的cdn地址,
        global: 'React'
      },
      {
        module: 'react-dom',
        entry: react-dom的cdn地址,
        global: 'ReactDOM'
      },
      {
        module: 'react-router-dom',
        entry: react-router-dom的cdn地址,
        global: 'ReactRouterDOM'
      },
      // ...
    ]
  })
]
  • 方式三:webpack4 替代 CommonsChunckPlugin插件
module.exports = {
  optimization: {
    minimize: true,
    runtimeChunk: {
      name: 'manifest'
    },
    splitChunks: {
      chunks: 'async', // async异步引入库进行分离(默认),initial同步引入库分离,all所有引入库分离
      minSize: 30000, // 抽离公共包最小的大小
      maxSize: 0,
      minChunks: 1, // 最小使用的次数 
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      name: true,
      cacheGroups: {
        // 提取基础库,不使用CDN的方式
        //commons: {
        //  test: /(react|react-dom|react-router-dom)/,
        //  name: "vendors",
        //  chunks: 'all'
        //},
        // 提取公共js
        commons: {
          chunks: "all", // initial
          minChunks: 2,
          maxInitialRequests: 5,
          minSize: 0,
          name: "commons"
        },
        // vendors: {
        //   test: /[\\/]node_modules[\\/]/,
        //   priority: -10
        // }
        // 合并所有css
        // styles: {
        //   name: 'style',
        //   test: /\.(css|scss)$/,
        //   chunks: 'all',
        //   minChunks: 1,
        //   enforce: true
        // }
      }
    }
  },
}

tree-shaking(静态分析,不是动态分析)

  • 代码不会被执行到,就不会打包到bound.js
  • 必须使用ES6的语法(import、export)才支持tree-shaking,commonjs方式不支持,
  • webpck默认支持,在.babelrc里面设置 modules: false即可,同时mode=production默认开启
  • 原理:利用ES6模块的特点:
    • 只能作为模块顶层的语句出现
    • import的模块只能是字符串常量 export function() {}
    • import binding 是 immutable 的 代码擦除: uglify阶段删除无用代码

Scope Hoisting

  • 大量函数闭包包裹代码,导致体积增大(模块越多越明显)
  • 运行代码时创建的函数作用域变多,内存开销变大
  • 被webpack转换后的模块会带上一层包裹,import会被转换成__webpack_require__
(function(module, __webpack_exports__,__webpack_require__){
  __webpack_require__.r(__webpack_exports__);
})()

  • scope hoisting原理:将所有模块的代码按照引用顺序放在一个函数作用域中,然后适当的重命名一些变量以防止变量名冲突

  • 对比,通过scope hoisting 可以减少函数声明代码和内存开销

  • 开启scop hoisting

    • webpack4 mode 为 production默认开启,必须是ES6语法,commonJS不支持
    • webpack3 增加插件 new webpack.optimize.ModuleConcatenationPlugin()

代码分割

  • splitChunck
  • 动态引用
    • 适用场景:抽离相同代码到一个共享块
    • 脚本懒加载,使得初始下载代码更小
  • 懒加载JS脚本方式
    • CommonJS: require.ensure
    • ES6: 动态import(需要babel支持,@babel/plugin-syntax-dynamic-import)
// 配置.babelrc
"plugins": [
  ["@babel/plugin-syntax-dynamic-import"],
]

dist代码通过window['webpackJsonp']来获取对应脚本

ESlint

//.eslint.js
/ 区分生产环境、开发环境
const _mode = process.env.NODE_ENV || 'production';

module.exports = {
	"env": {
		"browser": true,
		"es6": true,
		"node": true,
	},
	 "globals": {
    "$": true,
    "process": true,
    "dirname": true,
  },
	"parser": "babel-eslint",
	"extends": "eslint:recommended",
	"parserOptions": {
		"ecmaFeatures": {
			"jsx": true,
			"legacyDecorators": true
		},
		"ecmaVersion": 2018,
		"sourceType": "module"
	},
	"plugins": [
		"react"
	],
	"rules": {
		"no-console": "off",
		"no-debugger": _mode==='development' ? 0 : 2,
		"no-alert": _mode==='development' ? 0 : 2,
		// "no-multi-spaces": "error",
		"no-unused-vars": "off", // react中不适用
		"no-constant-condition": "off",
		"no-fallthrough": "off",
		// "keyword-spacing": ["error", { "before": true} ], // 不生效,先注释
		// "indent": [
		// 	"error",
		// 	2
		// ],
		"linebreak-style": [
			"error",
			"unix"
		],
		// "quotes": [
		// 	"error",
		// 	"single"
		// ],
		"semi": [0],
		"no-unexpected-multiline": 0,
		"no-class-assign": 0,
	}
};

检查eslint

  • 方式一: 安装husky,增加npm script,适合老项目
"scripts": {
  //"precommit": "eslint --ext .js --ext .jsx src/",
  "precommit": "eslint lint-staged", // 增量检查修改的文件  
},
"lint-staged": {
  //"src/**/*.js": [
  //  "eslint --ext .js --ext .jsx",
  //  "git add"
  //]
  "linters": {
    "*.[js,scss]": ["eslint --fix", "git add"]
  }
}
  • 方式二:webpack与eslint结合,新项目
rules: [
  {
    test: /\.jsx?$/,
    exclude: /node_modules/,
    use: ['babel-loader', 'eslint-loader']
  }
]

其实新项目中,可以将两种方式同时使用

优化命令行日志

  • 统计信息 stats,webpack属性,这种方式不好
    • error-only:值发生错误时输出
    • minimal:只在发生错误或有新的编译时输出
    • none:没有输出
    • normal:标准输出,默认
    • verbose:全部输出
// development
devServer: {
  // .....
  stats: 'errors-only'
}
// production
module.exports = {
  stats: 'errors-only'
}
  • stats结合friendly-errors-webpack-plugin(推荐)
plugins: [
  new FriendlyErrorsPlugin()
],
stats: 'errors-only'

构建异常和中断处理

  • wepback4之前的版本构建失败不会跑出错误码
  • node中的process.exit规范
    • 0 表示成功完成,回调函数中,err 为 null
    • 非0 表示执行失败,回调函数中,err 不为空,err.code就是传给exit的数字
  • 主动捕获错误,并处理构建错误
    • 写个插件,compiler 在每次构建结束后会出发done这个hook
plugins: [
  function() {
    // webpack3 this.plugin('done', (stats) => {})
    // webpack4
    this.hooks.done.tap('done', (stats) => {
      if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) {
        console.log('build error');
        // dosomething
        process.exit(1);
      }
    })
  }    
]      

❤️ 加入我们

字节跳动 · 幸福里团队

Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B