使用 webpack 搭建 vue 开发环境(三)
刚好断更一个月~~。不过我还是回来了!
老规矩,代码放在 码云@Jioho/webpack_config (opens new window) v0.0.3 分支,搞定后合并到 master
根据上次留下的锅,看下这次需要解决什么问题
- 当前项目的热更新是属于哪种热更新?
- 实现一个简单的多入口
本来还有 实现自动获取入口(重头戏)
和 添加代码运行后的 URL 输出
的。可是在写完多入口后,发现踩坑好多,已经记录了好多,这 2 个就继续放到下一个系列把~
# 小插曲
今天重新运行项目发现了一条红色提示
作为强迫症的我绝对不能忍的,这个提示意思也很清楚了,在 dev
模式下。不能使用 chunkhash
或者 contenthash
作为 js 的哈希值。而我们 dev 模式下的 output
输出不知道啥时候被我改错了把~造成了这样子,所以改一下配置:
- webpack.dev.js
module.exports = {
// ...
output: {
+ filename: '[name].[hash].js',
- filename: '[name].[contenthash].js',
publicPath: '/',
path: DIST
}
// ...
}
2
3
4
5
6
7
8
9
10
至于为什么开发环境的 js 不能用 contenthash
呢?其实也很简单,contenthash
是根据内容修改才重新生成的 hash 值,而 webpack 的宗旨就是万事万物最后都交给 js 处理,包括引入 css/引入其他内容
如果这时候我们只改动了 css
。理应 js 要重新加载新的 css,可是没检测到我们改写的 js 内容变化,所以 contenthash
就不会变,自然也不能引入新的 css。
改了之后重新运行就 OK 了
# 当前项目的热更新是属于哪种热更新?
# 第一种 WDM
WDM
webpack-dev-middleware 简称 WDM
编辑器修改了内容,通知浏览器,就是这个中间件发挥的作用
看一张 gif 效果
- 我们在输入框输入了一段文本
- 修改了 css 代码
- (我没按 F5 刷新)是 webpack 的热更新了
- 留意文本框的字!!没了!!
留意看几个地方:
- network 面板的请求
- console 控制台输出
- 输入框的字
- 文本颜色的变化
可以看到,触发热更新的步骤,
然后控制台先输出了
同时在请求 localhost:8080
注意这时候请求还没成功
在请求成功后,浏览器自动刷新了(因为控制台文本被清空了)
接着就请求 main.js,请求新的 css,最后在创建一个新的 websocket
来保持下一次的连接
看上去好像挺不错了,免去了人工刷新的一步,改了就能看到效果了,可是如果你开发的是表单页面,一大堆的输入框表单,输入了半天,最后发现 js/css 好像写错了一点东西,一改,刷新,全没了,心态都崩了,所以这种并不是最好的。
# 第二种 HMR
HMR
Hot Module Replacement(以下简称 HMR
)
是 webpack 发展至今引入的最令人兴奋的特性之一 ,当你对代码进行修改并保存后,webpack 将对代码重新打包,并将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下就能够对应用进行更新
看下啥特性能让程序猿那么兴奋
# 修改代码,实现 HRM
# 重新整理下 PATH.js
因为这次要用的目录还挺多的,重新看了下 PATH.js
文件,声明的变量挺多的,而且导出还得重新在复制一次,相对来说比较麻烦~
- build/PATH.js 修改前:
const path = require('path')
const ROOT = path.resolve(__dirname, '../')
const DIST = path.resolve(ROOT, 'dist')
const tmp_main = path.resolve(ROOT, 'src/main.js')
const TEMPLATE_HTML = path.resolve(ROOT, 'public/index.html')
module.exports = {
ROOT,
DIST,
TEMPLATE_HTML,
tmp_main
}
2
3
4
5
6
7
8
9
10
11
12
13
- build/PATH.js 修改后:
改了一下,把路径都统一放入 PATH 后导出,而且多加了几个配置文件的路径
const path = require('path')
const ROOT = path.resolve(__dirname, '../')
const PATH = {
ROOT: ROOT,
DIST: path.resolve(ROOT, 'dist'),
tmp_main: path.resolve(ROOT, 'src/main.js'),
TEMPLATE_HTML: path.resolve(ROOT, 'public/index.html'),
BASE_CONFIG: path.resolve(ROOT, 'build/webpack.base.js'),
DEV_CONFIG: path.resolve(ROOT, 'build/webpack.dev.js'),
PROD_CONFIG: path.resolve(ROOT, 'build/webpack.prod.js')
}
module.exports = PATH
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 理一下流程
我们需要自己用 express 运行一个服务(这也是为后面做铺垫的),然后使用 webpack 的 dev 模式,加热更新,让我们修改的文件能局部替换到浏览器中,尽可能不要全部刷新页面(只是尽可能,因为总有些情况会全部刷新的~)
不然不用 webpack-dev-serve
了。那我们运行的命令也得改一下,不能直接用 webpack 的命令了,而是自己用 node 运行,下面都会说到
# 安装依赖
要实现这个功能,我们需要安装 3 个包:express
、webpack-hot-middleware
、webpack-dev-middleware
webpack-dev-serve 可以卸载了,使用 npm uninstall webpack-dev-serve 当然也可以留到最后实现了才卸载,看个人把!
npm i express webpack-hot-middleware webpack-dev-middleware -D
- express 是为了给我们运行服务的,比如跑在指定端口
- webpack-dev-middleware 运行 webpack 的中间件
- webpack-hot-middleware 今天的主角,热更新就是他了
# 准备入口文件
新建: build/dev-server.js
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const { DEV_CONFIG } = require('./PATH')
const app = express()
const config = require(DEV_CONFIG)
const compiler = webpack(config)
const devMiddleware = webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
hot: true, // 开启热更新,浏览器自动刷新
quiet: true,
inline: true // 必须开启
})
// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(devMiddleware)
// 热更新
app.use(
webpackHotMiddleware(compiler, {
log: () => console.log
})
)
app.listen('8080', function() {
console.log('> Listening at 8080 !\n')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
要想达到热替换的效果,dev 模式也得添加一个 webpack 内置的插件,所以修改
- webpack.dev.js
其中要告诉 devServer
我们项目的路径位置(有没有都没关系,有些教程说要有)
然后就是添加 plugins
,添加一个 webpack.HotModuleReplacementPlugin 这个特别重要
const baseConfig = require('./webpack.base')
const { merge } = require('webpack-merge')
const path = require('path')
const { DIST } = require('./PATH')
const webpack = require('webpack')
module.exports = merge(baseConfig, {
mode: 'development',
output: {
filename: '[name].[hash].js',
publicPath: '/',
path: DIST
},
devServer: {
contentBase: DIST //指定需要提供给本地服务的内容的路径,默认加载index.html文件,可根据需要修改
},
plugins: [new webpack.HotModuleReplacementPlugin()] // 新增了这一行
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
有了 webpack.HotModuleReplacementPlugin 插件的支持,入口文件也得支持一下
- webpack.base.js
module.exports = {
// entry: tmp_main,
entry: { main: ['webpack-hot-middleware/client', tmp_main] } // 添加 webpack-hot-middleware/client 标识这是要热更新的文件
}
2
3
4
PS:等下记得把 'webpack-hot-middleware/client' 加一个判断,只有开发环境才打开,不然打包后的文件会出错的 main: devMode ? ['webpack-hot-middleware/client', tmp_main] : [tmp_main] 判断 devMode 在下面有讲到~
修改:package.json
把之前的 serve 命令改成是 dev,把 serve 留给现在的命令,使用 node 运行
注意没有后面的 --mode development
"scripts": {
"dev": "webpack-dev-server --config build/webpack.dev.js --mode development",
"serve": "node build/dev-server.js",
"build": "webpack --config build/webpack.prod.js --mode production"
},
2
3
4
5
OK,运行试下:
可能会遇到这种错误
GET http://localhost:8080/__webpack_hmr net::ERR_CONNECTION_RESET 200 (OK) Cannot find update (Probably because of restarting the server)
这种情况可能就真的只是网络问题了
BUT!!下面这种情况一定会遇到(这个问题我调试了一下午!)
# HRM 第一次热更新不起作用 / [HMR] Nothing hot updated.
如果按上面的步骤来写,很大几率会遇到这个问题,就是 webpack 启动后第一次,热更新总是失败的,可是你第二次热更新,他就 OK 了,具体表现如下:
- 这是网络不好/链接错误的表现,通常来说重新跑一下服务,或者刷新几次就好了
- 第二种, App 显示已经 update ,可是页面没更新,第二次在刷新,他又 OK 了?
来看我的一段不权威分析(乱说一通)
第一次不成功的时候,有加载一个 less/css 的更新,第二次就没了(后面几次也没有了)。
那现在要么就打个断点,从 console 的面板,进到对应的更新源码看看那就直接试下把 less 的热更新加上!
# 修改 less 配置,支持热更新
不用 webpack-dev-server 的话,每种类型的热更新,尤其是涉及需要编译的内容,都得我们自己写配置支持
less 的配置是在 webpack.base.js
- webpack.base.js
判断只有在开发模式下才打开 hrm
和 reloadAll
const devMode = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
// ...
module: {
rules: [
{
test: /\.(less|css)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
+ // only enable hot in development
+ hmr: devMode, // 添加hrm支持
+ // if hmr does not work, this is a forceful method.
+ reloadAll: devMode // 添加热更新
}
},
{ loader: 'css-loader', options: { esModule: false } },
'postcss-loader',
'less-loader'
]
}
]
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
是不是加了就生效了呢? No,还漏了一步 process.env.NODE_ENV
是需要指定的,在我们启动环境的时候,就需要指定当前 node 的运行环境,所以呢,得改一下 package.json
。用 cross-env
来跨平台的设置环境
npm i cross-env -D
- package.json
把 serve 命令改掉,添加运行的环境,然后在执行 node 去运行我们的文件
"serve": "cross-env NODE_ENV=development node build/dev-server.js",
还有一个要注意的地方:
如果项目的 css 用了 hash
或者 其他哈希的话,在 dev 热更新模式下,就记得不要用了!!不然 css 也会更新失败
所以开发环境配置,在改一下:添加了 css 插件,不过只输出名称,不输出哈希值了
- webpack.dev.js
const { merge } = require('webpack-merge')
const { DIST, BASE_CONFIG } = require('./PATH')
const baseConfig = require(BASE_CONFIG)
const webpack = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
mode: 'development',
output: {
filename: 'js/[name].[hash].js',
publicPath: '/',
path: DIST
},
devServer: {
contentBase: DIST //指定需要提供给本地服务的内容的路径,默认加载index.html文件,可根据需要修改
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin({
filename: 'css/[name].css'
})
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
重新运行项目,效果:
忘记录制 css 修改的效果了,可以自己试下
细心地可能发现了,我又在输入框输入内容了,这次的热更新,会比之前的 WDM
好一丢丢,起码我们修改标签内的文案/修改颜色,只是局部刷新
我也试了如果不是修改生命周期的 JS,通常也会局部刷新上去,但是如果修改了生命周期/改了布局,那可能 webpack 处理不了的,就直接页面刷新了,总的来说体验还是好了一点的
# 我是一段小尾巴
在排查问题的时候,发现有几个地方写的不咋的,比如打包后的文件,html 和 js 文件都在同级了!我们应该把 js 统一下,把 css 也统一下,img 也统一下
所以分别在 webpack.dev.js
webpack.base.js
webpack.prod.js
改一下 output 的 filename
。前面多加一个 js/
这会不会影响 publicPath 呢?不会的~亲测有效
第二个就是统一下 PATH,我们在前面把 dev 和 prod,base 的配置都写到了 PATH 里面了,所以在后面几个文件我们都统一用 PATH 来作为路径变量(具体的可以自己找一找改一改,或者到时候看 v0.0.3 的代码)
# 实现一个简单的多入口
# 啰嗦一下
看到了这一步,代表基本的打包(vue+less)是可以了,然后在稍微的锦上添花(HRM)也安排了一下,到目前为止我们都是只在修改App.vue
一个文件,可是到了实际的项目中,几百个路由是不在话下的(曾经做过一个项目就是 450+个路由,运行一次基本就热更新都得等 30s)
写这个 webpack 很大一部分原因是想把页面使用单页面的模式,可是每个页面都有独立的 HTML,这样有利于以后哪怕要优化 SEO,我们也可以把 SEO 部分内容放 html,实际上我们只控制 #app 节点来显示页面内容,让爬虫抓页面隐藏部分的内容来实现 SEO
还有一个好处就是,做成多入口后,之前的项目就可以进行升级,450+路由页面,划分几十个模块,每个模块几个-10 几个页面(这种热更新还是可以支持的),以后项目就按模块来更新,不用改了一行代码都要把整套项目重新打包~
# 话不多说,现在开始
看下当前的目录结构
.
|-- README.en.md
|-- README.md
|-- build
| |-- PATH.js
| |-- dev-server.js
| |-- webpack.base.js
| |-- webpack.dev.js
| `-- webpack.prod.js
|-- package-lock.json
|-- package.json
|-- postcss.config.js
|-- public
| `-- index.html
|-- src
| |-- App.vue
| |-- assets
| | |-- css
| | | `-- common.less
| | `-- image
| | `-- avatar.jpg
| `-- main.js
`-- testconfig.js
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在 src 文件夹下, App 和 main.js 同级,并且都在 src 下,那原先已经有的就不动他了
我们新建 2 个模块(商品模块(goods)和个人中心(user)模块),并且在 user 模块下,在模拟一下复杂的 URL 嵌套的情况!
内容特别简单, index.js 引入我们的对应的 vue 文件(其实就是 main.js 的作用),然后 vue 文件简单写点内容(我就直接把 App.vue 的内容扣过来了,顺便一起测试引入 less,引入图片的问题),其余几个文件同理
搬 App.vue 文件的时候记得项目引入路径的问题,之前都是 ./
,迁移目录后记得改成对应的 ../
甚至是 ../../
# 修改 webpack.base.js
敲黑板,这里很多知识点和细节!!
文件结构已经有了,那么下一步就是添加 webpack 的多入口。我是特意模拟了多入口,多入口文件嵌套的情况!
先说第一批知识点:
- 多入口的 webpack,entry 对象中,键值(也就是我们的模块名),在配置中可以用
[name]
来写 - 如果引入的对应资源/文件名 有
/
,假设起的入口名字是user/index
那么到时候输出的文件,就会是 user 文件夹下的 index.js 文件 - 还记得教程第二章中说的 webpack/vue-cli 中的 publicPath 区别 (opens new window) 和 配置-css-样式分离 (opens new window) 要知道,我们图片和 css 的配置,只能相差
一个文件夹
。也就是只能用../
。配合知识点 2,如果我们滥用user/index
这种名称为文件起名字,我们的图片很容易就找不到
综合考虑,于是就有了下面的配置:
- webpack.base.js
上个截图比较直观:
知识点小总结:
普通模块,我们还是用了文件本身的名称,比如
user,goods
之类的。像嵌套的文件夹,user/setting
的文件夹,我们在入口是用了user_setting
。但是输出的 html 中fileName
还是用回了/
。在 chunks 继续保留了user_setting
。因为 chunks 是需要和入口文件相对应的
而且留意看 user/setting 的配置中,还需要额外的 publicPath 为
../
也是因为文件夹嵌套引发的问题
如果太难理解,就先继续看下去把,等下项目运行起来就知道到底为什么要这么做了!
知识点 2
为什么我这里写
./src/xxxx
而不用 PATH?而且为什么是./src
不是../src
??
因为这只是临时的演示,等下就搞动态入口配置了,就不想去写 PATH 了
为什么是
./src
。这是一个很重要的知识点,webpack 运行后,他的目录并不是根据当前的文件目录,而且又从根目录开始找,所以../src
会让 webpack 找不到文件在哪里(是不是很拗口,所以我干脆直接写 PATH,里面统一我们的项目路径,一了百了~)
- webpack.base.js
module.exports = {
entry: {
gooods: devMode ? ['webpack-hot-middleware/client', './src/goods/index.js'] : ['./src/goods/index.js'],
user: devMode ? ['webpack-hot-middleware/client', './src/user/index.js'] : ['./src/user/index.js'],
user_setting: devMode
? ['webpack-hot-middleware/client', './src/user/setting/index.js']
: ['./src/user/setting/index.js']
},
plugins: [
new HtmlWebpackPlugins({
filename: 'goods.html',
template: TEMPLATE_HTML,
chunks: ['goods']
}),
new HtmlWebpackPlugins({
filename: 'user.html',
template: TEMPLATE_HTML,
chunks: ['user']
}),
new HtmlWebpackPlugins({
filename: 'user/setting.html',
template: TEMPLATE_HTML,
publicPath: '../',
chunks: ['user_setting']
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
运行后, localhost:8080
应该是访问不了了,因为 webpack 做了多入口后,又没有 index 入口,没有了默认入口就会报 404 。可以自己建一个 index 入口,那么接下来直接访问 localhost:8080/user.html
和 localhost:8080/goods.html
或者 localhost:8080/user/setting.html
看 setting 的路由,是 user/setting.html 这就是为什么在 html 的 fileName 我们需要写 /
,就是为了符合我们的 URL 地址和文件路径的映射!
本地运行没有问题,打一个包试下
回顾下刚才说的东西
- user/setting.html 的 publicPath 为什么要 ../? 根据打包后的目录来看,user/setting.html 想要引入 JS 资源,是不是得
../js/
。而普通的一层的页面,只需要./js/
就能找到 JS 了
- 为什么入口文件,要把
/
改成_
,即原本是user/setting
的,要改成user_setting
因为 css 的文件打包后[name]
配置读取的就是入口文件配置的名字,如果直接使用user/setting
。那么 css 生成的文件也会嵌套多层,到时候如果在 css 里面还引入了图片,那就真的找不到图片了,具体可以自己改一下入口文件试一下~
到这里,我们已经实现了一个多页面的 vue,而且摆脱了 #
哈希路由!!成功实现了一个多入口的单页面应用
# 最后
webpack 踩坑系列(三)也结束了~。摸索了一下热更新的区别,自己实现了一个 HRM。搭建了单页面多入口的项目雏形
下期预告:
- 公共模块一起打包
- 实现自动获取入口(重头戏)
- 添加代码运行后的 URL 输出
- 自动获取端口号