# webpack-native-book **Repository Path**: engineering-cavil/webpack-native-book ## Basic Information - **Project Name**: webpack-native-book - **Description**: webpack常见配置、高级使用、优化方法以及与vue、react开发环境搭建。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-09-19 - **Last Updated**: 2022-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## webpack5基础到进阶 ![](http://static.web.sdo.com/chuanshi/images/webpack.jpg) **学习目标:** - webpack的基础使用: - webpack的高级优化配置: - 从零搭建项目脚手架工具: - webpack中的loader和plugin原理: ### 基础使用: webpack中的基本配置,包含入口出口配置、样式文件处理、脚本文件处理以及图片资源处理,此外还包括预设设置和externals配置。 #### 1. 配置详解: webpack配置文件是一个对象配置,使用`module.exports`定义,常见的配置项目如下: ```js const { resolve } = require('path') module.exports = { // 打包环境 mode: 'development', // 入口文件 entry: './src/index-1.js', // 开发服务器环境配置 devServer: { }, // 出口文件配置 output: { }, // 预设配置 resolve: { }, // 模块配置 module: { }, // 插件配置 plugins: [ ], // 性能配置 performance: { }, // 优化配置 optimization: { } } ``` #### 2. 入口出口: 作为webpack的打包入口文件,可以是三种类型的配置: - 字符串: 单入口,打包形成一个chunk,输出一个bundle文件,chunk的名称默认是main,打包的文件名称默认为main.js - 数组: 多入口,所有文件最终只会形成一个chunk,输出只有一个bundle文件。 - 对象: 多入口,配置多对key-value值,有几个入口文件就会形成多个chunk,输出多个bundle文件。 ```javascript // 字符串入口 const {resolve} = require('path') module.exports={ mode:'production', entry:'./src/index-1.js', output:{ clean:true, path:resolve(__dirname,'dist'), filename:'js/[name]-bundle.js' } } // 数组入口 const { resolve } = require('path') module.exports = { mode: 'production', entry: ['./src/index-2.js', './src/base.js'], output: { clean: true, path: resolve(__dirname, 'dist'), filename: 'js/[name]-bundle.js' } } // 对象入口 const { resolve } = require('path') module.exports = { mode: 'production', entry: { index:{ import:'./src/index-3', dependOn:'vendor' }, vendor:'lodash' }, output: { clean: true, path: resolve(__dirname, 'dist'), filename: 'js/[name]-bundle.js' } } ``` output作为定义打包文件的配置,可以定义打包文件的存放位置,文件名称等信息。此外webpack5将文件的处理交给了output定义,不用借助loader处理。 - filename: 定义打包文件名称。 - path: 定义打包文件的存放位置。 - clean: 定义打包前是否清理打包文件 - environment: 定义打包的文件是否包含箭头函数之类的信息。 - assetModuleFilename: 定义打包之后的文件存放位置,比如图片、字体等文件,需要结合module使用。 ```javascript module.exports={ mode:'production', entry:{ common:{ import:'./src/common.js', dependOn:'vendor' }, index:{ import:'./src/index.js' }, vendor:'lodash' }, output:{ filename:'js/[name].js', clean:true, assetModuleFilename:'images/[name][ext]', environment:{ arrowFunction:false } } } ``` #### 3. 环境模式: webpack自版本4之后,新增了mode选项,用于定义开发环境,有development和production两个选项,在production环境下js默认会被压缩。 ```js // 开发环境配置 const {resolve} = require('path') module.exports={ mode:'development' } // 线上环境配置 const {resolve} = require('path') module.exports={ mode:'production' } ``` #### 4. 样式处理: 在开发环境和生成环境下,样式的处理不尽相同。 开发环境下,只需将样式注入到页面的style标签中,而生成环境下我们要单独分离出css文件,此外还要考虑css的兼容性(使用postcss)。 - **less的配置:** 依赖less和less-loader - **sass的配置:** 依赖sass和sass-loader - **postcss的配置:** postcss依赖于`postcss postcss-loader postcss-preset-env`。 - **样式抽离:** 依赖`mini-css-extract-plugin`对css文件进行抽取。 - **样式压缩:** 依赖`css-minimizer-webpack-plugin`对css文件进行压缩。 postcss在使用时,要定义`postcss.config.js`文件,并定义`browserslist`,`browserslist`可以直接在`package.json`文件中定义。 ```js const { resolve } = require('path') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CssMiniMizerPlugin = require('css-minimizer-webpack-plugin') module.exports = { mode: 'production', entry: './src/index.js', output: { clean: true, path: resolve(__dirname, 'dist'), filename: 'js/[name]-bundle.js', assetModuleFilename: 'images/[name][ext]', environment: { arrowFunction: false } }, module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] }, { test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] }, { test: /\.s[ac]ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] } ] }, plugins:[ new MiniCssExtractPlugin({ filename:'style/[name].css' }), new CssMiniMizerPlugin() ] } ``` #### 5. 资源处理: webpack5摒弃了之前版本借助loader处理图片等资源的方法,直接使用内置函数处理。 type的类型有以下几种: - **asset/resource:** 将文件打包输出并导出URL,类似于file-loader。 - **asset/inline:** 导出一个资源的64位data URL,编码到bundle中输出。 - **asset/source:** 导出资源的源代码,类似于raw-loader。 - **asset:** asset在导出一个data URL和发送一个单独的文件之间做选择,之前通过url-loader+limit属性实现。 ```js module.exports={ mode:'production', entry:'./src/index.js', output:{ filename:'js/[name].js', clean:true, assetModuleFilename:'assets/[name][ext]', environment:{ arrowFunction:false } }, module:{ rules:[ { test:/\.(png|jpg|jpeg)$/, type:'asset/resource', generator:{ filename:'images/[name].[ext]' } } ] } } ``` #### 6. 脚本处理: 脚本的处理,主要借助于`babel`进行配置: - **babel和babel-loader:** 主要借助`@babel/core @babel/preset-env babel-loader`。 - **babel-preset:** 预设配置可以配置在`babel.config.js`中,在该文件中也可以配置其他插件plugins。 - **core-js:** core-js主要是为了解决js兼容性问题,可以在预设中配置`useBuiltIns:usage`完成按需加载。 ```js // webpack.config.js const path = require('path') module.exports={ mode:'production', entry:'./src/index.js', output:{ path:path.resolve(__dirname,'build'), filename:'js/[name]-bundle.js', clean:true, environment:{ arrowFunction:false } }, module:{ rules:[ { test:/\.js$/, exclude:/node_modules/, use:'babel-loader' } ] } } // babel.config.js module.exports={ presets:[['@babel/preset-env',{ useBuiltIns:"usage", corejs:3 }]] } ``` #### 7. 页面处理: `html-webpack-plugin`主要用来处理html页面,用来将页面和脚本、css文件进行连接。 ```js const { resolve } = require('path') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const CssMiniMizerPlugin = require('css-minimizer-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'production', entry: './src/index.js', output: { clean: true, path: resolve(__dirname, 'dist'), filename: 'js/[name]-bundle.js', publicPath:'http://static.web.com/', assetModuleFilename: 'images/[name][ext]', environment: { arrowFunction: false } }, module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] }, { test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] }, { test: /\.s[ac]ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] }, { test:/\.(png|jpg|jpeg|gif)$/, type:'asset/resource' }, { test:/\.js$/, exclude:/node_modules/, use:'babel-loader' } ] }, plugins:[ new HtmlWebpackPlugin({ template:resolve(__dirname,'./src/index.html'), minify:false, hash:true, inject:'body' }), new MiniCssExtractPlugin({ filename:'style/[name].css' }), new CssMiniMizerPlugin() ] } ``` #### 8. devtool设置: sourcemap是提供源代码到构建后代码映射技术,能够方便我们快速定位到源代码的位置。 sourcemap是定义在**devtool**上面,常见的值有以下几种: - **inline-source-map:** 内连模式,sourcemap直接在文件中,只生成一个source-map - **hidden-source-map:** 提示错误代码错误原因,但是没有错误位置,不能追踪源代码错误,只能提示构建后代码的错误位置。 - **eval-source-map:** 没有生产具体的sourcemap文件,每一个文件都生成对应的source-map,sourcemap依靠eval生成 - **cheap-source-map:** 错误代码准确信息和源代码错误位置,缺点是只能精确到行。 - **cheap-module-source-map(开发模式下):** 生成外部sourcemap文件,能定位到错误代码准确位置和源代码错误位置。此外还能将loader中的source map加入进来。 ```js // 开发环境下 module.exports={ devtool:'cheap-module-source-map' } // 生成环境下 module.exports={ devtool:'source-map' } ``` #### 9. devServer配置: 开发环境下,我们需要配置`mode=development`,并且配置`devServer`选项。 在devServer中,可以配置以下参数: - **static:** 热启动启动的目录, - **hot:** 是否开启热更新。 - **open:** open定义服务是否自动启动。 - **port:** 服务启动的端口号。 - **headers:** 服务启动之后,自动发送的head字段。 - **proxy:** 开发中启动的代理。 - **watchFiles:** 检测指定文件,对于html文件这项功能很重要,可以在修改html文件后自动刷新页面。 ```js const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'development', entry: './src/index.js', devtool:'cheap-module-source-map', devServer:{ port:3500, hot:true, open:true, headers:{ "X-POWER":"cowen" }, watchFiles:[resolve(__dirname,'./src/index.html')] }, module: { /.../ }, plugins:[ new HtmlWebpackPlugin({ template:resolve(__dirname,'./src/index.html'), minify:false, hash:true, inject:'body' }) ] } ``` #### 10. resolve配置: 在路径引入中,可能路径的层级比较深,或者我们想定义默认文件后缀,可以在resolve中定义。 - **alias:** 用来解析路径别名,在定义路径别名之后,引入文件时直接使用路径别名即可。 - **extensions:** 定义路径默认后缀,默认值为`.js`,可以定义成数组,查找顺序从数组的第一项开始。 ```js const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'development', entry: './src/index.js', devtool:'cheap-module-source-map', devServer:{ port:3500, hot:true, open:true }, resolve:{ extensions:['.js','.css'], alias:{ 'styles':resolve(__dirname,'./src/styles/') } }, module: { /******/ }, plugins:[ new HtmlWebpackPlugin({ template:resolve(__dirname,'./src/index.html'), minify:false, hash:true, inject:'body' }) ] } ``` #### 11. externals设置: 对于一些常用的第三方框架,可能需要直接使用其CDN地址,但是在js文件中仍需要将其引入,这个时候就需要配置externals忽略对这些第三方框架的打包。 ```js const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'development', entry: './src/index.js', devtool:'cheap-module-source-map', devServer:{ port:3500, hot:true, open:true }, resolve:{ extensions:['.js','.css'], alias:{ 'styles':resolve(__dirname,'./src/styles/') } }, externals:{ 'react':'React', 'react-dom':'ReactDOM' }, module: { /******/ }, plugins:[ new HtmlWebpackPlugin({ template:resolve(__dirname,'./src/index.html'), minify:false, hash:true, inject:'body' }) ] } ``` #### 12. 环境区分: webpack在打包时分为开发环境和生产环境,因此对应不同的环境配置不同的配置文件。可以借助`webpack-merge`对不同的配置文件进行合并,这样便会将公共配置进行提取,而开发环境下和生产环境下配置不同。 借助`cross-env`配置不同的环境变量,将环境变量传至webpack的配置文件中。 ```js // package.json配置 "scripts": { "dev": "npx cross-env NODE_ENV=development webpack serve", "prod": "npx cross-env NODE_ENV=production webpack" } // webpack.config.js文件 const NODE_ENV = process.env.NODE_ENV; const CommonConfig={}; // 公共配置 const DevConfig={}; // 开发配置 const ProdConfig={}; // 生产配置 module.exports = NODE_ENV === 'development' ? merge(DevConfig, CommonConfig) : merge(ProdConfig, CommonConfig) ``` ### 性能优化: 利用webpack中的优化选项对开发环境下的打包速度、对打包文件的体积进行优化。 #### 1. 模块热更新: 模块热更新也可以认为是一种打包优化的方法,模块热更新可以规避一些没有修改的文件打包。 模块热更新目前只对css文件有效(style-loader中实现模块热更新),对于html文件要实现刷新功能(不是模块热更新),可以将html文件添加到入口文件中。而对于js文件的HMR,只能在代码中实现。 对于react和vue而言,其本身已实现了js文件的模块热更新,因此在真正的开发过程中,我们并不需要去单独配置模块热更新。 ```js // 开启模块热更新 devServer: { port: 3600, hot: true, // 开启模块热更新 open: true, watchFiles: [resolve(__dirname, './src/index.html')] }, // 单独配置js文件的模块热更新 if(module.hot){ module.hot.accept('./print.js', function() { console.log('Accepting the updated printMe module!'); printMe(); }) } ``` #### 2. 缓存的使用: 每次打包时js文件都要经过eslint检查和babel编译,速度比较慢,我们可以缓存之前的Eslint检查和Babel编译结果,这样下一次的打包速度就会变快。 - **babel-loader定义cacheDirectory:** 可以使用babel的打包缓存优化打包速度和打包效率。 - **hash、contenthash和chunkhash缓存文件:** hash、contenthash和chunkhash都可以用来清除文件缓存,但是不同的值其对应的含义也不一样。 - **hash:** 每次webpack构建时都会生成唯一的hash值。 - **chunkhash:** 根据entry中定义的chunk,如果打包资源来自于同一个chunk,则hash值是相同的。 - **contenthash:** 根据文件的内容生成hash值,不同文件的hash值一定不一样。 ```js // Babel缓存 module: { rules: [ { { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', options:{ cacheDirectory:true, // 开启babel缓存 cacheCompression:false // 关闭缓存文件压缩 } } } ] } ``` #### 3. oneOf、include: webpack中配置的loader其实是遍历的过程,配置oneOf可以保证一种文件只能有一种loader去处理。 项目中依赖的一些第三方包并不需要去打包,可以借助exclude去除这些第三方包路径。 exclude和include的两者只能选择一个。 ```js module: { rules: [ { oneOf:[ { test: /\.css$/, include:resolve(__dirname,'./src/'), use: ['style-loader', 'css-loader'] } ] } ] } ``` #### 4. tree shaking: tree shaking其实质就是去除无用代码,减少代码体积,tree shaking开启的前提有两个: - **必须使用ES6模块** es6中的具名导入,其他没有使用的模块不会被打包。 - **开启production环境** 开启production环境下,webpack会自动对js文件进行分割。 #### 5. 代码分割: 代码分割是将一些公共使用的chunk进行单独打包出来,这样会减少文件的体积。常见的code split分割方法有以下几种: - **entry中配置多入口:** 配置多入口,每个入口会被打包对应的bundle文件。 - **splitChunks配置:** 在**optimization**中配置**optimization**,打包时会自动分析公共js代码,将公共js部分打包成单独打包: ```js // 第一种:配置多入口 entry:{ base:'./src/base.js', home:'./src/home.js' } // 第二种:使用optimization中配置optimization optimization:{ splitChunks:{ chunks:'all' } } ``` splitChunks作为代码分割的配置项,有众多的配置选项可供参考: - **chunks** 决定要提取那些模块。默认是`async`。 - **async** 只提取异步加载的模块出来打包到一个文件中。 - **initial** 提取同步加载和异步加载模块,如果xxx在项目中异步加载了,也同步加载了,那么xxx这个模块会被提取两次,分别打包到不同的文件中。 - **all** 不管异步加载还是同步加载的模块都提取出来,打包到一个文件中。 - **minSize** 规定被提取的模块在压缩前的大小最小值,单位为字节,默认为30000,只有超过了30000字节才会被提取。 - **maxSize** 把提取出来的模块打包生成的文件大小不能超过maxSize值,如果超过了,要对其进行分割并打包生成新的文件。单位为字节,默认为0,表示不限制大小。 - **minChunks** 表示要被提取的模块最小被引用次数,引用次数超过或等于minChunks值,才能被提取。 - **maxAsyncRequests** 最大的按需(异步)加载次数,默认为 6。 - **automaticNameDelimiter** 打包生成的js文件名的分割符,默认为`~`。 - **cacheGroups** 核心重点,**配置提取模块的方案**。里面每一项代表一个提取模块的方案。 ```js optimization: { splitChunks: { chunks: 'all', cacheGroups:{ common:{ minSize:0, minChunks:2, priority:-20, reuseExistingChunk:true }, lib:{ test:/[\\/]node_modules[\\/]/, name:'vendor-lib', priority:10 } } } } ``` #### 6. 代码懒加载: 很多模块中方法只有在触发某个动作时才会执行,比如点击按钮触发请求等,但是很多时候我们是先引入再执行。 **懒加载的含义就是只有在触发动作时才会引入和执行**。懒加载在单页面的路由跳转中被大量应用。 ```js import './styles/App' const button = document.getElementById('btn'); button.addEventListener('click', () => { import(/*webpackChunkName:'base',webpackPrefetch:true*/'./js/load.js').then(res => { const result = res.default(1, 2, 56, 99); console.log(result) }) }, false) ``` #### 7. DLL第三方库打包: 对于第三方类库,如vue、react等,本身不参与业务,我们可以将这些体积较大的框架包提前打包,打包只处理业务打包。 第三方库打包打包步骤如下: - **DllPlugin:** DllPlugin生成manifest.json - **DllReferencePlugin:** DllReferencePlugin的作用就是读取manifest.json中的第三方库引用至项目中。 - **add-asset-html-webpack-plugin:** add-asset-html-webpack-plugin可以将打包生成的dll文件注入到页面中,省去了手动添加。 第三方库打包步骤如下: - 使用`webpack.DllPlugin`定义静态库打包: - 使用`webpack.DllReferencePlugin`引入`manifest.json`: - 使用`add-asset-html-webpack-plugin`将dll文件中的类库引入到html文件中。 ```js // dll 文件 const { resolve } = require('path'); const webpack = require('webpack'); module.exports = { mode: 'production', entry: { React: 'react', ReactDOM: 'react-dom' }, output: { filename: '[name].js', path: resolve(__dirname, 'dll'), library: '[name]' }, plugins: [ new webpack.DllPlugin({ name: '[name]', path: resolve(__dirname, './dll/manifest.json') }) ] } // prod 文件 const { resolve } = require('path') const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin') module.exports = { mode: 'production', entry: './src/index.js', output: { clean: true, filename: 'js/[name].js', path: resolve(__dirname, 'dist'), environment: { arrowFunction: false, const: false } }, plugins: [ new webpack.DllReferencePlugin({ manifest: resolve(__dirname, './dll/manifest.json') }), new HtmlWebpackPlugin({ template: resolve(__dirname, './src/index.html'), minify: false, hash: true, inject: 'body' }), new AddAssetHtmlWebpackPlugin({ filepath: resolve(__dirname, './dll/React.js'), outputPath: './lib/', publicPath: './lib/' }), new AddAssetHtmlWebpackPlugin({ filepath: resolve(__dirname, './dll/ReactDOM.js'), outputPath: './lib/', publicPath: './lib/' }) ] } ``` ### 搭建实例: #### 1. React开发环境搭建: react开发环境下,`react-refresh/babel`可以实现react组件的局部更新功能。在babel下需要配置plugins中配置react-refresh/babel。 ```js { test: /\.jsx?$/, include: resolve(__dirname, './src/'), loader: 'babel-loader', options: { cacheDirectory: true, cacheCompression: false, plugins: NODE_ENV === 'development' ? ['react-refresh/babel'] : [] } } ``` #### 2. Vue2开发环境搭建: vue开发环境下,使用`vue-loader`和`vue-template-compiler`对vue文件进行打包。 ```js const VueLoaderPlugin = require('vue-loader/lib/plugin') // webpack配置文件 module:{ { test:/\.vue$/, use:'vue-loader' } }, plugins:[ new VueLoaderPlugin() ] ``` ### 原理剖析: #### 1. loader的实质: loader的本质就是函数,函数中第一个参数即是文件的内容。 ```js // loader的引入 module:{ rules:[ { test:/\.js$/, loader:resolve(__dirname,'./loader/index.js') } ] } // loader的定义 module.exports=function(content){ console.log(content); return `${content.replace(' ','AAAA')}` } ``` #### 2. loader的传参: loader中参数通过schema进行校验,只有通过schema校验的参数才会被成功定义。 ```js // schema定义 { "type":"object", "properties":{ "publicPath":{ "type":"string" } }, "additionalProperties":false } // loader定义 const schema = require('./schema.json') module.exports=function(content){ const options = this.getOptions(schema); return content.replace(" ",options.publicPath); } ```