一看就懂之webpack基础配置javascript
所谓打包多页面,就是同时打包出多个html页面,打包多页面也是使用html-webpack-plugin,只不过,在引入插件的时候是建立多个插件对象,由于一个html-webpack-plugin插件对象只能打包出一个html页面。如:css
module.exports = { entry: { index: "./src/index.js", // 指定打包输出的chunk名为index foo: "./src/foo.js" // 指定打包输出的chunk名为foo }, plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html", // 要打包输出哪一个文件,可使用相对路径 filename: "index.html", // 打包输出后该html文件的名称 chunks: ["index"] // 数组元素为chunk名称,即entry属性值为对象的时候指定的名称,index页面只引入index.js }), new HtmlWebpackPlugin({ template: "./src/index.html", // 要打包输出哪一个文件,可使用相对路径 filename: "foo.html", // 打包输出后该html文件的名称 chunks: ["foo"] // 数组元素为chunk名称,即entry属性值为对象的时候指定的名称,foo页面只引入foo.js }), ] }
打包多页面时,关键在于 chunks属性的配置,由于在没有配置chunks属性的状况下,打包输出的index.html和foo.html都会同时引入index.js和foo.js,因此必须配置chunks属性, 来指定打包输出后的html文件中要引入的输出模块,数组的元素为entry属性值为对象的时候指定的chunk名,如上配置,才能实现,index.html只引入index.js,foo.html只引入foo.js文件js文件能够经过chunks属性进行筛选,可是css则没法筛选,css是否会被html文件所引入,彻底是看html中引入的js文件,若是引入的js中require或者import了某个css文件,那么这个css文件就会被引入到该html文件中。html
source-map就是源码映射,主要是为了方便代码调试,由于咱们打包上线后的代码会被压缩等处理,致使全部代码都被压缩成了一行,若是代码中出现错误,那么浏览器只会提示出错位置在第一行,这样咱们没法真正知道出错地方在源码中的具体位置。webpack提供了一个devtool属性来配置源码映射。前端
let foo = 1; console.lg(`console对象的方法名log写成了lg`); // 源文件第二行出错
index.js:1 Uncaught TypeError: console.lg is not a function at Object.<anonymous> (index.js:1) at o (index.js:1) at Object.<anonymous> (index.js:1) at o (index.js:1) at index.js:1 at index.js:1
源码中出错的位置明明是第二行代码,而浏览器中提示的错误确实在第一行,因此若是代码很复杂的状况下,咱们就没法找到出错的具体位置
devtool常见的有4种配置:
① source-map: 这种模式会产生一个.map文件,出错了会提示具体的行和列,文件里面保留了打包后的文件与原始文件之间的映射关系,打包输出文件中会指向生成的.map文件,告诉js引擎源码在哪里,因为源码与.map文件分离,因此须要浏览器发送请求去获取.map文件,经常使用于生产环境,如:vue
//# sourceMappingURL=index.js.map
② eval: 这种模式打包速度最快,不会生成.map文件,会使用eval将模块包裹,在末尾加入sourceURL,经常使用于开发环境,如:java
//# sourceURL=webpack:///./src/index.js
③ eval-source-map: 每一个 module 会经过 eval() 来执行,而且生成一个 DataUrl 形式的 SourceMap(即base64编码形式内嵌到eval语句末尾), 可是不会生成.map文件,能够减小网络请求*,可是打包文件会很是大*。node
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibGciXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLEdBQUcsR0FBRyxDQUFWO0FBQ0FDLE9BQU8sQ0FBQ0MsRUFBUix1RSxDQUFxQyIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBmb28gPSAxO1xuY29uc29sZS5sZyhgY29uc29sZeWvueixoeeahOaWueazleWQjWxvZ+WGmeaIkOS6hmxnYCk7IC8vIOa6kOaWh+S7tuesrOS6jOihjOWHuumUmVxuIl0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./src/index.js
④ cheap-source-map: 加上 cheap,就只会提示到第几行报错,少了列信息提示,同时不会对引入的库作映射,能够提升打包性能,可是会产生.map文件。jquery
③ cheap-module-source-map: 和cheap-source-map相比,加上了module,就会对引入的库作映射,而且也会产生.map文件,用于生产环境。webpack
④ cheap-module-eval-source-map: 经常使用于开发环境,使用 cheap 模式能够大幅提升 souremap 生成的效率,加上module同时会对引入的库作映射,eval提升打包构建速度,而且不会产生.map文件减小网络请求。web
凡是带eval的模式都不能用于生产环境,由于其不会产生.map文件,会致使 打包后的文件变得很是大。一般咱们并 不关心列信息,因此都会使用cheap模式,可是咱们也仍是须要对第三方库作映射,以便精准找到错误的位置。
webpack 能够监听文件变化,当它们修改后会从新编译,若是须要开启该功能,那么须要将watch设置为true,具体监听配置经过watchOptions进行相应的设置。
module.exports = { watch: true, watchOptions: { poll: 1000, // 每隔一秒轮询一次文件是否发生变化 aggregateTimeout: 1000, // 当第一个文件更改,会在从新构建前增长延迟。这个选项容许 webpack 将这段时间内进行的任何其余更改都聚合到一次从新构建里 ignored: /node_modules/ // 排除一些文件的监听 } }
① clean-webpack-plugin: 其做用就是每次打包前先先将输出目录中的内容进行清空,而后再将打包输出的文件输出到输出目录中。
const {CleanWebpackPlugin} = require("clean-webpack-plugin"); module.exports = { plugins: [ new CleanWebpackPlugin() // 打包前清空输出目录 ] }
须要注意的是,require("clean-webpack-plugin)的结果是一个对象而不是类, 这个对象中的CleanWebpackPlugin属性才是一个类,咱们就是用这个类去建立插件对象
② copy-webpack-plugin: 其做用就是打包的时候带上一些readMe.md、history.md等等一块儿输出到输出目录中。
module.exports = { plugins: [ new CopyWebpackPlugin([ { from: "./readMe.md", // 将项目根目录下的readMe.md文件一块儿拷贝到输出目录中 to: "" // 属性值为空字符串则表示是输出目录 } ]) ] }
③ BannerPlugin: 其做用就是在打包输出的js文件的头部添加一些文字注释,好比版权说明等等,BannerPlugin是webpack内置的插件,如:
module.exports = { plugins: [ new webpack.BannerPlugin("Copyright © 2019") // 在js文件头部添加版权说明 ] }
为何webpack会存在跨域问题?由于webpack打包的是前端代码,其最终会被部署到前端服务器上,而先后端代码一般部署在不一样的服务器上,即便是部署在同一个服务器上,所使用的端口也是不同的,当前端代码经过ajax等手段向后端服务器获取数据的时候,因为先后端代码不在同一个域中,故存在跨域问题。好比,咱们经过webpack的devServer来运行部署咱们的前端应用代码,devServer启动在8080端口上,而前端应用代码中会经过ajax请求后端数据,后端服务器启动在3000端口上。
// index.js
const xhr = new XMLHttpRequest(); // xhr.open("get", "http://localhost:3000/api/test"); // 因为跨域问题没法直接访问到http://localhost:3000下的资源 xhr.open("get", "/api/test"); // 原本是要访问http://localhost:3000/api/test xhr.onreadystatechange = () => { if (xhr.readyState === 4) { console.log(xhr.responseText); } } xhr.send();
因为前端代码是运行在浏览器中的,若是在前端代码中直接经过ajax向 http://localhost:3000/api/test发起请求获取数据,那么因为 浏览器同源策略的影响,会存在跨域的问题,因此必须访问/api/test,可是这样访问又会出现404问题,由于其实访问的是 http://localhost:8080/api/test,8080服务器上是没有该资源的,解决办法就是 经过devServer配置一个代理服务器
module.exports = { devServer: { proxy: { "/api": "http://localhost:3000" // 路径以/api开头则代理到localhost:3000上 } } }
访问 http://localhost:8080/api/test就会被代理到 http://localhost:3000/api/test上,proxy还支持路径的重写,若是3000端口服务器上并无/api/test路径,只有/test路径,那么就能够对路径进行重写,将/api替换掉
module.exports = { devServer: { proxy: { "/api": { target: "http://localhost:3000", pathRewrite: {"/api": ""} // 将/api替换掉 } } } }
访问 http://localhost:8080/api/test就会被代理到 http://localhost:3000/test上
若是前端只是想mock一些数据,并不须要真正的去访问后台服务器,那么咱们能够经过devServer提供的before钩子函数获取到内置的服务器对象进行处理请求,这个内置的服务器对象就是webpack的devServer即8080端口的server,由于是在同一个服务器中请求数据因此也不会出现跨域问题。
before(app) { // 此app即webpack的devServer app.get("/api/test", (req, res, next) => { res.json({name: "even"}); }) }
咱们还能够不经过webpack提供的devServer来启动webpack,而是使用本身服务器来启动webapck。
// server.js
const express = require("express"); const app = express(); const webpack = require("webpack"); // 引入webpack const config = require("./webpack.config.js"); // 引入配置文件 const compiler = webpack(config); // 建立webpack的编译器 const middleWare = require("webpack-dev-middleware"); //引入webpack的中间件 app.use(middleWare(compiler)); // 将compiler编译器交给中间件处理 app.get("/api/test", (req, res, next) => { res.json({name: "lhb"}); }); app.listen(3000);
经过自定义服务器启动webpack,这样webpack中的前端代码请求数据就和服务器的资源在同一个域中了。
resolve用于配置模块的解析相关参数的,其属性值为一个对象。
① modules: 告诉webpack 解析模块时应该搜索的目录,即require或import模块的时候,只写模块名的时候,到哪里去找,其属性值为数组,由于可配置多个模块搜索路径,其搜索路径必须为绝对路径,好比,src目录下面有一个foo.js文件和index.js文件:
// index.js
const foo = require("./foo"); // 必须写全foo.js模块的路径 // const foo = require("foo"); // resolve.modules中配置了模块解析路径为.src目录,则可用只写foo便可搜索到foo.js模块 console.log(foo);
module.exports = { resolve: { modules: [path.resolve(__dirname, "./src/"), "node_modules"] }, }
因为resolve.modules中配置了./src目录做为模块的搜索目录,因此index.js中能够只写模块名便可搜索到foo.js模块
② alias: 用于给路径或者文件取别名,当import或者require的模块的路径很是长时,咱们能够给该模块的路径或者整个路径名+文件名都设置成一个别名,而后直接引入别名便可找到该模块,好比,有一个模块位置很是深
// const foo = require("./a/b/c/foo"); // foo.js在./src/a/b/c/foo.js // const foo = require("foo"); // foo被映射成了./src/a/b/c/foo.js文件 const foo = require("bar/foo.js"); // bar被映射成了./src/a/b/c/路径 console.log(foo);
module.exports = { resolve: { alias: { "foo": path.resolve(__dirname, "./src/a/b/c/foo.js"), "bar": path.resolve(__dirname, "./src/a/b/c/") } }, }
须要注意的就是, alias能够映射文件也能够映射路径
③ mainFields: 咱们的package.json中能够有多个字段,用于决定优先使用哪一个字段来导入模块,好比bootstrap模块中含有js也含有css,其package.json文件中main字段对应的是"dist/js/bootstrap",style字段中对应的是"dist/css/bootstrap.css",咱们能够经过设置mainFields字段来改变默认引入,如:
module.exports = { resolve: { mainFields: ["style", "main"] }, }
④ extensions: 用于设置引入模块的时候,若是没有写模块后缀名,webpack会自动添加后缀去查找,extensions就是用于设置自动添加后缀的顺序,如:
module.exports = { resolve: { extensions: ["js", "vue"] }, }
若是项目中引入了foo模块,require("./foo"),其会优先找./foo.js,若是没有找到./foo.js则会去找./foo.vue文件
设置环境变量须要用到webpack提供的一个内置插件DefinePlugin插件,其做用是将一个字符串值设置为全局变量,如:
module.exports = { plugins: [ new webpack.DefinePlugin({ DEV_MODE: JSON.stringify('development') // 将'development'设置为全局变量DEV_MODE }), ] }
这样配置以后任何一个模块中均可以直接使用DEV_MODE变量了,而且其值为'development',与ProvidePlugin有点类似, ProvidePlugin是将一个模块注入到全部模块中, 实现模块不须要引入便可直接使用。
① noParse: 该配置是做为module的一个属性值,即不解析某些模块,所谓不解析,就是不去分析某个模块中的依赖关系,即不去管某个文件是否import(依赖)了某个文件,对于一些独立的库,好比jquery,其根本不存在依赖关系,jquery不会去引入其余的库(要根据本身对某个模块的了解去判断是否要解析该模块),因此咱们可让webpack不去解析jquery的依赖关系,提升打包速度,如:
module.exports = { module: { noParse:/jquery/,//不去解析jquery中的依赖库 } }
noParse是 module配置中的一个属性,其属性值为一个正则表达式, 填入不被解析的模块名称。
为了更清楚的展现noParse的做用,假设咱们在入口文件index.js中引入bar.js模块,同时这个bar.js模块中也引入了foo.js模块,foo.js再也不依赖其余模块了,那么在不使用noParse的状况下,webpack打包的时候,会先去分析index.js模块,发现其引入了bar.js模块,而后接着分析bar.js模块,发现其引入了foo.js模块,接着分析foo.js模块。
Entrypoint index = index.js [./src/bar.js] 55 bytes {index} [built] [./src/foo.js] 21 bytes {index} [built] [./src/index.js] 81 bytes {index} [built]
而此时若是使用了noParse: /bar/,那么webpack打包的时候,会先去分析index.js模块,发现其引入了bar.js模块,可是因为noParse的做用,将再也不继续解析bar.js模块了,即不会去分析bar.js中引入的foo.js模块了。
Entrypoint index = index.js [./src/bar.js] 55 bytes {index} [built] [./src/index.js] 81 bytes {index} [built]
② exclude: 在loader中使用exclude排除对某些目录中的文件处理,即引入指定目录下的文件时候,不使用对应的loader进行处理,exclude是loader配置中的一个属性,属性值为正则表达式,如:
module.exports = { module: { rules: [ { test: /\.js$/, use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env"], plugins: ["@babel/plugin-transform-runtime"] } } ], exclude: /node_modules/ } ] } }
③ 使用IgnorePlugin来忽略某个模块中某些目录中的模块引用,好比在引入某个模块的时候,该模块会引入大量的语言包,而咱们不会用到那么多语言包,若是都打包进项目中,那么就会影响打包速度和最终包的大小,而后再引入须要使用的语言包便可,如:
项目根目录下有一个time包,其中有一个lang包,lang包中包含了各类语言输出对应时间的js文件,time
包下的index.js会引入lang包下全部的js文件,那么当咱们引入time模块的时候,就会将lang包下的全部js文件都打包进去,添加以下配置:
const webpack = require("webpack"); module.exports = { plugins: [ new webpack.IgnorePlugin(/lang/, /time/) ] }
引入time模块的时候,若是time模块中引入了其中的lang模块中的内容,那么就忽略掉,即不引入lang模块中的内容,须要注意的是, 这/time/只是匹配文件夹和time模块的具体目录位置无关,即只要是引入了目录名为time中的内容就会生效。
④ 使用HappyPack:因为在打包过程当中有大量的文件须要交个loader进行处理,包括解析和转换等操做,而因为js是单线程的,因此这些文件只能一个一个地处理,而HappyPack的工做原理就是充分发挥CPU的多核功能,将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程,happypack主要起到一个任务劫持的做用,在建立HappyPack实例的时候要传入对应文件的loader,即use部分,loader配置中将使用通过HappyPack包装后的loader进行处理,如:
const HappyPack = require("happypack"); // 安装并引入happypack模块 module.exports = { plugins: [ new HappyPack({ // 这里对处理css文件的loader进行包装 id: "css",// 以前的loader根据具体的id进行引入 use: ["style-loader","css-loader"], threads: 5 // 设置开启的进程数 }) ], module: { rules: [ { test: /\.css$/, // 匹配以.css结尾的文件 use: ["happypack/loader?id=css"] //根据happypack实例中配置的id引入包装后的laoder,这里的happyPack的h能够大写也能够小写 } ] } }
webpack要打包的文件很是多的时候才须要使用happypack进行优化,由于 开启多进程也是须要耗时间的,因此文件少的时候,使用happypack返回更耗时
⑤ 抽离公共模块: 对于多入口状况,若是某个或某些模块,被两个以上文件所依赖,那么能够将这个模块单独抽离出来,不须要将这些公共的代码都打包进每一个输出文件中,这样会形成代码的重复和流量的浪费,即若是有两个入口文件index.js和other.js,它们都依赖了foo.js,那么若是不抽离公共模块,那么foo.js中的代码都会打包进最终输出的index.js和other.js中去,即有两份foo.js了。抽离公共模块也很简单,直接在optimization中配置便可,如:
module.exports = { splitChunks: { // 分割代码块,即抽离公共模块 cacheGroups: { // 缓存组 common: { // 组名为common可自定义 chunks: "initial", minSize: 0, // 文件大小为0字节以上才抽离 minChunks: 2, // 被引用过两次才抽离 name: "common/foo", // 定义抽离出的文件的名称 } } } }
这样就会将公共的foo.js模块抽离到common目录下foo.js中了,可是若是咱们也有多个文件依赖了第三方模块如jquery,若是按以上配置,那么jquery也会被打包进foo.js中, 会致使代码混乱,因此咱们但愿将jquery单独抽出来,即与foo.js分开,咱们能够复制一份以上配置,并经过设置抽离代码权重的方式来实现,即优先抽离出jquery,如:
module.exports = { splitChunks: { // 分割代码块,即抽离公共模块 cacheGroups: { // 缓存组 common: { // 组名为common可自定义 chunks: "initial", minSize: 0, // 文件大小为0字节以上才抽离 minChunks: 2, // 被引用过两次才抽离 name: "common/foo", // 定义抽离出的文件的名称 }, verdor: { test: /node_modules/, priority: 1, // 设置打包权重,即优先抽离第三方模块 chunks: "initial", minSize: 0, // 文件大小为0字节以上才抽离 minChunks: 2, // 被引用过两次才抽离 name: "common/jquery", // 定义抽离出的文件的名称 } } } }
这样就会在common目录下同时抽离出foo.js和jquery.js了,须要注意的是,代码的抽离 必须是该模块没有被排除打包,即该模块会被打包进输出bundle中,若是第三方模块已经经过externals排除打包,则以上vendor配置无效。
⑥ 按需加载,即在须要使用的时候才打包输出,webpack提供了import()方法,传入要动态加载的模块,来动态加载指定的模块,当webpack遇到import()语句的时候,不会当即去加载该模块,而是在用到该模块的时候,再去加载,也就是说打包的时候会一块儿打包出来,可是在浏览器中加载的时候并不会当即加载,而是等到用到的时候再去加载,好比,点击按钮后才会加载某个模块,如:
const button = document.createElement("button"); button.innerText = "点我" button.addEventListener("click", () => { // 点击按钮后加载foo.js import("./foo").then((res) => { // import()返回的是一个Promise对象 console.log(res); }); }); document.body.appendChild(button);
从中能够看到,import()返回的是一个Promise对象,其主要就是利用JSONP实现动态加载,返回的res结果不一样的export方式会有不一样,若是使用的module.exports输出,那么返回的res就是module.exports输出的结果;若是使用的是ES6模块输出,即export default输出,那么返回的res结果就是res.default,如:
// ES6模块输出,res结果为
{default: "foo", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
⑦ 开启模块热更新: 模块热更新能够作到在不刷新网页的状况下,更新修改的模块,只编译变化的模块,而不用所有模块从新打包,大大提升开发效率,在未开启热更新的状况下,每次修改了模块,都会从新打包。要开启模块热更新,那么只须要在devServer配置中添加hot:true便可。固然仅仅开启模块热更新是不够的,咱们须要作一些相似监听的操做,当监听的模块发生变化的时候,从新加载该模块并执行,如:
module.exports = { devServer: { hot: true // 开启热更新 } } ---------- import foo from "./foo"; console.log(foo); if (module.hot) { module.hot.accept("./foo", () => { // 监听到foo模块发生变化的时候 const foo = require("./foo"); // 从新引入该模块并执行 console.log(foo); }); }
若是不使用module.hot.accept监听,那么当修改foo模块的时候仍是会刷新页面的。