webpack进阶篇(四)

956 阅读14分钟

六、Webpack 底层原理及脚手架工具分析

1.如何编写一个Loader

css-loaderstyle-loaderfile-loader等 当我们打包一个类型文件或模块的时候,loader就会产生作用。

写完loaders之后,只要在module中配置好相应的规则,就可以使用了。

// index.js

console.log('hello fruit')
// wepack.config.js

module.exports = {
  mode: "development",
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [{
      test: /\.js/,
      use: [path.resolve(__dirname, './loaders/replaceLoader.js')] // 配置自己写的loader
    }]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}
// replaceLoader.js
module.exports = function (source) { // 不要用箭头函数,this指向会有问题
  return source.replace('fruit', 'fruitBro')
}
// package.json

{
  "name": "make-loader",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11"
  }
}
npm run build

通过replaceLoader成功将fruit替换为fruitBro

继续优化:

// wepack.config.js

module.exports = {
  module: {
    rules: [{
      test: /\.js/,
      use: [
        {
          loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
          options: {
              name: 'Fruit'
          }
        }
      ],
    }]
  },
}

通过query参数可获取name参数,可通过以下方式打印:

// replaceLoader.js
module.exports = function (source) {
  console.log("======",this.query) // { name: 'Fruit' }
  return source.replace('fruit', 'fruitBro')
}

因此我们可以用上述方式进行替换

// replaceLoader.js
module.exports = function (source) {
  return source.replace('fruit', this.query.name)
}

编译生成的代码如下:

参考文档: webpack.docschina.org/api/loaders…

但是this.query.name的写法并不是很方便。因此官方提供了loader-utils模块,帮助我们分析内容。

参考文档: webpack.docschina.org/api/loaders…

npm install loader-utils --save-dev

写法改为如下:

const loaderUtils = require('loader-utils')

module.exports = function (source) {
  const options = loaderUtils.getOptions(this) // 会将参数的所有内容都放到options中
  return source.replace('fruit', options.name)
}

参考文档:webpack.docschina.org/api/loaders…

此时的loader只能return一个结果,额外的东西无法带出去,比如sourceMap,此时可以通过this.callback带出去

this.callback共四个参数

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);
module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  const result = source.replace('fruit', options.name) // 通过loader解析后的新的源代码
  // this.callback(null, result, sourceMap) // 共四个参数
  this.callback(null, result) 这样写等价于return
}

在loader中做异步操作

参考文档:webpack.docschina.org/api/loaders…

this.async会告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。和上述的this.callback一样,需要传入4个参数。

module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  const callback = this.async()
  setTimeout(() => {
      const result = source.replace('fruit', options.name)
      callback(null, result)
  }, 5000) // loader需要5秒才能执行完
}

以上就是一个异步loader的编写方法。

多个loader同时使用的写法,replaceLoaderAsync.jsreplaceLoader.js。loader的使用顺序,从下到上,从右到左。

// replaceLoaderAsync.js

const loaderUtils = require('loader-utils')

module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  const callback = this.async()
  setTimeout(() => {
      const result = source.replace('fruit', options.name) // 先把fruit替换为Fruit,再把Fruit替换为Fruit Bro
      callback(null, result)
  }, 1000) // loader需要5秒才能执行完
}
// replaceLoader.js

module.exports = function (source) {
  return source.replace('Fruit', 'Fruit Bro')
}
// wepack.config.js

module.exports = {
  module: {
    rules: [{
      test: /\.js/,
      use: [
        {
          loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
        },
        {
          loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
          options: {
              name: 'Fruit'
          }
        },
      ],
    }]
  },
}

