新建文件夹webpack-tut并进入,打开cmd命令窗口,npm init -y,创建package.json 文件,管理依赖和脚本命令。npm install webpack webpack-cli --save-dev,安装webpack。webpack4把webpack命令抽离出来,形成了一个单独的包webpack-cli ,安装它,才能在命令行中使用webpack命令。为什么要单独形成一个包呢?因为webpack-cli 还提供了两个其它的功能,init 和 migrate命令,使我们快速创建webpack 配置和提供升级迁移。不过这两个基本不用,了解一下就可以了。只要记住安装webpack的同时安装上webpack-cli就可以了。
webpack是模块打包工具,默认情况下,它只认识js文件和json资源 。mkdir src 存放源文件,touch src/index.js, touch src/component.js 文件,component.js 文件
export default (text = 'hello world') => { const element = document.createElement('div'); element.innerHTML = text; return element; }
index.js 文件
import component from './component';
document.body.appendChild(component());
npx webpack可以直接执行webpack命令,进行打包。也可以在package.json的scripts的字段中,写上 “build”: “webpack”, 执行npm run build 命令进行打包,
生成了dist 目录,表示打包成功了,这就是webpack4提供的零配置,没有写配置文件,也能打包。当执行webpack命令而又没有配置文件时,webpack会寻找默认的入口文件,项目根目录下的src目录下的index.js文件,然后打包文件到dist 目录中,文件名为main.js。但有一个WARNING,没有设置mode。Webpack4 提供了一个mode 配置项 ,用什么模式进行打包,模式不同,打包后的文件内容不同。它有两个选择: production 和 development, 就是生产模式和开发模式。生产模式用于发布到生产环境,都是压缩代码。开发模式用于开发,有利于debug。mode 可以在命令行进行配置, build 命令改成 webpack --mode production 或 webpack --mode development,就可以了。
怎么验证一下打包后的文件是正确的?肯定是建一个html 文件,script 引入 dist/main.js。要不要手动创建呢?不用,webpack有一个插件html-webpack-plugin,它会创建html文件,并自动引入打包后文件。npm i html-webpack-plugin -D,但怎么使用呢?这要用到webpack配置文件了,零配置此时无能为力了。touch webpack.config.js 文件(配置文件的默认名称),配置文件有几个重要的概念,entry, output, module, plugins。
entry:打包的入口文件,就是webpack在进行打包的时候,从哪个文件开始。
output:打包后的文件放到什么地方,以及文件名是什么
module:处理哪些模块,制定规则用什么loader来处理模块。loaders就是告诉webpack怎样解释,编译和转译源代码,转译好代码,才能交给webpack进行打包,因为webpack只认识js文件。
plugins:打包过程其它的事情,比如压缩,生成html文件,扩展了webpack的功能
由于有零配置功能,webpack提供了默认的entry和output, 如果觉得ok,配置文件中只写module 和plugins 就可以,webpack.config.js如下
const htmlWebpackPlugin = require('html-webpack-plugin'); // 引入插件
module.exports = { plugins: [ new htmlWebpackPlugin() ] }
如果觉得不ok 的话,可以写entry 和output, 把它覆盖掉,mode 的配置也是如此,可以在命令行中指定,也可以在配置文件中书写。webpack.config.js如下
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin'); // 引入插件
module.exports = {
mode: 'development',
entry: path.join(__dirname, 'src/index.js'),
output: {
path: path.join(__dirname, 'dist'),
filename: 'main.js'
},
plugins: [
new htmlWebpackPlugin()
]
}
output的path要使用绝对路径,因此使用了Node.js内置path模块。plugins是个数组,每一个用到的插件都是数组中的一项。具体到每一个插件呢?插件都会暴露出构造函数,通过new 调用,就可以使用,如果插件还有配置项,就给构造函数传递一个配置对象作为参数。npm run build 打包成功了,用浏览器打开html 文件,没有问题。
npm i css-loader style-loader -D 来处理CSS(JS文件中引入CSS文件)。因为webpack不认识CSS文件,所以要使用loader来转化成JS。配置文件中
module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }
当使用多个loader来处理同一个文件时,要注意loader的执行顺序,它是按照从右到左,从下到上的顺序执行。css-loader 先执行,然后把执行结果交给style-loader,最终style-loader返回js,这样webpack才能打包。css-loader:将css的代码按照commonJS的规范转化成js,也就是说将css代码输出到打包后的JS文件中。style-loader 是把包含css内容的JS代码,挂载到页面的<style> 标签中。style 是window对象属性。
当在一个js文件中引入css文件时,css-loader会把css代码放到最后的打包文件中。这时如果新建一个html页面,用src引入bundle.js,css文件并不生效,页面并没有样式。需要使用style-loader,重新进行打包,这时html引入新的打包后的文件,样式显示,查看页面源代码,可以发现header中有了style 标签,原来这就是style-loader的作用。它将样式挂载到window对象的style 属性。
处理图片,webpack内置Asset Modules(资源模块)
{ test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset/resource', }
Asset Modules有几种type: asset/resource: 创建一个单独的文件并导出url,此时如果你import image或在css中url(image), 这个image会作为一个单独的文件被打包到输出目录。 asset/inline: 导出一个资源的data url,如果资源是图片,它会打包成base64编码。asset/source: 导出资源的源代码,把文件当作一个字符串,比如import txt文本。asset: 在导出一个data url 和创建一个单独文件之间进行选择
字体的处理和图片一样,也是使用内置的资源模块
{ test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', }
如果每次打包都会清理dist,就好了。output有个clean:true 属性。
output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), clean: true, },
了解了webpack的基本概念后,还要区分环境进行打包配置 ,因为在开发程序时,要便于开发,比如浏览器的自动刷新,代码的自动打包,便于debug,要部署的时候,那是要压缩代码之类的。区分环境进行打包有两种实现方式,一是整个项目的配置文件就一个,通过环境变量进行区分配置,二是,不同的环境有不同的配置文件,使用wepback --config指定使用哪个配置文件。使用环境变量进行区分时,webpack提供了 --env 来配置环境变量。
webpack --env goal=local --env production --progress
像production环境变量一样,没有赋值,它默认是true。在配置文件中怎么获取到env.production 这个环境变量呢?配置文件就不能module.export 一个对象了,就要export 出一个函数,函数的第一个参数就是环境变量env。
module.export = (env, argv) => { if(env.production) }
开发模式下的配置
新建一个wepback.dev.config.js,首先把mode改成 'development'
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'development', // mode改成 development entry: './src/index.js', output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), clean: true, }, plugins: [ new HtmlWebpackPlugin() ], };
使用source maps, 因为webpack在打包之后,包裹你的代码到模块中,并且会注入额外的代码,以让程序运行,最麻烦的地方是,打包之后,只生成一个文件。这时,如果程序运行后有问题,浏览器开发工具,只会把问题定位到打包后的文件中,而不是打包之前的源代码,你也不知道具体哪个文件出错了。使用source maps后就不一样了,问题会直接定位到源代码出错的文件中,因为source maps文件就是源代码和打包后的代码之间的映射文件。配置soure maps,也很简单,devtools: 'inline-source-map'。source maps有很多类型,挑选适合自己的。
webpack-dev-sever: webpack自带的一个小型服务器,它会监测每个文件的变动,每当有文件改动时,它就会重新打包,然后浏览器会自动刷新页面,能时时看到代码的变动,这是所谓的liveload 或hotload(热更新)。npm install --save-dev webpack-dev-server,在package.json的script中添加命令, 同时指令打包使用配置文件 "start": "webpack serve --open --config wepback.dev.config.js"。但这里要注意,webpack-dev-server 打包后文件是放到内存中的,而不像npm run build 把文件打包到硬盘上。想要访问服务器的文件,路径是http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename],host默认是localhost, port是8080, publicPath默认是/, filename就是打包后文件。所以http://localhost:8080/就可以访问服务器的内容。配置文件devServer 配置项,,可以对webpack-dev-server 进行配置,比如把端口改为9000
module.exports = {
devServer: {
port: 9000, // 设置端口号
stats: 'errors-only', // 只有产生错误的时候,才显示错误信息,日志的输出等级是error.
overlay: true // 当有编译错误的时候,在浏览器页面上显示。
},
plugins: [
new htmlWebpackPlugin()
]
}
重启服务器,这时看到项目启动在9000端口下。
除了wepack-dev-server,webpack也有一个watch属性,命令行中wepback --watch也会监测代码的变动,并实时进行打包,一般使用dev-sever,而不是--watch。
HRM:一个模块发生变化,只打包一个模块 。webpack-dev-server 默认开启了热替换功能,哪个模块有热替换功能,它就使用哪个模块的热替换功能。改变css文件时,并不会引起浏览器页面的自动刷新,style-loader实现了热替换功能。但js文件默认没有热替换功能,只要改变一个js文件,会引起页面会自动刷新。怎么才能让js也有热替换功能呢?在入口文件
if(module.hot) { module.hot.accept('./print.js', () => { print(); }) }
代码分割
代码分有三种方式,多入口,提取公共模块,异步加载。多入口就是配置文件中的entry可以是一个对象 {index: ‘./src/index’, main: ‘./src/mian’}, 有几个入口文件就会形成几个chunk, 输出几个bundle 文件。
多入口打包,output filename: [name].js, 抽取css时,css的name也要改成[name].js, 每一个入口中所包含的css都会打包成一个单获的css文件。但这种简单粗暴的多入口打包有几个问题,
1, 如果每一个入口中都引用机同的包,比如lodash, 那每一个入口对应的bundle都包含lodash,体积太大。
2, 不能根据业务逻辑动态分割代码。
第一个问题有两个解决办法,一是dependOn,
entry: { index: { import: './src/index.js', dependOn: 'shared', }, another: { import: './src/another-module.js', dependOn: 'shared', }, shared: 'lodash', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, optimization: { runtimeChunk: 'single', },
一种是SplitChunksPlugin, 提取公共模块
module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-module.js', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, optimization: { splitChunks: { chunks: 'all', }, }, };
抽取出来的模块名是webapck默认的。比如vendors-node_modules_lodash_lodash_js.bundle。 如果想改变这个名字,要在outtput中定义
动态导入:使用import()语法,import('lodash')就是动态导入lodash, 先把entry改成单入口和去掉optimization.splitchunks, index.js
function getComponent() { return import('lodash') .then(({ default: _ }) => { const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; }) .catch((error) => 'An error occurred while loading the component'); } getComponent().then((component) => { document.body.appendChild(component); });
要注入引入commonJS模块时,使用default导出。
cache缓存
文件名使用[contenthash], [name]-[contenthash].js. contenthash就是文件内容变,hash值就会变。但有时,文件内容没有变化,conenthash仍然会有变化,因为在入口代码块中,webpack有特定的模板,特别是运行时和manifest,最好把这些模版抽取出来形成一个单独的代码块(chunk)。optimization.runtimeChunk option. Set it to single to create a single runtime bundle for all chunks:
optimization: { runtimeChunk: 'single', }
此是最好把react, load第三方模块了打成一个包,因为它们也不经常变化,使用cacheGroup,命名为vendor
optimization: { runtimeChunk: 'single', splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, },
此时如果添加一个模块,比如print.js,然后引入index.js,再打包,你会发现,所有的contenthash,都发生变化,为什么呢?理论上,vendor不应该发生变化啊?这是因为module.id增加了,optimization.moduleIds 设为 'deterministic'
optimization: { moduleIds: 'deterministic', runtimeChunk: 'single', }
生产模式
1,自动压缩代码。2 devtool: 'source-map',3,压缩Css
Aliases are useful when importing or requiring modules or files, resolve.alias 针对的是import 文件的路径
import application from "../../../stylesheets/application.scss"
resolve: { alias: { CssFolder: path.resolve(__dirname, 'src/stylesheets/') } } }
import application from "CssFolder/application.scss"
resolve.modules:tell webpack what directories it should be looking in when resolving modules. module主要指第三方模块
By default, when doing something like importing a third-party library in one of our
JavaScript files, for example, when importing jQuery we do:
import $ from 'jquery'
Webpack will look into the “node_modules” directory, which is equivalent to the
resolving “node_modules” folder as seen below:
resolve: { modules: ['node_modules'] }
If we want to tell webpack to look in another directory before going to “node_
modules” (let’s imagine we have downloaded some JavaScript libraries manually and we
have a folder called “downloaded_libs”), in this case we are going to specify that folder
before the “node_modules” folder in the resolve.modules option.
resolve: { modules: [path.resolve(__dirname, 'src/downloaded_libs'), 'node_modules'] },
命令的意思是 监听webpack.config.js 的变化,然后执行(exec) webpack-dev-server 命令,注意\'' 双引号的转义。
webpack-dev-server 还有两个配置项需要注意一下:
contentBase: webpack-dev-server 会把所有的静态文件(css, js, img 等)进行打包,放到服务器根目录下,供我们访问。但我们可以访问服务器中的任何资源,一旦这些资源不是由webpack-dev-server 打包生成的,我们就要指定这些非打包生成的静态资源,比如index.html 文件,的位置,也就是contentbase,否则就会显示404. 如果不使用webpack-html-plugin, webpack 是不会打包生成index.html的, 那我们就要手动创建index.html, 这时index.html 文件,就是非webpack-dev-server 打包生成的资源,我们就要指定它的位置。因为我们在浏览器中输入localhost:8080, 我们是向webpack-dev-server 请求index.html 资源,webpack-dev-server 并没有生成这个文件,所以就会报错,如果告诉webpack-dev-server, index.html 在什么地方,它就会去找,就不会报错了。这就是contentbase的作用,webpack-dev-server 会向contentbase 指定的目录去找它没有打包生成的文件,你可能说,我们手动创建index.html时,也没有指定contnetbase, 整个项目也没有问题,这是因为contentbase的默认值是项目根目录,而我们创建的index.html 恰巧也在项目根目录下,所以没有问题。如果我们在项目根目录下新建一个文件夹叫public, 然后把index.html 放到里面,你再运行webapck-dev-serve , 它就会报错,这时就要指定contentbase, 它的取值就很清楚了,index.html(非webpack-dev-server 打包的资源)所在的位置, 绝对路径和相对路径都可以, 相对路径"build", 它是相对于项目根目录的, 绝对路径,path.join(__dirname, ‘public’)
proxy: 代理,做过前后端联调,都知道代理的作用。当我们在本地开发的时候,访问的服务器是localhost. 但是后端的代码却在同事的电脑上,我们要访问同事的服务,就要设置代理了,要不然访问的永远都是本地的服务localhost,一个接口都没有。我们在请求的接口面前加一个标识,如axios.post(‘/api/login’), /api 就是标识,然后我们再在proxy 配置项里面给这个标识配置一个代理到的真实路径,如 ‘/api’ : ‘http://102.03.34.58/api’, 那么当我们调用接口的时候,实际上变成了http://102.03.34.58/api/login, 代理配置中的真实路径,就是会替换到请求中的标示
module.exports = { devServer: { contentBase:'build', proxy: { '/api': 'http://102.03.34.58/api' }, port: 9000, // 设置端口号 stats: 'errors-only', // 只有产生错误的时候,才显示错误信息,日志的输出等级是error. overlay: true // 当有编译错误的时候,在浏览器页面上显示。 }, plugins: [ new htmlWebpackPlugin() ] }
但有时候,可能是多个同事进行开发,接口没有那么规范,可能有的以api 开始,有的没有api, 根本就没有统一的标识,以上这种配置方式肯定不行, '/api' 标识还可以是一个对象
proxy: { '/api': { target: 'http://102.03.34.58', pathRewrite: { '^/api': '' } } }
这里要注意target 是请求的服务器地址,后面没有api, 使用这种方式配置以后,代理会在前端请求中的/api前面加上target, 相当于还是请求了 http://102.03.34.58/api/login,所以这里增加了pathRewrite 路径重写,所有以/api 开头的路径都转化为 空,所以最后真实的请求路径中 http://102.03.34.58/login. pathRewrite 中的属性是正则表达式,^以什么开始, 值 呢?就是匹配到的路径重写成会什么。
proxy 中的属性'/api', 是前端发送请求时,请求接口中的url要加的参数,当真正发送请求时,webpack 服务中配置的代理碰到api 就会拦截,然后把它变成我们配置的真实的路径
The contentBase option role is to specify which folder should be used to serve the
static content (I’m referring more precisely to the index.html file) from. In case it's not
set, it will default to the root of the working directory
But in our case, even though
we have set that option explicitly, it won’t make any difference because we are using
htmlWebpackPlugin, as a consequence the index.html file will be part of the webpack
bundle, and when using webpack-dev-server, that bundle will be built and served from
memory (which takes precedence over the local filesystem) regardless of the specified
“contentBase” path. In case any file (HTML, JS, CSS, etc.) is not found in memory,
webpack-dev-server will fall back on “contentBase” directory and tries to fetch that file.
So, in short, you need to retain this: webpack-dev-server loads and serves any
bundled file from memory instead of the local filesystem. If any resource (HTML, JS, CSS,
etc.) is not found in memory, webpack-dev-server will look at the “contentBase” path
and try to find the missed file there. When no “contentBase” option is set, it will look at
the root of the working directory.
the webpack bundle is built and served from memory at the root url (/) of our server, which means that if we have an application.js file, we can access it in the browser at the url http://localhost:9000/application.js. That’s because webpack-dev-server has a “publicPath” option that is by default set to '/'.
当使用HRM的时候,文件名不要用hash值,因为
it’s reported that using it causes many other issues, like memory leak and also
because the devServer does not know when to clean up the old cached files.
因此it’s recommended to turn caching off in development and use it only for the production mode in which we won’t need to use webpack-dev-server anyway.
Setting optimization.minimizer overrides the defaults provided by webpack, so make sure to also specify a JS minimizer. --- 针对css 的压缩。