ESModules存在环境兼容问题
模块文件过多,网络请求频繁
所有的前端资源都需要模块化
需求,将所有ES6的代码编译成ES5或兼容性更好的代码,并且将转换后的代码打包成一个文件,并且支持不同类型的资源模块;前面两个需求可以使用前面学习的构建系统glup等,但是最后一个需求需要我们学习新的模块化打包工具;
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
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);
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会根据配置找到其中一个文件作为入口,顺着入口文件的代码,根据import或者require等语句,解析推断出文件所依赖的资源模块,分别去解析每个资源模块对应的依赖,最后形成整个项目的依赖树,webpack递归这个依赖树找到每个节点依赖的资源文件,在根据配置文件中rules属性找到资源文件对应的loader,最后将加载到的结果放入指定的出口文件中,从而实现整个项目的打包;如果没有loader那么webpack只能是一个用来打包和合并js代码的工具;
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' ] } ] } }
Plugin用来解决其他自动化工作,例如清除dist目录,拷贝静态资源文件,压缩输出代码等;
绝大多数插件导出的是一个类,在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
我们之前采用硬编码的方式,自己新建了html文件,然后引入了dist下打包后的bundle.js文件,这样存在很多的问题,比如引入js文件的目录需要我们手动修改;
使用插件以后同样会导出一个html文件,而且是动态引入的js文件;
原先我们在output属性中设置了publicPath来告诉webpack我们打包后的公共目录,现在自动生成了html文件则不需要配置publicPath属性了;
简单html文件我们可以通过配置选项来指定参数,复杂的html文件我们可以指定模板template,模板的html文件支持lodash模板引擎语法,通过htmlWebpackPlugin.options来获取配置选项
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Webpack</title> </head> <body> <div class="container"> <h1><%= htmlWebpackPlugin.options.title %></h1> </div> </body> </html>
使用多个实例对象可用于导出多个html文件;
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist'), publicPath: 'dist/' }, plugins: [ // 用于生成 index.html new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', meta: { viewport: 'width=device-width' }, template: './src/index.html' }), // 用于生成 about.html new HtmlWebpackPlugin({ filename: 'about.html' // 指定生成的html文件名 }) ] }
**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' ]) ]
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打包 =》 运行应用 =》 刷新浏览器 的方式来开发
自动编译: 我们可以使用watch工作模式来监听文件的变动,重新编译;
watch模式会自动编译代码,然后我们需要手动刷新浏览器才可以看到最新的结果
**自动刷新:**BrowserSync插件可以帮我们实现自动刷新的功能;
webpack自动编译到我们的dist目录中,被browser-sync拦截到变化然后自动刷新
**但是!**这么做的话操作麻烦,并且需要反复读取磁盘效率低下,要是有更好的工具能替代就完美了!
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 } } },
映射转换之后的代码与源代码之间的关系
在源文件中的最后使用注释 //# sourceMappingURL=bundle.js.map 来引入source Map
webpack现在支持12种source Map模式,这里只给推荐;
开发环境: cheap-module-eval-source-map
生产模式: none 或者 nosources-source-map
前面我们配置了自动刷新,但是每次自动刷新都会丢失状态,体验不友好;
问题核心: 自动刷新导致的页面状态丢失;
开启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并不是开箱即用的,需要手动处理模块热替换逻辑;
module.hot提供了热更新的api,使用module.hot.accept来注册热更新
当使用了module.hot.accept来注册了模块的热更新以后,模块发生改变就不会触发自动刷新了
也可以通过配置文件中hotOnly属性来配置,无论该模块是否配置了热更新,都不会刷新浏览器,这样就可以看到错误信息了
// webpack.congfig.js devServer: { // hot: true, hotOnly: true },
使用了HMR提供的API但是没有开启HMR的选项,则会报错,所以最外面加一个if (module.hot)判断是否存在
为了实现热更新,我们在业务代码中写了很多与业务无关的代码,但是在生产环境中,我们去除了HMR插件,false以后并不会影响生产环境的状态;
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 内部自动处理更新样式,所以不需要手动处理样式模块 }
生产环境与开发环境的差别大,开发环境注重开发效率,而生产环境注重运行效率;
webpack4提供了mode模式,在生产环境下为我们提供了很多默认的配置;
要使用不同环境下的配置有以下方法
配置文件根据环境不同导出不同配置;
配置文件导出的函数中,会传入两个参数,其中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 }
一个环境对应一个配置文件,通常会配置三个文件,公共配置、开发环境配置、生产环境配置;
对于配置文件的合并,我们一般会用到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() ] })
为代码注入全局成员,在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' " }) ] }
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' }] ] } } } ] },
副作用:模块执行时除了导出成员之外所作的事情(比如在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()) }) }
将Css文件单独提取一个文件中,推荐当css文件大于150kb时在考虑使用该插件,否则会多一次css的请求;
const MiniCssExtractPlugin = require('mini-css-extract-plugin') ... module: { rules: [ { test: /\.css$/, use: [ // 'style-loader', // 将样式通过 style 标签注入 MiniCssExtractPlugin.loader, 'css-loader' ] } ] },
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,当资源文件发生改变时,文件名称也会发生变化,对于后端而言就是全新的请求,这样我们可以把缓存策略的时间设置的比较长,不用担心更新不及时的问题;
配置中的所有filename等路径都支持hash模式
hash有以下三种模式:
filename: ‘[name]-[hash].bundle.css’;
hash模式,当项目中任何一个地方发生改变,所有文件的hash都会发生改变
filename: ‘[name]-[chunkhash].bundle.css’;
chunkhash模式,chunk级别的hash,项目中一路的chunk的hash值是相同的(项目中使用动态导入时会形成多路chunk),改变时只会影响当前路的hash值
filename: ‘[name]-[contenthash].bundle.css’;
contenthash模式,文件级别的hash,不同的文件就有不同的hash值;
默认hash长度为20,可以手动指定长度,个人认为8位左右就可以了
filename: ‘[name]-[contenthash:8].bundle.css’
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' }) ] }