可以把path.resolve(__dirname省略掉,借助resolveLoader,写法如下:

// wepack.config.js

module.exports = {
  resolveLoader: {
      modules: ['node_modules', './loaders']
  },
  module: {
    rules: [{
      test: /\.js/,
      use: [
        {
          loader: 'replaceLoader',
        },
        {
          loader: 'replaceLoaderAsync',
          options: {
              name: 'Fruit'
          }
        },
      ],
    }]
  },
}

以上就是自定义loader的常见语法. 重点:

  1. loader这个函数一定要是声明式的定义function,而不能是箭头函数,loader中要用this方法,this的指向在箭头函数下可能会出错。
  2. loader分同步和异步。异步要先调用一下this.async()

常用实例:

  1. 前端代码的异常捕获。try catch,代码中出现function的时候就替换成try{function {} } catch(e),把所有的代码都放到try catch中执行,这样我们只需要写一个loader就能解决错误监控的问题了,就能异常捕获了。
  2. 国际化网站。在loader中进行中英文替换,通过占位符的方法来解决,写法如下:
if (Node全局变量 === '中文') {
    source.replace('{{title}}', '中文标题')
} else {
    source.replace('{{title}}', 'english title')
}

不过我们现在都用react-intl-universal来实现国际化。

2.如何编写一个Plugin

loaderplugin之间有什么区别呢?loader在文件执行的过程中修改文件,帮助我们处理模块;plugin在打包结束之后或某些具体时刻上,如在打包之前用CleanWebpackPlugin,在打包之后用htmlWebpackPlugin。在打包过程中的某些时刻,想做一些时间,可以用插件来实现。随着plugin越来越多,webpack能做的事情也越来越多。pluginwebpack的灵魂。plugin的核心设计模式:事件驱动、发布订阅的设计模式。代码之前的执行是通过事件来驱动的。

plugin(插件)的定义形式和loader的定义形式不同,loader是一个函数,plugin是一个class类。

为什么使用plugin的时候,要使用实例的方法new,因为plugin本质上是class,要创建一个实例才能使用它。

定义插件:

// copyright-webpack-plugin.js

class CopyRightWebpackPlugin {
  constructor () {
    console.log('plugin used')
  }
  apply(compiler) {
  }
}

module.exports = CopyRightWebpackPlugin

使用插件:

// webpack.config.js

const path = require('path')
const CopyrightwebpackPlugin = require('./plugins/copyright-webpack-plugin')

module.exports =  {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: [
    new CopyrightwebpackPlugin()
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  }
}

插件接收参数的方式:

// 传递参数
new CopyrightwebpackPlugin({
  name: 'Fruit'
})

// 接收参数 options,使用constructor来接收参数
constructor (options) {
  console.log('plugin', options)
}

compiler-hooks 参考文档: webpack.docschina.org/api/compile…

钩子:就是在某一时刻会自动执行的一个函数

webpack.docschina.org/api/compile…

本次目的:在dist文件夹中新增一个copyright.txt

compilation.assets的打印结果如下图:

// copyright-webpack-plugin.js

class CopyRightWebpackPlugin {
  apply(compiler) {
    // compiler的实例,存储了webpack相关的各种配置文件、打包过程等一系列内容
    compiler.hooks.emit.tapAsync('CopyRightWebpackPlugin', (compilation, cb) => {
      // compilation: 存放只是跟本次打包相关的内容
      // compilation.assets : 打包生成的内容是放在assets中的
      console.log('Fruit Bro', compilation.assets)
      // 有mian.js的key值,我们在新增一个'copyright.txt'的key值
      compilation.assets['copyright.txt'] = {
        source: function () {
          return 'copyright by FruitBro'
        },
        size: function () {
          return 21
        }
      }
      cb()
    })

  }
}

module.exports = CopyRightWebpackPlugin

上述插件的作用:在即将把代码放到dist目录之前,往代码中增加了一个copyright.txt文件。下图打印结果,使用了emit时刻。

compilation还有很多时刻

// package.json 调试命令

{
  "scripts": {
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
    "build": "webpack"
  },
}

  1. 在要调试的地方加入debugger
class CopyRightWebpackPlugin {
  apply(compiler) {
    compiler.hooks.compile.tap('CopyRightWebpackPlugin', (compilation) => {
      console.log('compiler')
    })
    compiler.hooks.emit.tapAsync('CopyRightWebpackPlugin', (compilation, cb) => {
      debugger // 调试
      compilation.assets['copyright.txt'] = {
        source: function () {
          return 'copyright by FruitBro'
        },
        size: function () {
          return 21
        }
      }
      cb()
    })

  }
}

module.exports = CopyRightWebpackPlugin

2.运行npm run debug

npm run debug
  1. 打开devtool,点击左上角的node按钮

  2. watch处添加compilation

  3. 这样即可看到compilation中包含的所有方法

    这样我们在写插件的时候,就可以通过这个方法来调用其中的方法了,帮助我们看到在打包的插件里用到的变量是什么形式。我们编写webpack插件的时候,就是基于调式工具来编写的。

总结:

  1. 学会写一个类class
  2. 写apply方法
  3. 学会如何使用node调试工具进行插件调试

3. Bundler源码编写(模块分析)

手写简单类似webpack的Bundler

打印index.js的文件内容如下:

npm install cli-highlight -g

安装完成后,打印的内容可以部分高亮

node bundler.js | highlight
npm install @babel/parser  // 帮助分析源代码

babeljs.io/docs/en/bab…

如上图所示,打印出来的就是抽象语法书AST,可以很好的表述当前的代码。 program:代表当前运行的程序,其中body字段

const fs = require('fs')
const parser = require('@babel/parser')

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  console.log(ast.program.body)
}

