webpack发展史
- 在没有ajax和jQuery之前,前端是不存在打包这个说法的,js没有大规模使用,只做简单的时钟、mp3等效果,直接弄一个js文件引入就行
- 之后,人们开始使用iframe和flash等于服务器通信,因为这两种方式太过于tricky(棘手),直到google退出gmail的时候,人们发现了XMLHttpRequest,也就是AJAX,从此开始,前端出现了jquery等各种插件和库,js文件越来越多。
- 随着js做的事越来越多,js文件越来越大,U管理费用US等js文件压缩合并工具陆续诞生,但是也有很多问题,比如:
- 库和插件为了要给他人调用,肯定要找个地方注册,一般就是在 window 下申明一个全局的函数或对象。难保哪天用的两个库在全局用同样的名字,那就冲突了
- 库和插件如果还依赖其他的库和插件,就要告知使用人,需要先引哪些依赖库,那些依赖库也有自己的依赖库的话,就要先引依赖库的依赖库,以此类推
- 2009年,后端js发展,人们提出了CommonJS模块化规范,也就是exports和require语法。但是它并不适用于浏览器,require是同步的,堵塞js脚本的执行,所以人们基于CommonJS定义了AMD规范(2011年),使用异步回调的语法来并行下载多个依赖项,也就是define函数(必须返回值)。现在出了ES6,7之后,已经差不多淘汰AMD了
- 2012年,webpack诞生,Browserify同期诞生,但webpack比它的优点:多文件打包、可以关心所有文件的打包、对资源文件的加载支持完善、支持CommonJS、AMD和ES6模块规范等
简单搞一个SPA应用
页面搭建
- 接下来我们简单搞一个SPA应用,弄一个webpack配置
- 首先,本地肯定需要安装nodeJS,因为webpack是基于nodeJS的
- 然后,搭建项目目录,npm init初始化package.json文件,晒一下目录结构
├─ .eslintrc.js ├─ .gitignore ├─ dist │ ├─ index.html │ ├─ index.js │ └─ index.js.map ├─ index.html ├─ package-lock.json ├─ package.json ├─ src │ ├─ index.js │ ├─ router.js │ ├─ untils │ │ └─ axios.js │ └─ views │ ├─ about │ └─ home └─ webpack.config.js
- 接下来,需要安装依赖的包了
- 先安装eslint进行语法检查
npm install eslint eslint-config-enough babel-eslint eslint-loader --save-dev
// package.json里面插入如下配置 "eslintConfig": { "extends": "enough", "env": { "browser": true, "node": true } }
- 先安装eslint进行语法检查
- 然后编辑index.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>首页</title> </head> <body> <div id="app">首页</div> </body> </html>
- 编辑src下的index.js文件
// 引入 router import router from './router' // 启动 router router.start()
- 编辑src下的router.js文件
// 引入页面文件 import home from './views/home' import about from './views/about' const routes = { '/home': home, '/about': about } // Router 类,用来控制页面根据当前 URL 切换 class Router { start () { // 点击浏览器后退 / 前进按钮时会触发 window.onpopstate 事件,我们在这时切换到相应页面 // https://developer.mozilla.org/en-US/docs/Web/Events/popstate window.addEventListener('popstate', () => { this.load(window.location.pathname) }) // 打开页面时加载当前页面 this.load(window.location.pathname) } // 前往 path,变更地址栏 URL,并加载相应页面 go (path) { // 变更地址栏 URL window.history.pushState({}, '', path) // 加载页面 this.load(path) } // 加载 path 路径的页面 load (path) { // 首页 if (path === '/') path = '/home' // 创建页面实例 const view = new routes[path]() // 调用页面方法,把页面加载到 document.body 中 view.mount(document.body) } } // 导出 router 实例 export default new Router()
- 然后在src的view下建立home和about两个文件夹,并在俩文件夹下新建index.js文件和style.css文件
// home下的index.js举例 // 引入 router import router from '../../router' // 引入 html 模板,会被作为字符串引入 import template from '../../../index.html' // 引入 css, 会生成 <style> 块插入到 <head> 头中 import './style.css' // 导出类 export default class { mount (container) { document.title = 'foo' container.innerHTML = template container.querySelector('#app').addEventListener('click', () => { // 调用 router.go 方法加载 /bar 页面 router.go('/about') }) } }
webpack配置
-
安装webpack和它的插件
npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev
- 安装babel以支持打包生成ES5
npm install @babel/core @babel/preset-env babel-loader --save-dev
- 在package.json里面添加配置项
"babel": { "presets": ["env"] }
- 配置webpack
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: { /* 配置各种类型文件的加载器,称之为 loader webpack 当遇到 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/2022010701245379171.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 就是匹配的 loader html-loader 编译后产生的字符串,会由 html-webpack-plugin 储存为 html 文件到输出目录,默认文件名为 index.html 可以通过 filename 参数指定输出的文件名 html-webpack-plugin 也可以不指定 template 参数,它会使用默认的 html 模板。 */ template: './src/index.html', /* 因为和 webpack 4 的兼容性问题,chunksSortMode 参数需要设置为 none https://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 = { // 配置监听端口,默认值 8080 port: 8080, // add: 用来给服务器的 koa 实例注入 middleware 增加功能 add: app => { /* 配置 SPA 入口 SPA 的入口是一个统一的 html 文件,比如 http://localhost:8080/foo 我们要返回给它 http://localhost:8080/index.html 这个文件 */ app.use(convert(history())) } } }
- 启动和打包
- 用命令行启动 node_modules\.bin\webpack-serve webpack.config.js
- 用命令行打包 /node_modules/.bin/webpack-cli
- script配置
- 每次都写那么多有点麻烦,用script快捷命令入口启动,在package.json里面配置
"scripts": { "dev": "webpack-serve webpack.config.js", "build": "webpack-cli" }
- 每次都写那么多有点麻烦,用script快捷命令入口启动,在package.json里面配置
进阶配置
-
以上就完成了简单的配置,但是还是有很多点可以优化,比如 设置静态资源的 url 路径前缀、各个页面分开打包、第三方库和业务代码分开打包、输出的 entry 文件加上 hash、开发环境关闭 performance.hints、配置 favicon、开发环境允许其他电脑访问、打包时自定义部分参数、webpack-serve 处理路径带后缀名的文件的特殊规则、代码中插入环境变量、简化 import 路径、优化 babel 编译后的代码性能、使用 webpack 自带的 ES6 模块处理功能、使用 autoprefixer 自动创建 css 的 vendor prefixes