使用 webpack 搭建 vue 开发环境(四)
公共模块一起打包
在开始之前,直接运行一下打包,看下优化前的效果:
可以看到打包了 3 个 html 页面,对应着 3 份 JS
看下 dist 目录下的 JS 的内容(goods 模块和 user_setting 模块):
代码看着是一样的,因为页面的 JS 都是一样的,差别不大,不过下面的 vue.js 又重复引入了一次,就是有多少个模块,vue 就得引入多少次
像这种公共资源,其实可以让他第一次加载后,配合缓存下来,下次加载就能快一点了
用 webpack 新 api splitChunks
来进行代码切割,整合
- webpack.prod.js 修改:
module.exports = merge(baseConfig, {
// ... 旧配置
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
priority: 1, // 优先级配置,优先匹配优先级更高的规则,不设置的规则优先级默认为0
test: /node_modules/, // 匹配对应文件
chunks: 'initial',
name: 'vendor',
minSize: 0, // 当模块大于minSize时,进行代码分割
minChunks: 1
},
commons: {
priority: 0,
chunks: 'initial',
name: 'commons', // 打包后的文件名
minSize: 0,
minChunks: 2 // 重复2次才能打包到此模块
}
}
}
}
}
重新打包后会发现多了一个文件,而且其他模块普遍小了很多,因为 vue.js
都被抽离出去了。不仅仅是 vue.js ,就算是我们自己写的方法他也会进行抽离,以后公共模块就会都到了 commons 和 vendor 那边去了
实现自动获取入口(重头戏)
自动获取入口生成配置文件是这个项目的核心之一
::: tip 这块的代码稍微先说一下想法和需要实现的思路:
- 通过 nodejs 遍历我们指定的
src/pages
。找出所有的 文件/文件夹。 - 匹配我们对应的入口文件。如果是根目录的
.vue
文件,自然认为是一个入口。如果是某个文件夹下的.vue
文件,我们根据文件夹名称查找对应的index/对应文件夹名称的模块
(有点绕,下面会详细解析) - 找到文件后,自然是生成入口,匹配对应的 html 模版
- 在生成配置之前,入口文件/html 的 filename 保持
目录/目录
的写法,这样到时候生成的文件也能保持我们的目录结构 - 最后生成我们需要的入口
:::
在开发之前,重新整理了一下 build/PATH.js
文件,主要是新增了一些入口和删除了一些不用的入口
// build/PATH.js
const path = require('path')
const ROOT = path.resolve(__dirname, '../')
const SRC = path.resolve(ROOT, 'src')
const BUILD_CONFIG = path.resolve(ROOT, 'build')
const PATH = {
ROOT: ROOT,
SRC: SRC,
BUILD_CONFIG: BUILD_CONFIG,
PAGES: path.resolve(SRC, 'pages'),
DIST: path.resolve(ROOT, 'dist'),
BASE_CONFIG: path.resolve(BUILD_CONFIG, 'webpack.base.js'),
DEV_CONFIG: path.resolve(BUILD_CONFIG, 'webpack.dev.js'),
PROD_CONFIG: path.resolve(BUILD_CONFIG, 'webpack.prod.js'),
BUILD_TEMPLATE: path.resolve(BUILD_CONFIG, 'templates'),
SERVER: path.resolve(BUILD_CONFIG, 'server.js')
}
module.exports = PATH
::: details 先贴一下获取入口的代码 (build/getEntry.js
)
const path = require('path')
const fs = require('fs-extra')
const {
BUILD_TEMPLATE, PAGES } = require('./PATH')
const HtmlWebpackPlugins = require('html-webpack-plugin')
const devMode = process.env.NODE_ENV === 'development' // 是否是开发模式
/**
* 生成需要打包入口的一维数组
* 先通过nodejs的能力递归生成对应的文件和文件夹树状
* 在通过 flatEntrTree 方法获取对应的一维数组
* @param {*} entry 开始的入口路径
* @param {*} parent 递归遍历时的父级目录名称
* @param {*} fileDir 父级累计下来的路径
*/
function getEntryList(entry, parent = '', fileDir = '') {
if (!entry) {
console.error('开发目录有误')
process.exit()
return {
}
}
const FILE_PATH = path.resolve(entry, fileDir)
let entryList = []
let fileList = fs.readdirSync(FILE_PATH)
let fileMap = {
}
fileList.forEach(filtItem => {
const fullPath = path.join(FILE_PATH, filtItem)
const stat = fs.statSync(fullPath)
const ext = path.extname(fullPath).replace('.', '')
const name = path.basename(fullPath, `.${
ext}`)
const _isDir = stat.isDirectory()
// 目录级
if (_isDir) {
!fileMap['dir'] && (fileMap['dir'] = [])
fileMap['dir'].push({
name: name,
path: fileDir + name + '/'
})
} else {
!fileMap[ext] && (fileMap[ext] = {
})
fileMap[ext][name] = {
name: name,
path: fileDir + name
}
}
})
entryList = flatEntrTree(fileMap, parent)
fileMap.dir &&
fileMap.dir.forEach(dirItem => {
entryList = entryList.concat(getEntryList(entry, dirItem.name, dirItem.path))
})
return entryList
}
/**
* 把目录树拍平为一维数组
* 生成的一维数组需要3个信息:1. 入口的模块名称 2. 对应的入口JS 3. 对应入口的html模版
* 根目录匹配对应的 `.vue` 文件名称,即为他自己的模块,否则根据父级目录的名称作为模块名称
* js 和 html 查找规则为优先查找 index 文件名(index.js index.html index.vue)
* 如果index模块不存在,则找到文件夹名称对应的 js vue html 文件作为入口文件
* @param {*} fileMap 当前目录的树形结构
* @param {*} parent 当前目录的父级名称
*/
function flatEntrTree(fileMap, parent = '') {
// 没有vue文件/和父级不匹配的
if (!fileMap.vue || (parent !== '' && !fileMap.vue[parent] && !fileMap.vue.index)) return []
let tree = []
// 根目录,根目录找JS和html只能和自相匹配的
if (parent === '') {
Object.keys(fileMap.vue).forEach(key => {
let item = fileMap.vue[key]
item.jsPath = fileMap.js && fileMap.js[key] ? `./src/pages/${
fileMap.js[key].path}` : null
item.htmlPath = fileMap.html && fileMap.html[key] ? `./src/pages/${
fileMap.html[key].path}` : null
tree.push(item)
})
} else {
// 非根目录的,匹配当前名称和index都可以。并且有且只有一个
let item = fileMap.vue.index || fileMap.vue[parent]
item.jsPath = null
item.htmlPath = null
if (fileMap.js) {
let jsInfo = fileMap.js.index || fileMap.js[item.name] || null
item.jsPath = jsInfo ? `./src/pages/${
jsInfo.path}` : null
}
if (fileMap.html) {
let htmlInfo = fileMap.html.index || fileMap.html[item.name] || null
item.htmlPath = htmlInfo ? `./src/pages/${
htmlInfo.path}` : null
}
tree.push(item)
}
return tree
}
/**
* 生成入口配置文件
* 根据 `getEntryList` 生成的一维数组 / 自己传入对应的一维数组生成入口配置
* 返回值数据 `deep` 为最大的目录深度,因为打包出去的 html 文件需要引入对应的JS,意味着我们需要往上 `../` 多少次
* css 同理
*
* 查找对应的 jsPath 作为入口文件,如果对应的JS不存在,则自动生成
* 自动生成的JS来自模版文件,然后复制到对应的目录下(因为对应目录下没有匹配的JS文件,所以这一步不会覆盖原先的文件)
* html 模版文件同理,不过html不用复制,因为html模版如果无须自定义的话,统一一个html入口即可
* 最后的 `publicPath` 就是我们要用到的项目文件夹目录深度
* @param {*} entryList
*/
function initEntryConfig(entryList) {
let config = {
entry: {
},
plugins: [],
deep: 0
}
entryList.forEach(item => {
let itemDeep = item.path.split('/').length - 1
// 记录最大深度
if (config.deep < itemDeep) {
config.deep = itemDeep
}
if (!item.jsPath) {
// 就在当前目录生成对应JS
item.jsPath = `./src/pages/${
item.path}`
// 生成JS
let jsContent = fs.readFileSync(path.resolve(BUILD_TEMPLATE, 'template.js'), {
encoding: 'utf8' })
jsContent = jsContent.replace('{
{filePath}}', `./${
item.name}.vue`)
fs.outputFileSync(path.resolve(PAGES, `${
item.path}.js`), jsContent)
}
if (!item.htmlPath) {
// 用模版
item.htmlPath = BUILD_TEMPLATE + '/template'
}
config.entry[item.path] = devMode
? ['webpack-hot-middleware/client', `${
item.jsPath}.js`]
: [`${
item.jsPath}.js`]
config.plugins.push(
new HtmlWebpackPlugins({
filename: `${
item.path}.html`, // 输出的html文件名
template: `${
item.htmlPath}.html`,
chunks: [item.path, 'vendor'], // 指定在html自动引入的js打包文件
publicPath:
Array(itemDeep)
.fill('../')
.join('') || './'
})
)
})
return config
}
module.exports = {
getEntryList,
initEntryConfig
}
:::
动态生成入口文件的逻辑中思考了很久,因为涉及的内容非常的多,如果仅靠 .vue
文件生成入口,必须要有 js 文件承载(也就是我们常用的 main.js 文件) webpack 才识别。
如果我们把入口 JS 放到一个文件夹中不生成到代码文件夹下固然可以,可是那就失去了在入口文件引入 JS 的便利性,html 模版同理
所以思考了很久,在当前目录下如果没有 JS 文件,那就自动生成一个入口JS文件
,如果没有 html 模版,则加载打包工具中的模版,如果当前的页面需要额外引入一些 jq 或者百度地图的,也可以在当前根目录下重新写一个 html 模版,那入口配置文件也会自动识别到。
看下效果:
在 build/webpack.base.js
引入 build/getEntry.js
文件。然后运行获取配置的方法
- build/webpack.base.js
_config.entry 就是我们的入口配置了
_config.plugins 就是 html 的配置
_config.deep 就是当前生成入口的目录最大深度
const {
getEntryList, initEntryConfig } = require('./getEntry')
let _config = initEntryConfig(getEntryList(PAGES))
// _config 生成的效果:
// {
// "entry": {
// "App": ["webpack-hot-middleware/client", "./src/pages/App.js"],
// "goods/goods": ["webpack-hot-middleware/client", "./src/pages/goods/goods.js"],
// "goods2/index": ["webpack-hot-middleware/client", "./src/pages/goods2/index.js"],
// "newPage/index": ["webpack-hot-middleware/client", "./src/pages/newPage/index.js"],
// "user/index": ["webpack-hot-middleware/client", "./src/pages/user/index.js"],
// "user/teshuma/teshuma": ["webpack-hot-middleware/client", "./src/pages/user/teshuma/teshuma.js"]
// },
// "plugins": [
// {
// "options": {
// "template": "(xxxxx脱敏处理)templates/template.html",
// "templateContent": false,
// "filename": "App.html",
// "publicPath": "./",
// "hash": false,
// "inject": "body",
// "scriptLoading": "blocking",
// "compile": true,
// "favicon": false,
// "minify": "auto",
// "cache": true,
// "showErrors": true,
// "chunks": ["App", "vendor"],
// "excludeChunks": [],
// "chunksSortMode": "auto",
// "meta": {},
// "base": false,
// "title": "Webpack App",
// "xhtml": false
// },
// "version": 4
// },
// {
// "options": {
// "template": "(xxxxx脱敏处理)templates/template.html",
// "templateContent": false,
// "filename": "goods/goods.html",
// "publicPath": "../",
// "hash": false,
// "inject": "body",
// "scriptLoading": "blocking",
// "compile": true,
// "favicon": false,
// "minify": "auto",
// "cache": true,
// "showErrors": true,
// "chunks": ["goods/goods", "vendor"],
// "excludeChunks": [],
// "chunksSortMode": "auto",
// "meta": {},
// "base": false,
// "title": "Webpack App",
// "xhtml": false
// },
// "version": 4
// },
// {
// "options": {
// "template": "(xxxxx脱敏处理)templates/template.html",
// "templateContent": false,
// "filename": "goods2/index.html",
// "publicPath": "../",
// "hash": false,
// "inject": "body",
// "scriptLoading": "blocking",
// "compile": true,
// "favicon": false,
// "minify": "auto",
// "cache": true,
// "showErrors": true,
// "chunks": ["goods2/index", "vendor"],
// "excludeChunks": [],
// "chunksSortMode": "auto",
// "meta": {},
// "base": false,
// "title": "Webpack App",
// "xhtml": false
// },
// "version": 4
// },
// {
// "options": {
// "template": "./src/pages/newPage/index.html",
// "templateContent": false,
// "filename": "newPage/index.html",
// "publicPath": "../",
// "hash": false,
// "inject": "body",
// "scriptLoading": "blocking",
// "compile": true,
// "favicon": false,
// "minify": "auto",
// "cache": true,
// "showErrors": true,
// "chunks": ["newPage/index", "vendor"],
// "excludeChunks": [],
// "chunksSortMode": "auto",
// "meta": {},
// "base": false,
// "title": "Webpack App",
// "xhtml": false
// },
// "version": 4
// },
// {
// "options": {
// "template": "(xxxxx脱敏处理)templates/template.html",
// "templateContent": false,
// "filename": "user/index.html",
// "publicPath": "../",
// "hash": false,
// "inject": "body",
// "scriptLoading": "blocking",
// "compile": true,
// "favicon": false,
// "minify": "auto",
// "cache": true,
// "showErrors": true,
// "chunks": ["user/index", "vendor"],
// "excludeChunks": [],
// "chunksSortMode": "auto",
// "meta": {},
// "base": false,
// "title": "Webpack App",
// "xhtml": false
// },
// "version": 4
// },
// {
// "options": {
// "template": "./src/pages/user/teshuma/index.html",
// "templateContent": false,
// "filename": "user/teshuma/teshuma.html",
// "publicPath": "../../",
// "hash": false,
// "inject": "body",
// "scriptLoading": "blocking",
// "compile": true,
// "favicon": false,
// "minify": "auto",
// "cache": true,
// "showErrors": true,
// "chunks": ["user/teshuma/teshuma", "vendor"],
// "excludeChunks": [],
// "chunksSortMode": "auto",
// "meta": {},
// "base": false,
// "title": "Webpack App",
// "xhtml": false
// },
// "version": 4
// }
// ],
// "deep": 2
// }
目录文件夹深度
在上一步中,我们已经拿到了入口配置,拿到了要打包的模块的最大入口深度(_config.deep)。剩下的就是 css 引入的文件
还记得在配置(二)中的一个探索吗? 配置-css-样式分离
这时候我们的目录层级已经不仅仅只一级(因为每个页面都会生成对应的 html。有对应的层级结构),所以在 MiniCssExtractPlugin.loader
里面的配置中,publicPath
要往最大层级去写。配置改成如下:
{
"test": /\.(less|css)$/,
"use": [
{
"loader": MiniCssExtractPlugin.loader,
"options": {
"publicPath": Array(_config.deep ? _config.deep + 1 : 2)
.fill("../")
.join(""),
// only enable hot in development
"hmr": devMode,
// if hmr does not work, this is a forceful method.
"reloadAll": devMode
}
},
{
"loader": "css-loader", "options": {
"esModule": false } },
"postcss-loader",
"less-loader"
]
}
那问题来了,如果我一个根目录的页面,原先在 css 引入图片只需要
'../'
。如果按这样下来,岂不是改成了'../../'
,那资源不会找不到吗?
duck 可不必担心这个问题,因为按相对路径来说,无论../
层级有多少个,找到了资源的根目录就不会在往上找了。
动态入口的坑
这里不仅仅是编码的时候遇到的坑,还有很多未解决的问题,比如服务开启后,如何动态添加入口?动态修改入口文件?等。不过到目前为止,也算是一个可以凑合用的版本了~
添加代码运行后的 URL 输出
搞了那么久的动态入口,可是运行起来我压根都不知道有哪些模块可以运行。而且 webpack 的输出并不直观,显示的都是某个模块的大小,在了更新的时候这些其实都不是关心的重点。接下来就是优化一下运行时终端输出的问题~
找到之前开发的 build/dev-server.js
文件
引入一个插件 friendly-errors-webpack-plugin
简单的说一下插件 friendly-errors-webpack-plugin
的使用
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
// 然后在 webpack 的 plugins 加入:
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [`You application run at: http://localhost:8080/`, '这是第二句消息', '....']
},
clearConsole: true
})
既然我们有了美化输出的插件,那 webpack 原先的输出就可以"闭嘴了"。找到 webpackDevMiddleware()
配置中的 stats
配置改为 none;quiet
改为 true(具体有哪些值可以看下 webpack 的文档)
devserver-quiet
devserver-stats
这还不是最终的效果,毕竟
自动获取端口号
const express = require('express')
var net = require('net')
const interfaces = require('os').networkInterfaces() // 在开发环境中获取局域网中的本机iP地址
let app = null
let ip = null
// 检测端口是否被占用
function getPort(port = 8080, cb) {
// 创建服务并监听该端口
var server = net.createServer().listen(port)
server.on('listening', function() {
// 执行这块代码说明端口未被占用
server.close() // 关闭服务
cb && cb(port)
})
server.on('error', function(err) {
if (err.code === 'EADDRINUSE') {
getPort(port + 1, cb)
}
})
}
function start(_port, cb) {
!app && (app = express())
getPort(_port, port => {
app.listen(port, error => {
console.log('App run at ' + port)
cb(port)
})
})
return app
}
function getIp() {
if (ip) return ip
for (var devName in interfaces) {
var iface = interfaces[devName]
for (var i = 0; i < iface.length; i++) {
var alias = iface[i]
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
ip = alias.address
}
}
}
return ip
}
module.exports = {
start, getIp }
自动获取端口号其实也没啥特别要说的东西,主要是获取本地 IP,然后检查端口号是否占用
最后
webpack 的打包系列 4 就到这里了。其实写到这里发现还有很多体验的问题没有解决,看来想开发一个多入口工具也并非那么简单。。。不过当作是 webpack 入门学习,感觉还是不错滴