moduleAnalyser('./src/index.js')

打印结果如下:

一共有两个节点:代表program对应的两个节点,

ImportDeclaration: 代表引入语法,对应 import message from './message.js'

ExpressionStatement: 代表表达式,对应 console.log(message)

通过抽象语法书AST,就可以找到声明的语句,找到对应的依赖关系,将我们的js代码转化为js对象。这样就拿到了代码里所有的依赖关系。

babel提供了工具帮助我们快速找到import节点。@babel/traverse

npm install @babel/traverse --save
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default //默认导出的es module

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  traverse(ast, {
    ImportDeclaration({ node }) {
      console.log(node)
    }
  })
}

moduleAnalyser('./src/index.js')

通过traverse分析出代码里的依赖。

traverse(ast, {
  ImportDeclaration({ node }) { // 只要语法书ast中包含ImportDeclaration这样的引入语句时,就会走这个函数
    console.log(node.source.value)
  }
})

对入口文件的依赖分析

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencies = [] // 依赖,对入口文件的依赖分析
  traverse(ast, {
    ImportDeclaration({ node }) { // 只要语法书ast中包含ImportDeclaration这样的引入语句时,就会走这个函数
      dependencies.push(node.source.value)
    }
  })
  console.log(dependencies)
}

运行结果如下:

依赖是一个相对路径,相对于src/index.js入口文件。但是我们要是用的相对于根目录的路径。

以对象的方式存储依赖.

对引入的模块的原始代码打包编译成在浏览器上能够运行的代码,因此我们还要借助babel进行代码转化。

npm install @babel/core --save

参考文档:

babeljs.io/docs/en/bab…

babeljs.io/docs/en/bab…

npm install --save-dev @babel/preset-env
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default //默认导出的es module,用export default的内容,需要用.default方法。
const babel = require('@babel/core')

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencies = {} // 依赖,对入口文件的依赖分析
  traverse(ast, {
    ImportDeclaration({ node }) { // 只要语法书ast中包含ImportDeclaration这样的引入语句时,就会走这个函数
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
      // console.log(newFile)
    }
  })
  const { code } =  babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  }) // code就是编译生成的可以在浏览器上运行的模块的代码
  console.log(code)
  return {
    filename,
    dependencies,
  }
  // console.log(dependencies)
}

moduleAnalyser('./src/index.js')

运行,打印结果如下:

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default //默认导出的es module,用export default的内容,需要用.default方法。
const babel = require('@babel/core')

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencies = {} // 依赖,对入口文件的依赖分析
  traverse(ast, {
    ImportDeclaration({ node }) { // 只要语法书ast中包含ImportDeclaration这样的引入语句时,就会走这个函数
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
      // console.log(newFile)
    }
  })
  const { code } =  babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  }) // code就是编译生成的可以在浏览器上运行的模块的代码
  // console.log(code)
  return {
    filename,
    dependencies,
    code,
  }
  // console.log(dependencies)
}

const moduleInfo = moduleAnalyser('./src/index.js')
console.log(moduleInfo)

分析: 入口文件filename对应的文件路径为./src/index.js. 依赖dependencies./message.js真正的路径为./src/message.js 对应的代码被翻译过后,要在浏览器中运行的部分为code.

抽象:当我们去做项目打包的时候,首先要对项目中的模块做分析。目前我们只对入口文件做了分析。

