使用 webpack 搭建 vue 开发环境(四)

2/7/2021 webpack

# 使用 webpack 搭建 vue 开发环境(四)

对应分支 Jioho/webpack_config@v0.0.4 (opens new window)

# 公共模块一起打包

在开始之前,直接运行一下打包,看下优化前的效果:

可以看到打包了 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次才能打包到此模块
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

重新打包后会发现多了一个文件,而且其他模块普遍小了很多,因为 vue.js 都被抽离出去了。不仅仅是 vue.js ,就算是我们自己写的方法他也会进行抽离,以后公共模块就会都到了 commons 和 vendor 那边去了

# 实现自动获取入口(重头戏)

自动获取入口生成配置文件是这个项目的核心之一

这块的代码稍微先说一下想法和需要实现的思路:

  1. 通过 nodejs 遍历我们指定的 src/pages 。找出所有的 文件/文件夹
  2. 匹配我们对应的入口文件。如果是根目录的 .vue 文件,自然认为是一个入口。如果是某个文件夹下的 .vue 文件,我们根据文件夹名称查找对应的 index/对应文件夹名称的模块(有点绕,下面会详细解析)
  3. 找到文件后,自然是生成入口,匹配对应的 html 模版
  4. 在生成配置之前,入口文件/html 的 filename 保持 目录/目录 的写法,这样到时候生成的文件也能保持我们的目录结构
  5. 最后生成我们需要的入口

在开发之前,重新整理了一下 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
先贴一下获取入口的代码 (`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
}
1
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

动态生成入口文件的逻辑中思考了很久,因为涉及的内容非常的多,如果仅靠 .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
// }
1
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

# 目录文件夹深度

在上一步中,我们已经拿到了入口配置,拿到了要打包的模块的最大入口深度(_config.deep)。剩下的就是 css 引入的文件

还记得在配置(二)中的一个探索吗? 配置-css-样式分离 (opens new window)

这时候我们的目录层级已经不仅仅只一级(因为每个页面都会生成对应的 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"
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

那问题来了,如果我一个根目录的页面,原先在 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
})
1
2
3
4
5
6
7
8
9

既然我们有了美化输出的插件,那 webpack 原先的输出就可以"闭嘴了"。找到 webpackDevMiddleware() 配置中的 stats 配置改为 none;quiet 改为 true(具体有哪些值可以看下 webpack 的文档)
devserver-quiet (opens new window)
devserver-stats (opens new window)

这还不是最终的效果,毕竟

# 自动获取端口号

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 }
1
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

自动获取端口号其实也没啥特别要说的东西,主要是获取本地 IP,然后检查端口号是否占用

# 最后

webpack 的打包系列 4 就到这里了。其实写到这里发现还有很多体验的问题没有解决,看来想开发一个多入口工具也并非那么简单。。。不过当作是 webpack 入门学习,感觉还是不错滴

Last Updated: 5/9/2021, 10:45:03 PM