背景:

前端打包的历史
    在很长的一段前端历史里,是没有打包的。那个时候页面基本是纯静态的或者服务端输出的,没有 AJAX,也没有 jQuery。
    随后,人们开始尝试在一个页面里做更多的事情。随着XMLHttpRequest, 也就是我们俗称的 AJAX, 这是一个使用方便的,兼容性良好的服务器通信接口。从此开始,我们的页面开始玩出各种花来了,前端一下子出现了各种各样的库,Prototype、Dojo、MooTools、Ext JS、jQuery…… 我们开始往页面里插入各种库和插件,我们的 js 文件也就爆炸了。
    随后js 文件压缩合并工具陆陆续续诞生了,压缩工具是有了,但我们得要执行它,最简单的办法呢,就是 windows 上搞个 bat 脚本,mac / linux 上搞个 bash 脚本,哪几个文件要合并在一块的,哪几个要压缩的,发布的时候运行一下脚本,生成压缩后的文件。
    如果有跨平台需求的话,windows 要装个可以执行 bash 脚本的命令行工具,比如 msys(目前最新的是 msys2),或者使用 php 或 python 等其他语言的脚本来编写,对于非全栈型的前端程序员来说,写 bash / php / python 还是很生涩的。因此我们需要一个简单的打包工具,可以利用各种编译工具,编译 / 压缩 js、css、html、图片等资源。随后,打包工具就出现gulp\webpack就出现了。
    依托 AMD 模块化编程,SPA(Single-page application) 的实现方式更为简单清晰,一个网页不再是传统的类似 word 文档的页面,而是一个完整的应用程序。SPA 应用有一个总的入口页面,我们通常把它命名为 index.html、app.html、main.html,这个 html 的 一般是空的,或者只有总的布局(layout),比如下图:

    布局会把 header、nav、footer 的内容填上,但 main 区域是个空的容器。这个作为入口的 html 最主要的工作是加载启动 SPA 的 js 文件,然后由 js 驱动,根据当前浏览器地址进行路由分发,加载对应的 AMD 模块,然后该 AMD 模块执行,渲染对应的 html 到页面指定的容器内(比如图中的 main)。在点击链接等交互时,页面不会跳转,而是由 js 路由加载对应的 AMD 模块,然后该 AMD 模块渲染对应的 html 到容器内。

一、先上手一个简单的SPA应用

先弄个简单的webpack配置来热一下身。
安装Node.js
webpack是基于Node.js的打包工具。
初始化一个项目
    先建一个文件夹叫simple,然后,在这里面搭项目。可以先看一下目录结构:

├── dist                      打包输出目录,只需部署这个目录到生产环境
├── package.json              项目配置信息
├── node_modules              npm 安装的依赖包都在这里面
├── src                       我们的源代码
│   ├── components            可以复用的模块放在这里面
│   ├── index.html            入口 html
│   ├── index.js              入口 js
│   ├── shared                公共函数库
│   └── views                 页面放这里
└── webpack.config.js         webpack 配置文件

给项目加上语法报错和代码规范检查

npm install eslint eslint-config-enough babel-eslint eslint-loader --save-dev

安装 webpack 和 Babel

npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev

配置webpack
创建 webpack 配置文件 webpack.config.js,注意这个文件是在 node.js 中运行的,因此不支持 ES6 的 import 语法。我们来看文件内容:

