上手先搞一个简单的 SPA 应用
一上来步子太大容易扯到蛋,让我们先弄个最简单的 webpack 配置来热一下身。
安装 Node.js
webpack 是基于我大 Node.js 的打包工具,上来第一件事自然是先安装 Node.js 了,传送门 ->。
初始化一个项目
我们先随便找个地方,建一个文件夹叫 simple
, 然后在这里面搭项目。完成品在 examples/simple 目录,大家搞的时候可以参照一下。我们先看一下目录结构:
├── dist 打包输出目录,只需部署这个目录到生产环境
├── package.json 项目配置信息
├── node_modules npm 安装的依赖包都在这里面
├── src 我们的源代码
│ ├── components 可以复用的模块放在这里面
│ ├── index.html 入口 html
│ ├── index.js 入口 js
│ ├── shared 公共函数库
│ └── views 页面放这里
└── webpack.config.js webpack 配置文件
打开命令行窗口,cd
到刚才建的 simple 目录。然后执行这个命令初始化项目:
npm init
命令行会要你输入一些配置信息,我们这里一路按回车下去,生成一个默认的项目配置文件 package.json
。
给项目加上语法报错和代码规范检查
我们安装 eslint, 用来检查语法报错,当我们书写 js 时,有错误的地方会出现提示。
npm install eslint eslint-config-enough babel-eslint eslint-loader --save-dev
npm install
可以一条命令同时安装多个包,包之间用空格分隔。包会被安装进 node_modules
目录中。
--save-dev
会把安装的包和版本号记录到 package.json
中的 devDependencies
对象中,还有一个 --save
, 会记录到 dependencies
对象中,它们的区别,我们可以先简单的理解为打包工具和测试工具用到的包使用 --save-dev
存到 devDependencies
, 比如 eslint、webpack。浏览器中执行的 js 用到的包存到 dependencies
, 比如 jQuery 等。那么它们用来干嘛的?
因为有些 npm 包安装是需要编译的,那么导致 windows / mac /linux 上编译出的可执行文件是不同的,也就是无法通用,因此我们在提交代码到 git 上去的时候,一般都会在 .gitignore
里指定忽略 node_modules 目录和里面的文件,这样其他人从 git 上拉下来的项目是没有 node_modules 目录的,这时我们需要运行
npm install
它会读取 package.json
中的 devDependencies
和 dependencies
字段,把记录的包的相应版本下载下来。
这里 eslint-config-enough 是配置文件,它规定了代码规范,要使它生效,我们要在 package.json
中添加内容:
{
"eslintConfig": {
"extends": "enough",
"env": {
"browser": true,
"node": true
}
}
}
业界最有名的语法规范是 airbnb 出品的,但它规定的太死板了,比如不允许使用 for-of
和 for-in
等。感兴趣的同学可以参照 这里 安装使用。
babel-eslint 是 eslint-config-enough
依赖的语法解析库,替代 eslint 默认的解析库以支持还未标准化的语法。比如 import()。
eslint-loader 用于在 webpack 编译的时候检查代码,如果有错误,webpack 会报错。
项目里安装了 eslint 还没用,我们的 IDE 和编辑器也得要装 eslint 插件支持它。
Visual Studio Code 需要安装 ESLint 扩展
atom 需要安装 linter 和 linter-eslint 这两个插件,装好后重启生效。
WebStorm 需要在设置中打开 eslint 开关:
写几个页面
我们写一个最简单的 SPA 应用来介绍 SPA 应用的内部工作原理。首先,建立 src/index.html 文件,内容如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
</body>
</html>
它是一个空白页面,注意这里我们不需要自己写 <script src="index.js"></script>
, 因为打包后的文件名和路径可能会变,所以我们用 webpack 插件帮我们自动加上。
src/index.js:
// 引入 router
import router from './router'
// 启动 router
router.start()
src/router.js:
// 引入页面文件
import foo from './views/foo'
import bar from './views/bar'
const routes = {
'/foo': foo,
'/bar': bar
}
// Router 类,用来控制页面根据当前 URL 切换
class Router {
start() {
// 点击浏览器后退 / 前进按钮时会触发 window.onpopstate 事件,我们在这时切换到相应页面
// https://developer.mozilla.org/en-US/docs/Web/Events/popstate
window.addEventListener('popstate', () => {
this.load(location.pathname)
})
// 打开页面时加载当前页面
this.load(location.pathname)
}
// 前往 path,变更地址栏 URL,并加载相应页面
go(path) {
// 变更地址栏 URL
history.pushState({}, '', path)
// 加载页面
this.load(path)
}
// 加载 path 路径的页面
load(path) {
// 首页
if (path === '/') path = '/foo'
// 创建页面实例
const view = new routes[path]()
// 调用页面方法,把页面加载到 document.body 中
view.mount(document.body)
}
}
// 导出 router 实例
export default new Router()
src/views/foo/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('.foo__gobar').addEventListener('click', () => {
// 调用 router.go 方法加载 /bar 页面
router.go('/bar')
})
}
}
src/views/bar/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 = 'bar'
container.innerHTML = template
container.querySelector('.bar__gofoo').addEventListener('click', () => {
// 调用 router.go 方法加载 /foo 页面
router.go('/foo')
})
}
}
借助 webpack 插件,我们可以 import
html, css 等其他格式的文件,文本类的文件会被储存为变量打包进 js 文件,其他二进制类的文件,比如图片,可以自己配置,小图片作为 Data URI 打包进 js 文件,大文件打包为单独文件,我们稍后再讲这块。
其他的 src 目录下的文件大家自己浏览,拷贝一份到自己的工作目录,等会打包时会用到。
页面代码这样就差不多搞定了,接下来我们进入 webpack 的安装和配置阶段。现在我们还没有讲 webpack 配置所以页面还无法访问,等会弄好 webpack 配置后再看页面实际效果。
安装 webpack 和 Babel
我们把 webpack 和它的插件安装到项目:
npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev
webpack 即 webpack 核心库。它提供了很多 API, 通过 Node.js 脚本中 require('webpack')
的方式来使用 webpack。
webpack-cli 是 webpack 的命令行工具。让我们可以不用写打包脚本,只需配置打包配置文件,然后在命令行输入 webpack-cli --config webpack.config.js
来使用 webpack, 简单很多。webpack 4 之前命令行工具是集成在 webpack 包中的,4.0 开始 webpack 包本身不再集成 cli。
webpack-serve 是 webpack 提供的用来开发调试的服务器,让你可以用 http://127.0.0.1:8080/ 这样的 url 打开页面来调试,有了它就不用配置 nginx 了,方便很多。
html-webpack-plugin, html-loader, css-loader, style-loader 等看名字就知道是打包 html 文件,css 文件的插件,大家在这里可能会有疑问,html-webpack-plugin
和 html-loader
有什么区别,css-loader
和 style-loader
有什么区别,我们等会看配置文件的时候再讲。
file-loader 和 url-loader 是打包二进制文件的插件,具体也在配置文件章节讲解。
接下来,为了能让不支持 ES6 的浏览器 (比如 IE) 也能照常运行,我们需要安装 babel, 它会把我们写的 ES6 源代码转化成 ES5,这样我们源代码写 ES6,打包时生成 ES5。
npm install babel-core babel-preset-env babel-loader --save-dev
这里 babel-core
顾名思义是 babel 的核心编译器。babel-preset-env 是一个配置文件,我们可以使用这个配置文件转换 ES2015/ES2016/ES2017 到 ES5,是的,不只 ES6 哦。babel 还有 其他配置文件。
光安装了 babel-preset-env
,在打包时是不会生效的,需要在 package.json
加入 babel
配置:
{
"babel": {
"presets": ["env"]
}
}
打包时 babel 会读取 package.json
中 babel
字段的内容,然后执行相应的转换。
babel-loader 是 webpack 的插件,我们下面章节再说。
配置 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: {
/*
配置各种类型文件的加载器,称之为 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="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAA...">
而
<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()))
}
}
}
走一个
配置 OK 了,接下来我们就运行一下吧。我们先试一下开发环境用的 webpack-serve
:
./node_modules/.bin/webpack-serve webpack.config.js
执行时需要指定配置文件。
上面的命令适用于 Mac / Linux 等 * nix 系统,也适用于 Windows 上的 PowerShell 和 bash/zsh 环境(Windows Subsystem for Linux, Git Bash、Babun、MSYS2 等)。安利一下 Windows 同学使用 Ubuntu on Windows,可以避免很多跨平台的问题,比如设置环境变量。
如果使用 Windows 的 cmd.exe,请执行:
node_modules\.bin\webpack-serve webpack.config.js
npm 会把包的可执行文件安装到 ./node_modules/.bin/
目录下,所以我们要在这个目录下执行命令。
命令执行后,控制台显示:
「wdm」: Compiled successfully。
这就代表编译成功了,我们可以在浏览器打开 http://localhost:8080/
看看效果。如果有报错,那可能是什么地方没弄对?请自己仔细检查一下~
我们可以随意更改一下 src 目录下的源代码,保存后,浏览器里的页面应该很快会有相应变化。
要退出编译,按 ctrl+c
。
开发环境编译试过之后,我们试试看编译生产环境的代码,命令是:
./node_modules/.bin/webpack-cli
不需要指定配置文件,默认读取 webpack.config.js
执行脚本的命令有点麻烦,因此,我们可以利用 npm,把命令写在 package.json
中:
{
"scripts": {
"dev": "webpack-serve webpack.config.js",
"build": "webpack-cli"
}
}
package.json
中的 scripts
对象,可以用来写一些脚本命令,命令不需要前缀目录 ./node_modules/.bin/
,npm 会自动寻找该目录下的命令。我们可以执行:
npm run dev
来启动开发环境。
执行
npm run build
来打包生产环境的代码。