亲宝软件园·资讯

展开

【webpack 系列】进阶篇

阿林十一 人气:0
本文将继续引入更多的 `webpack` 配置,建议先阅读[【webpack 系列】基础篇](https://www.cnblogs.com/alsy/p/12594946.html)的内容。如果发现文中有任何错误,请在评论区指正。本文所有代码都可在 [github](https://github.com/2944927590/webpack-practice/tree/master/webpack-demo2) 找到。 ## 打包多页应用 之前我们配置的是一个单页的应用,但是我们的应用可能需要是个多页应用。下面我们来进行多页应用的 `webpack` 配置。 先看一下我们的目录结构 ``` ├── public │   ├── detail.html │   └── index.html ├── src │   ├── detail-entry.js │   ├── index-entry.js ``` `public` 下面有 `index.html` 和 `detail.html` 两个页面,对应 `src` 下面有 `index-entry.js` 和 `detail-entry.js` 两个入口文件。 在`webpack.config.js` 配置 ``` // webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); // ... module.exports = { entry: { index: path.resolve(__dirname, 'src/index-entry.js'), detail: path.resolve(__dirname, 'srchttps://img.qb5200.com/download-x/detail-entry.js') }, output: { path: path.resolve(__dirname, 'dist'), // 输出目录 filename: '[name].[hash:6].js', // 输出文件名 }, plugins: [ // index.html new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'public/index.html'), // 指定模板文件,不指定会生成默认的 index.html 文件 filename: 'index.html', // 打包后的文件名 chunks: ['index'] // 指定引入的 js 文件,对应在 entry 配置的 chunkName }), // detail.html new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'publichttps://img.qb5200.com/download-x/detail.html'), // 指定模板文件,不指定会生成默认的 index.html 文件 filename: 'detail.html', // 打包后的文件名 chunks: ['detail'] // 指定引入的 js 文件,对应在 entry 配置的 chunkName }), // 打包前自动清除dist目录 new CleanWebpackPlugin() ] } ``` `npm run build` 之后可以看到生成的 `dist` 目录如下 ``` dist ├── assets │   └── author_ee489e.jpg ├── detail.dbcb15.js ├── detail.dbcb15.js.map ├── detail.html ├── index.dbcb15.js ├── index.dbcb15.js.map └── index.html ``` `index.html` 页面中已经引入了打包好的 `index.dbcb15.js` 文件,`detail.html` 文件也已经引入了 `detail.dbcb15.js` 文件。更多配置请查看 [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin)。 ## 将 CSS 样式单独抽离生成文件 `webpack4` 对 `css` 模块支持的完善以及在处理 `css` 文件提取的方式上也做了些调整,由 `mini-css-extract-plugin` 来代替之前使用的 `extract-text-webpack-plugin`,使用方式很简单。 该插件将 `css` 提取到单独的文件中,为每个包含 `css` 的 `js` 文件创建一个 `css` 文件,支持 `css` 和 `sourcemap` 的按需加载。 与 `extract-text-webpack-plugin` 相比有如下优点 1. 异步加载 2. 没有重复的编译(性能) 3. 更容易使用 4. 特定于 `css` 安装 `extract-text-webpack-plugin` ``` npm i -D mini-css-extract-plugin ``` 配置 `webpack.config.js` ``` // webpack.config.js const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // ... module.exports = { // ... module: { rules: [ { test: /\.(c|le)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'], exclude: /node_modules/ }, { test: /\.sass$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'], exclude: /node_modules/ }, // ... ] }, plugins: [ // ... new MiniCssExtractPlugin({ filename: 'css/[name].[hash:6].css' }) ] } ``` `npm run build` 之后会发现在 `dist/css` 目录有了抽离出来的 `css` 文件了。 ![](https://user-gold-cdn.xitu.io/2020/4/5/1714a1957c0b73a2?w=713&h=567&f=png&s=60097) 这时我们发现两个问题: 1. 打包生成的 `css` 文件没有进行压缩。 2. 所有文件命名的 `hash` 部分都是一样的,存在缓存问题。 ### 对 css 文件进行压缩 通过 `optimize-css-assets-webpack-plugin` 插件压缩 `css` 代码 ``` npm i -D optimize-css-assets-webpack-plugin ``` 配置 `webpack.config.js` ``` // webpack.config.js //... const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin'); module.exports = { //... plugins: [ //... new OptimizeCssPlugin() ] } ``` 这样就可以对 `css` 文件进行压缩了。 对于第二个问题,我们首先需要了解下 `hash`、`chunkHash`、`contentHash` 的区别。 ## hash、chunkhash、contenthash 的区别和使用 ### hash `hash` 是基于整个 `module identifier` 序列计算得到的,webpack 默认为给各个模块分配一个 `id` 以作标识,用来处理模块之间的依赖关系,默认的 `id` 命名规则是根据模块引入的顺序赋予一个整数(`1`、`2`、`3`...)。任意修改、增加、删除一个模块的依赖,都会对整个 `id` 序列造成影响,从而改变 `hash` 值。也就是每次修改或者增删任何一个文件,所有文件名的 `hash` 值都将改变,整个项目的文件缓存都将失效。 ``` output: { path: path.resolve(__dirname, 'dist'), // 输出目录 filename: '[name].[hash:6].js', // 输出文件名 } new MiniCssExtractPlugin({ filename: 'css/[name].[hash:6].css' }) ``` ![](https://user-gold-cdn.xitu.io/2020/4/5/1714a41afcac2ef5?w=700&h=527&f=png&s=53004) 可以看到打包后的 `js` 和 `css` 文件的 `hash` 值是一样的,所以对于没有发生改变的模块而言,这样做是不合理的。 当然可以看到,对于图片等资源该 `hash` 还是可以生成一个唯一值的。 ### chunkhash `chunkhash` 根据不同的入口文件进行依赖文件解析、构建对应的 `chunk`,生成对应的哈希值。我们将 `filename` 配置成 `chunkhash` 来看一下打包的结果。 ``` output: { path: path.resolve(__dirname, 'dist'), // 输出目录 filename: '[name].[chunkhash:6].js', // 输出文件名 } new MiniCssExtractPlugin({ filename: 'css/[name].[chunkhash:6].css' }) ``` ![](https://user-gold-cdn.xitu.io/2020/4/5/1714a450f7753b8e?w=696&h=525&f=png&s=51918) 可以看到此时打包之后的 `index.js` 和 `detail.js` 的 `chunkhash` 是不一样的。但是会发现 `index.js` 和 `index.css` 以及 `detail.js` 和 `detail.css` 的 `chunkhash` 是一致的,并且任意改动 `js` 或者 `css` 都会引起对应的 `css` 和 `js` 文件的 `chunkhash` 的改变,这是不合理的。所以这里抽离出来的 `css` 文件将使用 `contenthash`,来区分 `css` 文件和 `js` 文件的更新。 ### contenthash `contenthash` 是针对文件内容级别的,只有你自己模块的内容变了,那么 `hash` 值才改变。 ``` output: { path: path.resolve(__dirname, 'dist'), // 输出目录 filename: '[name].[chunkhash:6].js', // 输出文件名 } new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:6].css' }) ``` ![](https://user-gold-cdn.xitu.io/2020/4/5/1714a4c6b2762af5?w=694&h=486&f=png&s=46828) `OK`,可以看到分离出来的 `css` 文件已经和入口文件的 `hash` 值区分开了。 ### 如何使用 为了实现理想的缓存,我们一般这样使用他们: 1. `JS` 文件使用 `chunkhash` 2. 抽离的 `CSS` 样式文件使用 `contenthash` 3. `gif|png|jpe?g|eot|woff|ttf|svg|pdf` 等使用 `hash` ## 按需加载 很多时候我们并不需要在一个页面中一次性加载所有的 `js` 或者 `css` 文件,而是应该是需要用到时才去加载相应的 `js` 或者 `css` 文件。 ### import() 比如,现在我们需要点击一个按钮才会使用对应的 `js`、`css` 文件,需要 `import()` 语法: ``` // index-entry.js import './index.sass'; //... const handle = () => import('./handle'); const handle2 = () => import('./handle2'); document.querySelector('#btn').onclick = () => { handle().then(module => { module.handleClick(); }); handle2().then(module => { module.default(); }); } ``` ``` // handle.js import './handle.css'; export function handleClick () { console.log('handleClick'); } ``` ``` // handle2.js export default function handleClick () { console.log('handleClick2'); } ``` `npm run build` 可以看到,多了这 `3` 个文件,并且只有在我们点击该按钮是才会去加载这 `3` 个文件。 ![](https://user-gold-cdn.xitu.io/2020/4/5/1714a5d94d596532?w=698&h=612&f=png&s=64226) ### webpackChunkName 这些文件可能不太好区分,我们可以通过设置 `webpackChunkName` 来定义生成的文件名 ``` // index-entry.js const handle = () => import(/* webpackChunkName: "handle" */ './handle'); const handle2 = () => import(/* webpackChunkName: "handle2" */ './handle2'); ``` 我们再将这些文件的 `hash` 长度设置为 `8` 加以区分 ``` // webpack.config.js module.exports = { output: { path: path.resolve(__dirname, 'dist'), // 输出目录 filename: '[name].[chunkhash:6].js', // 输出文件名 chunkFilename: '[name].[chunkhash:8].js' } // ... new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:6].css', chunkFilename: 'css/[name].[contenthash:8].css' }) } ``` `npm run build` 之后查看 ![](https://user-gold-cdn.xitu.io/2020/4/5/1714a6c41d8a4c44?w=690&h=698&f=png&s=78447) 当然我们也可以将 `handle` 和 `handle2` 文件的 `webpackChunkName` 设置成一样的,这样这两个文件将会打包在一起生成一个文件,可以减少请求数量。 ## 热更新( HMR, Hot Module Replacement ) 开发过程中,我们希望在浏览器不刷新页面的情况下能够去加载我们修改的代码,来提高我们的开发效率。我们来看下如何配置: 1. 打开 `webpack-dev-server` 的热更新开关 2. 使用 `HotModuleReplacementPlugin` 插件 `HotModuleReplacementPlugin` 插件是 `Webpack` 自带的,在 `webpack.config.js` 直接配置 ``` // webpack.config.js module.exports = { devServer: { //... hot: true }, plugins: [ //... new webpack.HotModuleReplacementPlugin() // 热更新插件 ] } ``` 在入口文件添加 ``` if (module && module.hot) { module.hot.accept() } ``` 这样就完成了热更新的配置,但是此时 `webpack` 打包却报错了。 ![](https://user-gold-cdn.xitu.io/2020/4/5/1714ae2e83151608?w=1249&h=343&f=png&s=56574) 搜了一下[相关的问题](https://stackoverflow.com/questions/50217480/cannot-use-chunkhash-or-contenthash-for-chunk-in-name-chunkhash-js-us),在开发环境中我们使用了 `HotModuleReplacementPlugin` 此时需要使用 `hash` 来输出文件,使用 `chunkhash` 会导致 `webpack` 报错,而生产环境则没有问题。但是现在我们只是通过 `process.env.NODE_ENV` 这个变量来区分环境,这显然不是一个很好的方式。 我们最好能够需要区分一下开发环境和生产环境的配置文件。 ## 定义不同环境的配置 我们可以给不同的环境定义不同的配置文件,但是这些文件将会有大量相似的配置,这时我们可以这样来定义文件: 1. `webpack.base.js`:定义公共的配置 2. `webpack.dev.js`:定义开发环境的配置 3. `webpack.prod.js`:定义生产环境的配置 我们可以将一些公共的配置抽离到 `webpack.base.js`,然后在 `webpack.dev.js` 和 `webpack.prod.js` 进行对应环境的配置。我们还需要通过 `webpack-merge` 来合并两个配置文件。 安装 `webpack-merge` ``` npm i -D webpack-merge ``` 现在 `webpack.dev.js` 就是这样的 ``` // webpack.dev.js const path = require('path'); const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const merge = require('webpack-merge'); const baseConfig = require('./webpack.config.base'); module.exports = merge(baseConfig, { mode: 'development', devtool: 'inline-source-map', devServer: { contentBase: path.join(__dirname, 'dist'), port: '9000', // 默认是8080 compress: true, // 是否启用 gzip 压缩 hot: true }, output: { path: path.resolve(__dirname, 'dist'), // 输出目录 filename: '[name].[hash:6].js', // 输出文件名 chunkFilename: '[name].[hash:8].js' }, plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].[hash:6].css', chunkFilename: 'css/[name].[hash:8].css' }), new webpack.HotModuleReplacementPlugin() // 热更新插件 ] }); ``` 同时需要在 `package.json` 中指定我们的配置文件 ```js // package.json "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js", "build": "cross-env NODE_ENV=production webpack --config webpack.config.pro.js" }, ``` 这时我们就很优雅的区分开不同环境的配置了。 ## 拷贝静态资源 有时候我们需要在 `html` 中直接引用一个打包好的第三方插件库,这个库不需要通过 `webpack` 编译。比如我们 `lib` 目录下有个 `lib-a.js`,需要在 `public/index.html` 中直接引用它。 ```html ``` 这时 `build` 之后会发现 `dist` 下是没有 `lib` 目录的,这时会找不到这个文件。这时我们需要借助 `CopyWebpackPlugin` 这个插件来帮助我们把根目录下的 `lib` 目录拷贝到 `dist` 目录下面。 首先安装 `CopyWebpackPlugin` ``` npm i -D CopyWebpackPlugin ``` 配置 `webpack.config.js` ``` // webpack.config.js const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { //... plugins: [ //... new CopyWebpackPlugin([ { from: path.resolve(__dirname, 'lib'), to: path.resolve(__dirname, 'dist/lib') } ]) ] } ``` 这时后运行 `npm run build` 就会发现,`dist` 目录下已经有了 `lib`目录及文件了。 更多的配置请查看[copy-webpack-plugin](https://www.webpackjs.com/plugins/copy-webpack-plugin/)。 ## Resolve 配置 `Webpack` 在启动后会从配置的入口模块出发找出所有依赖的模块,`Resolve` 配置 `Webpack` 如何寻找模块所对应的文件。 `Webpack` 内置 `JavaScript` 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你也可以根据自己的需要修改默认的规则。 ### alias `resolve.alias` 配置项通过别名来把原导入路径映射成一个新的导入路径。 比如我们在 `index-entry.js` 中引入 `lib/lib-b.js`,你可能需要这样引入 ```js import '../lib/lib-b.js'; ``` 而当目录层级比较深时,这个相对路径就会变得不好辨认了。这时我们可以配置 `lib` 的一个别名。 ``` // webpack.config.js module.exports = { //... resolve: { alias: { '@lib': path.resolve(__dirname, 'lib') // 为lib目录添加别名 } } } ``` 这时无论你处于目录的哪个层级,你只需要这样引入 ```js import '@lib/lib-b.js'; ``` ### extensions 如果在导入文件时没有带后缀名,`webpack` 会自动带上后缀后去尝试访问文件是否存在。 `resolve.extensions` 用于配置在尝试过程中用到的后缀列表,默认是 ``` extensions: ['.js', '.json'] ``` 就是说当遇到 `import '@lib/lib-b';` 时,`webpack` 会先去寻找 `@lib/lib-b.js` 文件,如果该文件不存在就去寻找 `@lib/lib-b.json` 文件, 如果还是找不到就报错。 如果你想优先使用其他后缀文件,比如 `.ts` 文件,可以这样配置 ``` // webpack.config.js module.exports = { //... resolve: { alias: { '@lib': path.resolve(__dirname, 'lib'), // 为lib目录添加别名 extensions: ['.ts', '.js', '.json'] // 从左往右 } } } ``` 这样就会先去找 `.ts` 了。不过一般我们会将高频的后缀放在前面,并且数组不要太长,减少尝试次数,不然会影响打包速度。 现在我们引入 `js` 文件时可以省略后缀名了。 ### modules `resolve.modules` 配置 `webpack` 去哪些目录下寻找第三方模块,默认是只会去 `node_modules` 目录下寻找。如果项目中某个文件夹下的模块经常被导入,不希望写很长的路径,比如 `import '../../../components/link'`,那么就可以通过配置 `resolve.modules` 来简化。 ``` // webpack.config.js module.exports = { //... resolve: { modules: ['./src/components', 'node_modules'] // 从左到右查找 } } ``` 这时,你就可以通过 `import 'link'` 引入了。 ### mainFields 有一些第三方模块会针对不同环境提供几份代码。例如分别提供采用 `es5` 和 `es6` 的 `2` 份代码,这 `2` 份代码的位置写在 `package.json` 文件里。 ``` { "jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件 "main": "lib/index.js" // 采用 ES5 语法的代码入口文件 } ``` `webpack` 会根据 `mainFields` 的配置去决定优先采用那份代码, `mainFields` 默认配置如下: ``` mainFields: ['browser', 'main'] ``` 假如你想优先采用 `ES6` 的那份代码,可以这样配置: ``` mainFields: ['jsnext:main', 'browser', 'main'] ``` ### enforceExtension `resolve.enforceExtension` 如果配置为 `true`,那么所有导入语句都必须要带文件后缀。 ### enforceModuleExtension `enforceModuleExtension` 和 `enforceExtension` 作用类似,但 `enforceModuleExtension` 只对 `node_modules`下的模块生效。 因为安装的第三方模块中大多数导入语句没带文件后缀,如果这时你配置了 `enforceExtension` 为 `true`,那么就需要配置 `enforceModuleExtension: false`来兼容第三方模块。 ## 利用 webpack 解决跨域问题 本地开发时,前端项目的端口号是 `9000`,但是服务端可能是 `9001`,根据浏览器的同源策略,是不能直接请求到后端服务的。当然你可以在后端配置 `CORS` 相关的头部来实现跨域,其实也可以通过 `webpack` 的配置来解决跨域问题。 首先,我们起一个后端服务,安装 `koa`、`koa-router` ``` npm i -D koa koa-router ``` 新建 `server/index.js` ``` // server/index.js const Koa = require('koa'); const KoaRouter = require('koa-router'); const app = new Koa(); // 创建 router 实例对象 const router = new KoaRouter(); // 注册路由 router.get('/user', async (ctx, next) => { ctx.body = { code: 0, data: { name: '阿林十一' }, msg: 'success' }; }); app.use(router.routes()); // 添加路由中间件 app.use(router.allowedMethods()); // 对请求进行一些限制处理 app.listen(9001); ``` 使用 `node server/index.js` 启动服务后,在 `http://localhost:9001/user` 可以访问结果。 之后再修改 `handle.js`,在点击按钮之后会请求接口 ``` import './handle.css'; export function handleClick () { console.log('handleClick'); fetch('/api/user') .then(r => r.json()) .then(data => console.log(data)) .catch(err => console.log(err)); } ``` 这是会发现接口报 `404`,下面我们配置一下 `webpack.config.dev.js` ``` // webpack.config.dev.js module.exports = { //... proxy: { '/api': { target: 'http://127.0.0.1:9001/', pathRewrite: { '^/api': '' } } } } ``` 请求到 `http://localhost:9000/api/user` 现在会被代理到请求 `http://localhost:9001/user`。点击按钮发起请求: ![](https://user-gold-cdn.xitu.io/2020/4/6/1714ec3f2b47130c?w=1790&h=264&f=png&s=57146) ## 最后 现在,我们对 `webpack` 的配置有了更进一步的了解了,快动手试试吧。本文所有代码可以查看 [github](https://github.com/2944927590/webpack-practice/tree/master/webpack-demo2)。 后续将会继续推出 `webpack` 系列的其他内容哦~ 喜欢本文的话点个赞吧~ ![](https://user-gold-cdn.xitu.io/2020/4/6/1714ecf558f98914?w=258&h=258&f=jpeg&s=28268) 更多精彩内容,欢迎关注微信公众号~

加载全部内容

相关教程
猜你喜欢
用户评论