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

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

对应分支 Jioho/[email protected]

公共模块一起打包

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

可以看到打包了 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 那边去了

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

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

扫描二维码关注公众号,回复: 12970031 查看本文章

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

  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

::: 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 入门学习,感觉还是不错滴

猜你喜欢

转载自blog.csdn.net/Jioho_chen/article/details/113738445