webpack的基本使用1
可参考:https://segmentfault.com/a/1190000006178770#articleHeader2
1、webpack的概念
本质上,webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,webpack 从命令行或配置文件中定义的一个模块列表开始,处理你的应用程序,它会递归地在内部构建一个依赖关系图,其中包含该应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle,通常只有一个 - - 可由浏览器加载。
在 webpack 看来,前端所有的资源文件(js、json、css、img、less...)都会作为模块处理。它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源(bundle)。webpack 本身只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。
webpack的五个核心概念:入口(Entry)、输出(output)、loader、插件(plugin)、模式(mode)。
2、安装webpack
全局安装命令如下,但我们并不推荐全局安装 webpack,因为这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中,可能会导致构建失败。
npm i webpack -g
本地安装:
npm i webpack -D
新的版本比如 webpack4.xx+ 还需要安装webpack-cli,而旧的版本比如3.5.3就不需要安装webpack-cli(此工具用于在命令行中运行 webpack),应该是因为新的版本webpack和webpack-cli分离了,而旧的版本中webpack包括了webpac-cli。
安装 webpack-cli:
npm install webpack-cli -g
2.1、版本
我们并不建议安装最新版本的 webpack,在安装这些最新体验版本时要小心,因为它们可能仍然包含 bug,因此不应该用于生产环境。
查看webpack版本命令如下:
npm info webpack
npm info webpack可查看webpack 相关信息,第一条就是版本号:
3、使用webpack
先使用 npm init 初始化一个项目,建立如下目录结构:
src/index.js 文件代码:
import _ from 'lodash'; //lodash是一个 JavaScript 实用工具库 function component() { const element = document.createElement('div'); //引入lodash后就可以使用全局变量 _,如下 _.join() 只是一个操作数组的方法 element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } document.body.appendChild(component());
安装依赖:
npm install webpack webpack-cli --save-dev
npm install --save lodash
dist/index.html 文件代码:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>起步</title> <!-- <script src="https://unpkg.com/lodash@4.17.20"></script> --> </head> <body> <!-- <script src="./src/index.js"></script> --> <script src="main.js"></script> </body> </html>
如上,使用 webpack 构建后不需要手动引入 lodash,并且也不需要引入 index.js ,只需要引入构建后生成的 main.js 文件即可。
执行 npx webpack 命令开始构建:
npx webpack
该命令会调用 webpack,webpack 默认会以当前项目根目录下的 src/index.js 文件为入口,生成一个 bundle 在 dist 文件夹下,并且命名为 main.js 。所以构建过后,我们只需要在 index.html 文件中引入 main.js 文件即可。
3.1、使用配置文件(webpack.config.js)
Webpack拥有很多其它的比较高级的功能,这些功能其实都可以通过命令行模式实现,但非常不方便,所以定义一个配置文件非常有必要。这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。
我们新建一个 webpack.config.js 文件:
webpack.config.js 文件代码:
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), //path:打包得到的结果文件的输出路径。当前代码是得到了当前文件目录下的dist文件夹路径。还可以配置 publicpath,这个是资源的引用路径。 __dirname - 当前文件目录 path.resolve() - 路径拼接方法 }, };
现在我们通过新的配置文件再次执行构建:
npx webpack --config webpack.config.js
如果 webpack.config.js
存在,其实 webpack
命令就会自动默认选择使用它。但是我们也可以使用 --config
选项来使用任何名称的配置文件。
3.2、使用npm scripts
调整 package.json 文件,添加一个 npm script:
{ "name": "webpackdemo01", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, "author": "", "license": "ISC", "devDependencies": { "webpack": "^5.26.1", "webpack-cli": "^4.5.0" }, "dependencies": { "lodash": "^4.17.21" } }
现在,可以使用 npm run build
命令,来替代我们之前使用的 npx
命令。使用 npm scripts
,我们可以像使用 npx
那样通过模块名来直接引用本地安装的 npm packages。其实跟使用 npx 是一样的。
4、加载css等资源
模块 loader 可以链式调用。链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。
应保证 loader 的先后顺序,比如我们应该保证'style-loader'
在前,而 'css-loader'
在后。如果不遵守此约定,webpack 可能会抛出错误。
4.1、加载css资源
为了在 JavaScript 模块中 import
一个 CSS 文件,你需要安装 style-loader 和 css-loader,并在 module
配置 中添加这些 loader:
npm install --save-dev style-loader css-loader
webpack.config.js
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.css$/i, use: ['style-loader', 'css-loader'], }, ], }, };
4.1.1、css模块化
CSS modules
意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,id 名默认都只作用于当前模块。
Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。具体的代码如下
module.exports = { ... module: { rules: [ { test: /(\.jsx|\.js)$/, use: { loader: "babel-loader" }, exclude: /node_modules/ }, { test: /\.css$/, use: [ { loader: "style-loader" }, { loader: "css-loader", options: { modules: true, // 指定启用css modules localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式 } } ] } ] } };
由此通过 import 引入的css 文件只会作用于该模块中,而且只有通过先引入再使用才能起作用,相当于把 css 文件看成了模块。比如:
创建一个Greeter.css
文件来进行一下测试
/* Greeter.css */ .root { background-color: #eee; padding: 10px; border: 3px solid #ccc; }
import React, {Component} from 'react'; import config from './config.json'; import styles from './Greeter.css';//导入一个 Greeter.css 样式,但是不会自动起作用,更不会影响其他 JS 模块,只有自动引入再使用才能起作用。 class Greeter extends Component{ render() { return ( <div className={styles.root}> //使用cssModule添加类名的方法,这里就是使用引入的 css 文件 {config.greetText} </div> ); } } export default Greeter
如果没有使用 CSS Module 的话,在模块中引入 .css 文件,那么该 .css 文件将作用于全部模块而不仅仅是引入的模块(相当于直接在html文件的style标签中引入了样式,而且类名没有更改)
4.1.2、css处理平台PostCSS(使css代码兼容各种浏览器)
使用PostCSS来为CSS代码自动添加适应不同浏览器的CSS前缀。首先安装postcss-loader
和 autoprefixer
(自动添加前缀的插件)
npm install --save-dev postcss-loader autoprefixer
接下来,在webpack配置文件中添加postcss-loader
,在根目录新建postcss.config.js
,并添加如下代码之后,重新使用npm start
打包时,你写的css会自动根据Can i use里的数据添加不同前缀了。
//webpack.config.js module.exports = { ... module: { rules: [ { test: /(\.jsx|\.js)$/, use: { loader: "babel-loader" }, exclude: /node_modules/ }, { test: /\.css$/, use: [ { loader: "style-loader" }, { loader: "css-loader", options: { modules: true } }, { loader: "postcss-loader" } ] } ] } }
// postcss.config.js module.exports = { plugins: [ require('autoprefixer') ] }
5、HtmlWebpackPlugin插件
这个插件的作用是依据一个简单的index.html
模板,生成一个自动引用你所有打包后的JS文件的新index.html
。这在每次生成的js文件名称不同时非常有用(比如如果添加了hash
值,每次生成的JS文件名称都不一样,如果不使用该模板,那么每次编译都需要修改html文件中引入的JS文件名称)。
npm install --save-dev html-webpack-plugin
这个插件将会自动生成一个新的 HTML 文件并自动引入生成的所有的 JS 文件。示例:
在app目录下,创建一个index.tmpl.html
文件模板,这个模板包含title
等必须元素,在编译过程中,插件会依据此模板生成最终的html页面,会自动添加所依赖的 css, js,favicon等文件,index.tmpl.html
中的模板源代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Webpack Sample Project</title> </head> <body> <div id='root'> </div> </body> </html>
更新webpack
的配置文件,新建一个build
文件夹用来存放最终的输出文件,使用插件
const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件 output: { path: __dirname + "/build", filename: "bundle.js" }, devtool: 'eval-source-map', devServer: { contentBase: "./public",//本地服务器所加载的页面所在的目录 historyApiFallback: true,//不跳转 inline: true//实时刷新 }, module: { rules: [ { test: /(\.jsx|\.js)$/, use: { loader: "babel-loader" }, exclude: /node_modules/ }, { test: /\.css$/, use: [ { loader: "style-loader" }, { loader: "css-loader", options: { modules: true } }, { loader: "postcss-loader" } ] } ] }, plugins: [new HtmlWebpackPlugin({ template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数 }) ], };
再次执行npm start
会发现在build文件夹下面生成了bundle.js
和index.html
。生成的新的 HTML 文件将自动引入所有的资源。
6、使用 Source Maps(定位报错文件)
如果原文件中代码出错,通过打包后在浏览器中很难找到对应的出错的地方,此时的出错提示并不一定会指出正确的代码出错文件名和位置。而Source Maps能
解决这个问题。
通过简单的配置,webpack
就可以在打包时为我们生成的source maps
,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。
在webpack
的配置文件中配置source maps
,只需要配置devtool
module.exports = { devtool: 'eval-source-map', entry: __dirname + "/app/main.js", output: { path: __dirname + "/public", filename: "bundle.js" } }
devtool 字段有很多选项,网上看到都推荐开发时使用:cheap-module-eval-source-map 字段,生产时使用:cheap-module-source-map。
但是开发时使用cheap-module-eval-source-map 字段报的错误是在编译后的文件中
我觉得这非常有问题啊,cheap-module-eval-source-map 和 eval-source-map 字段在浏览器中都不能很好地显示错误的位置,而 cheap-module-source-map 和 source-map 字段就能很好显示错误出现的文件名和行数。
所以我建议使用:cheap-module-source-map
官网建议开发使用:eval-source-map,但是这个有些错误在浏览器中并不能很好地展示出来,而是要展开才行,错误文件在第一行。生产时使用:none
7、构建本地服务器(修改文件后自动编译)
devserver 能让浏览器监听你的代码的修改,并自动刷新显示修改后的结果。
Webpack
提供一个可选的本地开发服务器,该服务器基于node.js构建。它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖
npm install --save-dev webpack-dev-server
配置webpack.config.js 文件:
module.exports = { devtool: 'eval-source-map', entry: __dirname + "/app/main.js", output: { path: __dirname + "/public", filename: "bundle.js" }, devServer: { contentBase: "./public",//本地服务器所加载的页面所在的目录 historyApiFallback: true,//不跳转 inline: true//实时刷新 } }
在package.json
中的scripts
对象中添加如下命令,用以开启本地服务器:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack", "server": "webpack-dev-server --open" }
在终端中输入npm run server
即可在本地的8080
端口查看结果。
webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。也就是说,在启动webpack-dev-server后,你在目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存当中。因此使用webpack-dev-server进行开发的时候是看不到编译后的文件。
8、代码分离(提取公共模块到单独的bundle)
8.1、使用SplitChunksPlugin插件
SplitChunksPlugin插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
该插件可以将node_modules中代码单独打包成一个chunk最终输出。在多入口配置时,该插件会自动分析多入口chunk中有没有公共的依赖文件,如果有会打包成单独一个chunk。
比如,index.js 和 another-module.js 都依赖 lodash,配置文件 webpack.config.js 如下:
使用 optimization.splitChunks 配置选项之后,插件将会将 lodash 分离到单独的 chunk。编译后的 index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块 lodash。
8.2、使用 CommonChunkPlugin 插件
参考:https://www.webpackjs.com/guides/code-splitting/
如果多个入口文件都共同引入了同一个模块,正常编译的话webpack会将依赖的模块编译进每个出口文件中,而 CommonsChunkPlugin
插件可以将多个入口文件共同依赖的公共模块提取到一个新生成的出口文件中(chunk)。
const path = require('path'); const webpack = require('webpack'); const HTMLWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, plugins: [ new HTMLWebpackPlugin({ title: 'Code Splitting' }), new webpack.optimize.CommonsChunkPlugin({ name: 'common' //指定公共 bundle 的名称。 }) ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } };
如此生成的多个出口文件将减轻了大小。使用 HTMLWebpackPlugin 插件将生成的模块自动引入 HTML 文件中。
8.2.1、将第三库的代码提取到单独的出口文件中
代码中引入的第三方依赖库(例如 lodash
或 react、vue
)它们很少像本地的源代码那样频繁修改。因此将它们提取到一个单独的出口文件中,并且用固定的一个名字命名,这样浏览器可以只用缓存的文件,减少请求。
通过指定 entry
配置中一个未用到的名称,去重插件会自动将我们指定的第三方库提取到单独的包中:
var path = require('path'); const webpack = require('webpack'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', entry: { main: './src/index.js', vendor: [ 'lodash' ] }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Caching' }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' //指定的第三方库即上面的 lodash 将自动提取到这里 }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest' //其他的共用代码提取到这里 }) ], output: { filename: '[name].[chunkhash].js', //这个时候用[chunkhash]才有意义 path: path.resolve(__dirname, 'dist') } };
配合 [chunkhash] 使用,当我们修改本地代码时,由第三方库提取出的文件的文件名并不会改变,因此浏览器将会使用缓存的文件,达到了减少请求的功能。
8.2.2、解决去重代码出现的问题(模板标识符)
提取出第三库的文件有可能会因为在模块中的引用顺序发生变化而导致编译的文件名也发生改变,这显然不是我们想要的,因为第三库并没有改变。
可以使用两个插件来解决这个问题。第一个插件是 NamedModulesPlugin
将使用模块的路径,而不是数字标识符。虽然此插件有助于在开发过程中输出结果的可读性,然而执行时间会长一些,推荐用于开发环境。第二个选择是使用 HashedModuleIdsPlugin
,推荐用于生产环境构建。
只要在webpack的配置文件中添加代码:
//只要在plugins 中引入插件: //使用HashedModuleIdsPlugin插件 new webpack.HashedModuleIdsPlugin() //使用NamedModulesPlugin插件 new webpack.NamedModulesPlugin()
8.3、动态导入(import()、require.ensure)
当我们使用动态导入(import().then())来导入一个模块而不是静态导入时,webpack会自动将该模块分离到单独的 bundle。import() 将会返回一个 promise。
//动态导入语法 import('a.js').then(() => { console.log('导入成功'); }) //静态导入语法 import {func} from a.js
使用上面动态导入会将模块自动分离到单独的 bundle,但是文件命名会根据 id 命名,每次也可能会发生改变。我们可以使用 webpackChunkName 来指定生成的文件名称:
//动态导入语法 import(/* webpackChunkName: 'aaa' */'a.js').then(() => { console.log('导入成功'); })
上述写法将会将 a.js 打包至 aaa.js 文件中。
9、懒加载和预加载
9.1、懒加载
使用动态导入语法我们就可以实现懒加载的功能了。
之前使用的一些静态导入比如:import {func} from a.js ,这种语法在页面一加载时就会立即加载 a.js 文件。但是当我们使用动态导入时,就可以在一些特定条件时才导入文件,因为动态导入语法不需要放在文件开头,也不会在编译阶段执行。
比如在某个按钮被点击时才调用 a.js 中的某个方法,此时才需要加载 a.js,这时候我们可以这么写:
btn.onclick = function() { import('a.js').then( ({add}) => { console.log(add(1, 2)); }) }
上述代码就实现了懒加载,只有当按钮点击时即某个事件触发时,才会加载 a.js 文件,否则将一直不会加载该文件。并且不会重复加载,即只加载一次,再次点击不会重新加载。
9.2、预加载
预加载会在浏览器空闲时加载文件。可以跟懒加载结合使用,就可以在用户主动触发或者在浏览器空闲时都能加载了。
btn.onclick = function() { import(/* webpackChunkName: 'aaa', webpackPrefetch: true */'a.js').then( ({add}) => { console.log(add(1, 2)); }) }
但是预加载兼容性比较差,在IE和低版本浏览器中可能会有问题,所以请慎用。
10、缓存问题(hash)
当我们使用 webpack 来打包代码,webpack 会生成一个可部署的文件夹(比如:/dist),然后把打包后的内容放置在此目录中。只要把 /dist
目录中的内容部署到服务器上,客户端(通常是浏览器)就能够访问网站此服务器的网站及其资源。当浏览器会使用缓存技术来将文件缓存。当我们在部署新版本时如果不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,浏览器可能无法获取到我们部署的新版本。
通过使用 [hash] 来使得每次更改代码之后也能更改掉文件名,可以确保浏览器获取到修改后的文件。[hash]
替换可以用于在文件名中包含一个构建相关的 hash,但是更好的方式是使用 [chunkhash]
替换,在文件名中包含一个 chunk 相关的哈希。
const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { .. output: { path: __dirname + "/build", filename: "[name].hash].js" }, ... };
[chunkhash] 跟热替换模块功能的插件 HotModuleReplacementPlugin 有冲突,要想使用 [chunkhash] 必须把热替换注释掉。[chunkhash] 只建议在生产环境中使用。
[hash] 是有一个文件修改那么所有的文件的hash值都修改。