const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const history = require('connect-history-api-fallback')
const convert = require('koa-connect')// 使用 WEBPACK_SERVE 环境变量检测当前是否是在 webpack-server 启动的开发环境中
const dev = Boolean(process.env.WEBPACK_SERVE)module.exports = {/*webpack 执行模式development:开发环境,它会在配置文件中插入调试相关的选项,比如 moduleId 使用文件路径方便调试production:生产环境,webpack 会将代码做压缩等优化*/mode: dev ? 'development' : 'production',/*配置 source map开发模式下使用 cheap-module-eval-source-map, 生成的 source map 能和源码每行对应,方便打断点调试生产模式下使用 hidden-source-map, 生成独立的 source map 文件,并且不在 js 文件中插入 source map 路径,用于在 error report 工具中查看 (比如 Sentry)*/devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map',// 配置页面入口 js 文件entry: './src/index.js',// 配置打包输出相关output: {// 打包输出目录path: resolve(__dirname, 'dist'),// 入口 js 的打包输出文件名filename: 'index.js'},module: {/*配置各种类型文件的加载器,称之为 loaderwebpack 当遇到 import ... 时,会调用这里配置的 loader 对引用的文件进行编译*/rules: [{/*使用 babel 编译 ES6 / ES7 / ES8 为 ES5 代码使用正则表达式匹配后缀名为 .js 的文件*/test: /\.js$/,// 排除 node_modules 目录下的文件,npm 安装的包不需要编译exclude: /node_modules/,/*use 指定该文件的 loader, 值可以是字符串或者数组。这里先使用 eslint-loader 处理,返回的结果交给 babel-loader 处理。loader 的处理顺序是从最后一个到第一个。eslint-loader 用来检查代码,如果有错误,编译的时候会报错。babel-loader 用来编译 js 文件。*/use: ['babel-loader', 'eslint-loader']},{// 匹配 html 文件test: /\.html$/,/*使用 html-loader, 将 html 内容存为 js 字符串,比如当遇到import htmlString from './template.html';template.html 的文件内容会被转成一个 js 字符串,合并到 js 文件里。*/use: 'html-loader'},{// 匹配 css 文件test: /\.css$/,/*先使用 css-loader 处理,返回的结果交给 style-loader 处理。css-loader 将 css 内容存为 js 字符串,并且会把 background, @font-face 等引用的图片,字体文件交给指定的 loader 打包,类似上面的 html-loader, 用什么 loader 同样在 loaders 对象中定义,等会下面就会看到。*/use: ['style-loader', 'css-loader']},{/*匹配各种格式的图片和字体文件上面 html-loader 会把 html 中 <img> 标签的图片解析出来,文件名匹配到这里的 test 的正则表达式,css-loader 引用的图片和字体同样会匹配到这里的 test 条件*/test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,/*使用 url-loader, 它接受一个 limit 参数,单位为字节(byte)当文件体积小于 limit 时,url-loader 把文件转为 Data URI 的格式内联到引用的地方当文件大于 limit 时,url-loader 会调用 file-loader, 把文件储存到输出目录,并把引用的文件路径改写成输出后的路径比如 views/foo/index.html 中<img src="smallpic.png">会被编译成<img src="https://img-blog.csdnimg.cn/2022010612535544178.png">而<img src="largepic.png">会被编译成<img src="/f78661bef717cf2cc2c2e5158f196384.png">*/use: [{loader: 'url-loader',options: {limit: 10000}}]}]},/*配置 webpack 插件plugin 和 loader 的区别是,loader 是在 import 时根据不同的文件名,匹配不同的 loader 对这个文件做处理,而 plugin, 关注的不是文件的格式,而是在编译的各个阶段,会触发不同的事件,让你可以干预每个编译阶段。*/plugins: [/*html-webpack-plugin 用来打包入口 html 文件entry 配置的入口是 js 文件,webpack 以 js 文件为入口,遇到 import, 用配置的 loader 加载引入文件但作为浏览器打开的入口 html, 是引用入口 js 的文件,它在整个编译过程的外面,所以,我们需要 html-webpack-plugin 来打包作为入口的 html 文件*/new HtmlWebpackPlugin({/*template 参数指定入口 html 文件路径,插件会把这个文件交给 webpack 去编译,webpack 按照正常流程,找到 loaders 中 test 条件匹配的 loader 来编译,那么这里 html-loader 就是匹配的 loaderhtml-loader 编译后产生的字符串,会由 html-webpack-plugin 储存为 html 文件到输出目录,默认文件名为 index.html可以通过 filename 参数指定输出的文件名html-webpack-plugin 也可以不指定 template 参数,它会使用默认的 html 模板。*/template: './src/index.html',/*因为和 webpack 4 的兼容性问题,chunksSortMode 参数需要设置为 nonehttps://github.com/jantimon/html-webpack-plugin/issues/870*/chunksSortMode: 'none'})]
}/*
配置开发时用的服务器,让你可以用 http://127.0.0.1:8080/ 这样的 url 打开页面来调试
并且带有热更新的功能,打代码时保存一下文件,浏览器会自动刷新。比 nginx 方便很多
如果是修改 css, 甚至不需要刷新页面,直接生效。这让像弹框这种需要点击交互后才会出来的东西调试起来方便很多。因为 webpack-cli 无法正确识别 serve 选项,使用 webpack-cli 执行打包时会报错。
因此我们在这里判断一下,仅当使用 webpack-serve 时插入 serve 选项。
issue:https://github.com/webpack-contrib/webpack-serve/issues/19
*/
if (dev) {module.exports.serve = {// 配置监听端口,默认值 8080port: 8080,// add: 用来给服务器的 koa 实例注入 middleware 增加功能add: app => {/*配置 SPA 入口SPA 的入口是一个统一的 html 文件,比如http://localhost:8080/foo我们要返回给它http://localhost:8080/index.html这个文件*/app.use(convert(history()))}}
}

二、进阶配置

上面的项目虽然可以跑起来了,但有几个点我们还没有考虑到:

  • 设置静态资源的 url 路径前缀;
  • 各个页面分开打包;
  • 第三方库和业务代码分开打包;
  • 输出的 entry 文件加上 hash;
  • 开发环境关闭 performance.hints;
  • 配置 favicon;
  • 开发环境允许其他电脑访问;
  • 打包时自定义部分参数;
  • webpack-serve 处理路径带后缀名的文件的特殊规则;
  • 代码中插入环境变量;
  • 简化 import 路径;
  • 优化 babel 编译后的代码性能;
  • 使用 webpack 自带的 ES6 模块处理功能;
  • 使用 autoprefixer 自动创建 css 的 vendor prefixes

三、使用webpack打包多页面应用(Multiple-Page Application)

多页面网站,同样可以用webpack来打包,以便使用npm包,import(),code splitting等好处。
    MPA意味着并没不是一个单一的html入口和js入口,而是每个页面对应一个html和多个js。那么,我们可以把项目结构设计为:
├── dist
├── package.json
├── node_modules
├── src
│ ├── components
│ ├── shared
| ├── favicon.png
│ └── pages 页面放这里
| ├── foo 编译后生成 http://localhost:8080/foo.html
| | ├── index.html
| | ├── index.js
| | ├── style.css
| | └── pic.png
| └── bar http://localhost:8080/bar.html
| ├── index.html
| ├── index.js
| ├── style.css
| └── baz http://localhost:8080/bar/baz.html
| ├── index.html
| ├── index.js
| └── style.css
└── webpack.config.js

这里每个页面的 index.html 是个完整的从 开头到 结束的页面,这些文件都要用 html-webpack-plugin 处理。index.js 是每个页面的业务逻辑,作为每个页面的入口 js 配置到 entry 中。这里我们需要用 glob 库来把这些文件都筛选出来批量操作。为了使用 webpack 4 的 optimization.splitChunks 和 optimization.runtimeChunk 功能,我写了 html-webpack-include-sibling-chunks-plugin 插件来配合使用。还要装几个插件把 css 压缩并放到 中。

npm install glob html-webpack-include-sibling-chunks-plugin uglifyjs-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin --save-dev

webpack.config.js 修改的地方:

// ...
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackIncludeSiblingChunksPlugin = require('html-webpack-include-sibling-chunks-plugin')
const glob = require('glob')const dev = Boolean(process.env.WEBPACK_SERVE)
const config = require('./config/' + (process.env.npm_config_config || 'default'))const entries = glob.sync('./src/**/index.js')
const entry = {}
const htmlPlugins = []
for (const path of entries) {const template = path.replace('index.js', 'index.html')const chunkName = path.slice('./src/pages/'.length, -'/index.js'.length)entry[chunkName] = dev ? [path, template] : pathhtmlPlugins.push(new HtmlWebpackPlugin({template,filename: chunkName + '.html',chunksSortMode: 'none',chunks: [chunkName]}))
}module.exports = {entry,output: {path: resolve(__dirname, 'dist'),// 我们不定义 publicPath,否则访问 html 时需要带上 publicPath 前缀filename: dev ? '[name].js' : '[chunkhash].js',chunkFilename: '[chunkhash].js'},optimization: {runtimeChunk: true,splitChunks: {chunks: 'all'},minimizer: dev ? [] : [new UglifyJsPlugin({cache: true,parallel: true,sourceMap: true}),new OptimizeCSSAssetsPlugin()]},module: {rules: [// ...{test: /\.css$/,use: [dev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']},// ...]},plugins: [// .../*这里不使用 [chunkhash]因为从同一个 chunk 抽离出来的 css 共享同一个 [chunkhash][contenthash] 你可以简单理解为 moduleId + content 生成的 hash因此一个 chunk 中的多个 module 有自己的 [contenthash]*/new MiniCssExtractPlugin({filename: '[contenthash].css',chunkFilename: '[contenthash].css'}),// 必须放在html-webpack-plugin前面new HtmlWebpackIncludeSiblingChunksPlugin(),...htmlPlugins],// ...
}

entry 和 htmlPlugins 会通过遍历 pages 目录生成,比如:
entry:

{'bar/baz': './src/pages/bar/baz/index.js',bar: './src/pages/bar/index.js',foo: './src/pages/foo/index.js'
}

在开发环境中,为了能够修改 html 文件后网页能够自动刷新,我们还需要把 html 文件也加入 entry 中,比如:

{foo: ['./src/pages/foo/index.js', './src/pages/foo/index.html']
}

这样,当 foo 页面的 index.js 或 index.html 文件改动时,都会触发浏览器刷新该页面。虽然把 html 加入 entry 很奇怪,但放心,不会导致错误。记得不要在生产环境这么做,不然导致 chunk 文件包含了无用的 html 片段。
htmlPlugins:

[new HtmlWebpackPlugin({template: './src/pages/bar/baz/index.html',filename: 'bar/baz.html',chunksSortMode: 'none',chunks: ['bar/baz']},new HtmlWebpackPlugin({template: './src/pages/bar/index.html',filename: 'bar.html',chunksSortMode: 'none',chunks: ['bar']},new HtmlWebpackPlugin({template: './src/pages/foo/index.html',filename: 'foo.html',chunksSortMode: 'none',chunks: ['foo']}
]

参考博客:
    Webpack 4 和单页应用入门 https://github.com/wallstreetcn/webpack-and-spa-guide

【学习笔记】Webpack4和单页应用入门相关推荐

  1. vue学习笔记-03-浅谈组件-概念,入门,如何用props给组件传值?

    vue学习笔记-03-浅谈组件-概念,入门,如何用props给组件传值? 文章目录 vue学习笔记-03-浅谈组件-概念,入门,如何用props给组件传值? 什么是组件? 为什么要使用组件? 如何使用 ...

  2. Windows保护模式学习笔记(八)—— 页目录表基址/页表基址

    Windows保护模式学习笔记(八)-- 页目录表基址/页表基址 要点回顾 一.页目录表基址 实验:拆分线性地址C0300000,并查看其对应的物理页 第一步:打开一个进程,获得它的Cr3 第二步:查 ...

  3. python表单提交的两种方式_Flask框架学习笔记之表单基础介绍与表单提交方式

    本文实例讲述了Flask框架学习笔记之表单基础介绍与表单提交方式.分享给大家供大家参考,具体如下: 表单介绍 表单是HTML页面中负责数据采集功能的部件.由表单标签,表单域和表单按钮组成.通过表单,将 ...

  4. ASP.NET MVC 2 学习笔记二: 表单的灵活提交

    ASP.NET MVC 2 学习笔记二:  表单的灵活提交 前面说到有做到公司内部的一个请假系统,用的是ASP.NET MVC 2+Entity Framework.虽然EF(Entity Frame ...

  5. 个人学习笔记——庄懂的技术美术入门课(美术向)19

    个人学习笔记--庄懂的技术美术入门课(美术向)19 1 顶点平移 2 顶点缩放 3 顶点旋转 4 综合应用 1 顶点平移 2 顶点缩放 方法类似 避免产生负值 3 顶点旋转 方法类似 以下是涉及到的一 ...

  6. 个人学习笔记——庄懂的技术美术入门课(美术向)01

    个人学习笔记--庄懂的技术美术入门课(美术向)01 0 前言 1 工程搭建示范 2 理论 2.1 结构(struct) 2.2 渲染管线 3 操作 3.1-2 向量/标量/点积等若干线代基础 3.3 ...

  7. 个人学习笔记——庄懂的技术美术入门课(美术向)07

    个人学习笔记--庄懂的技术美术入门课(美术向)07 1 单色环境光 2 三色环境光 3 投影 4 光照模型组合 有关AO的知识之前涉及到就是 SSAO的实现了,可以回顾下 1 单色环境光 环境光加上环 ...

  8. flink1.12.0学习笔记第1篇-部署与入门

    flink1.12.0学习笔记第 1 篇-部署与入门 flink1.12.0学习笔记第1篇-部署与入门 flink1.12.0学习笔记第2篇-流批一体API flink1.12.0学习笔记第3篇-高级 ...

  9. 个人学习笔记——庄懂的技术美术入门课(美术向)02

    个人学习笔记--庄懂的技术美术入门课(美术向)02 1 作业点评 2 作业批改 2.1 作业1 2.1.1 模拟高光 2.1.2 菲涅尔 2.1.3 叠加模式 2.2 作业2 2.2.1 关于屏幕UV ...

最新文章

  1. 我给 Apache 顶级项目提了个 Bug
  2. 锐捷网络GSN全局安全政府行业解决方案
  3. 浏览器angent分析工具
  4. .NET通用基本权限系统
  5. Gym100917 A - Abstract Picture
  6. 《android基于andFix的热修复方案》思路篇
  7. java能不能不用jvm_Java、JVM和操作系统之间的关系,写给新人,
  8. 配置多个ssh-key
  9. c ++创建二维数组_C ++中的二维数组
  10. 最严格的身份证校验(JavaScript版)
  11. unhandled exception in MSDEV.EXE(DEVSHL.DLL) :0xC0000005
  12. 计算机网络中的冗余的意思,计算机网络基础 实验四 交换网络中冗余链路.ppt
  13. Adobe dreamweaver CS6小白入门教程
  14. php shopex,用PHP为SHOPEX增加日志功能代码
  15. 解除WORD文档保护
  16. 关于汽车诊断OBD的理解(ISO15031-5)
  17. Linux简介与安装
  18. Codeforces Round #807 (Div. 2) A-C题解
  19. Python 数据可视化基础教程
  20. 港湾公园 Haven Park for Mac(好玩的探索建造冒险游戏)

热门文章

  1. S5pv210 android 的 一些时钟设置太蛋疼了,找了好久才找到,要改一个频率牵扯太多,远不如wince方便
  2. 鸿蒙上线发布会,华为6月2日线上新品发布会开启-鸿蒙系统正式上线
  3. MFS分布式文件系统存储之文件的删除恢复
  4. Python爬取张国荣最火的8首歌,60000评论看完泪奔!
  5. 数据分析最常用的36个Excel函数
  6. 微信公众号php发送图片素材,php版微信公众号接口实现发红包的方法
  7. java面试题,看我这篇就够了,前端后台应有尽有,包你通过面试
  8. [Android开发那点破事]解决android.os.NetworkOnMainThreadException
  9. 2018年英语计算机职称考试成绩,2018年重庆职称英语考试成绩查询时间及入口
  10. 网络工程专业毕业设计选题