总结:

  1. 为了分析入口文件我们写了moduleAnalyser函数,
  2. 通过传入的文件名,用fs.readFileSync读出了文件中的内容
  3. 再通过parser.parse将读出的代码转化为js对象,即AST抽象语法树。
  4. 通过ImportDeclaration分析声明都在哪些地方,找到import语句对应的内容,分析依赖,并拼装为js对象dependencies,以键值对的方式存入。
  5. 借助babel.transformFromAst对源代码进行编译,转化为浏览器可以执行的代码,存入{ code }
  6. 最终返回入口文件名字filename、依赖关系dependencies、编译后的代码code

4.bundle源码编写(Dependencies Graph)

想要实现把所有模块都分析出来,我们还需要写一个函数。生成依赖图谱。通过递归实现。如下:

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default //默认导出的es module,用export default的内容,需要用.default方法。
const babel = require('@babel/core')

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencies = {} // 依赖,对入口文件的依赖分析
  traverse(ast, {
    ImportDeclaration({ node }) { // 只要语法书ast中包含ImportDeclaration这样的引入语句时,就会走这个函数
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
      // console.log(newFile)
    }
  })
  const { code } =  babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  }) // code就是编译生成的可以在浏览器上运行的模块的代码
  // console.log(code)
  return {
    filename,
    dependencies,
    code,
  }
  // console.log(dependencies)
}

const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyser(entry)
  const graphArr = [ entryModule ]
  for (let i = 0; i < graphArr.length; i++) {
    const item = graphArr[i]
    const { dependencies } = item
    if (dependencies) {
      for (const j in dependencies) {
        graphArr.push(
          moduleAnalyser(dependencies[j])
        ) // 对每一个依赖的内容进行分析
      }
    }
  }
  console.log(graphArr)
  // console.log('entryModule', entryModule)
  // 对数组做格式上的转化
  const graph = {}
  graphArr.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code,
    }
  })
  // 转化为对象
  return graph
}
const graphInfo = makeDependenciesGraph('./src/index.js')
console.log(graphInfo)

5.bundle源码编写(生成代码)

从上面的代码我们可以拿到所有模块代码分析生成的结果(Dependencies Graph),接下来就让我们用(Dependencies Graph)生成真正可以在浏览器上运行的代码。

需要用到闭包。

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default //默认导出的es module,用export default的内容,需要用.default方法。
const babel = require('@babel/core')

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencies = {} // 依赖,对入口文件的依赖分析
  traverse(ast, {
    ImportDeclaration({ node }) { // 只要语法书ast中包含ImportDeclaration这样的引入语句时,就会走这个函数
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
      // console.log(newFile)
    }
  })
  const { code } =  babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  }) // code就是编译生成的可以在浏览器上运行的模块的代码
  // console.log(code)
  return {
    filename,
    dependencies,
    code,
  }
  // console.log(dependencies)
}

const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyser(entry)
  const graphArr = [ entryModule ]
  for (let i = 0; i < graphArr.length; i++) {
    const item = graphArr[i]
    const { dependencies } = item
    if (dependencies) {
      for (const j in dependencies) {
        graphArr.push(
          moduleAnalyser(dependencies[j])
        ) // 对每一个依赖的内容进行分析
      }
    }
  }
  // console.log(graphArr)
  // console.log('entryModule', entryModule)
  // 对数组做格式上的转化
  const graph = {}
  graphArr.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code,
    }
  })

  return graph
}
const generateCode = (entry) => {
  const graph = JSON.stringify(makeDependenciesGraph(entry))
  // 生成的代码为闭包,避免污染全局环境
  return `
    (function(graph) {
      function require(module) {
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath]) // 返回真实路径
        }
        var exports = {}; // 需要加;,否则编译后的代码执行会报错
        (function(require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports;
      };
      require('${entry}')
    })(${graph});
  `
}
const code = generateCode('./src/index.js')
console.log(code)

编译后的代码如下:

(function(graph) {
      function require(module) {
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath]) // 返回真实路径
        }
        var exports = {};
        (function(require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports;
      };
      require('./src/index.js')
    })({"./src/index.js":{"dependencies":{"./message.js":"./src/message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src/message.js":{"dependencies":{"./word.js":"./src/word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}});

可以反复尝试写一下。 在创建打包工具的过程中,我们会使用到 babel、node、闭包、递归等知识点。

七、Creact-React-App 和 Vue-Cli 3.0脚手架工具配置分析

1. 通过CreacteReactApp深入学习webpack配置

通过阅读别人的配置文件来提升自己的配置能力。

create-react-app脚手架工具。

npx create-react-app my-app

脚手架工具会尽可能的让我们的开发更简单。

// package.json

{
    "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject",
    }
}

npm run eject此命令可以把隐藏的webpack配置项展示出来。执行完之后会多出./config./scripts文件夹。

// package.json

{
    "scripts": {
        "start": "node scripts/start.js",
        "build": "node scripts/build.js",
        "test": "node scripts/test.js",
    }
}

遇到不会的就去搜,去解决。

// webpack.config.js

bail: isEnvProduction // 代表一旦打包的过程中出错就及时停止,不再继续打包。

output: {
    path: 打包路径,
    pathinfo:把引入的信息输出,
    filename: 输出文件名,
    chunkFileName: ,
    publicPath: ,
    devtoolModuleFilenameTemplate: sourcemap依赖文件在硬盘中的真正位置,
    optimization: 优化、代码压缩,
},
resolve: { // 当引入其他模块的时候,就会走resolve,用里面plugins对应的插件.
    modules: 根据你的配置生成数组,
    extensions: 找一个模块项的后缀对应的文件是否存在,
    alias: 别名,
    plugins: 插件,
    
},
resolveLoader: { 只有引入一些loader的时候,里面的plugin才会执行。resolve是全集,resolveLoader是只有在引入loader的时候才会执行。
  plugins: [
  
  ]
},
module: {
    strictExportPresence: true, // 引入的模块必须明确export导出自己的内容
    rules: [
        {
            parser: {requireEnsure: false} // 不允许用require语法,必须用import进行异步代码的加载
        }
    ]
    
}

WorkboxWebpackPlugin: pwa相关配置

./config/paths.js存放的都是打包文件要用到的一些路径信息。 ./config/env.js是帮助我们初始化项目运行环境。 webpackDevServer.config.js对应的配置内容。

主要文件作用:

  1. ./scripts/build.js./scripts/start.js为打包和开发对应的编译文件。
  2. webpack.config.js是整个webpack打包的核心.
  3. webpackDevServer.config.js是开发环境的配置项.

2. Vue CLI3的配置方法

参考文档:cli.vuejs.org/zh/

npm install -g @vue/cli

// 创建项目
vue create my-project
// 启动项目
cd my-project
npm run serve

理念:

vue: 让用户配置起来更简单

react:让使用配置起来更灵活

npm run build // 生成dist目录

想要改变生成目录dist等内容,不需要改变webpack.config.js,只需要创建vue.config.js,vue提供了一套配置文件

参考文档:https://cli.vuejs.org/zh/config/

cli.vuejs.org/zh/config/#… 修改输出目录 在底层再转化为webpack配置,配置大量精简。

// webpack.config.js

module.exports = {
  outputDir: 'fruit', // 将dist改为fruit目录
  pages: {
    index: {
      // page 的入口
      entry: 'src/index/main.js',
      // 模板来源
      template: 'public/index.html',
      // 在 dist/index.html 的输出
      filename: 'index.html',
      // 当使用 title 选项时,
      // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
      title: 'Index Page',
      // 在这个页面中包含的块,默认情况下会包含
      // 提取出来的通用 chunk 和 vendor chunk。
      chunks: ['chunk-vendors', 'chunk-common', 'index']
    },
    list: {
       entry: 'src/index/main.js',
       template: 'public/index.html',
       filename: 'index.html',
       title: 'Index Page',
       chunks: ['chunk-vendors', 'chunk-common', 'index']
    }
  },
  css: {
    modules: true // 以css的module形式引入css,防止组件间的影响。
  }
}
// webpack

module.exports = {
  output: {
      path: path.resolve(__dirname, 'fruit') // webpack中需要这样配置
  }
}

多页配置参考文档: cli.vuejs.org/zh/config/#…

参考文档:

  1. webpack.docschina.org/guides/ 找webpack解决方案
  2. webpack.docschina.org/concepts/ 核心概念
  3. webpack.docschina.org/configurati… 非常精确的配置项
  4. webpack.docschina.org/api/ Loader和Plugin的API
  5. webpack.docschina.org/loaders/ 官方推荐的Loader及其作用
  6. webpack.docschina.org/plugins/ 官方推荐的Plugin及其作用