如题,请问vue serve/build和npm run serve/build(vue-cli-service serve/build)到底有啥区别嘞,感觉有点搞不清楚,有没有大佬能帮忙解惑下,万分感谢~
现有一个基于vue-cli3和webpack4的vue2项目,我使用vue-cli-service inspect命令导出webpack配置后,minimizer部分如代码所示: minimizer: [ { options: { test: /\.m?js(\?.*)?$/i, chunkFilter: () => true, warningsFilter: () => true, extractComments: false, sourceMap: false, cache: true, cacheKeys: defaultCacheKeys => defaultCacheKeys, parallel: true, include: undefined, exclude: undefined, minify: undefined, terserOptions: { compress: { arrows: false, collapse_vars: false, comparisons: false, computed_props: false, hoist_funs: false, hoist_props: false, hoist_vars: false, inline: false, loops: false, negate_iife: false, properties: false, reduce_funcs: false, reduce_vars: false, switches: false, toplevel: false, typeofs: false, booleans: true, if_return: true, sequences: true, unused: true, conditionals: true, dead_code: true, evaluate: true }, mangle: { safari10: true } } } } ] 可以看到,里面并没有new TerserPlugin(),但是里面确实又有terserOption,请问下大佬们这是不是vue-cli内置了terser插件,如果是内置插件的话为什么不会被inspect导出来呢,而且如果是这种没有new TerserPlugin()的情况,我该如何修改这个插件的配置或者删除这个插件以更换别的插件呢?
从源码可以看到,执行"npm run build"命令后,先创建"Service"类的实例而后再调用其"run"方法,而"run"方法执行的则是"build"文件下"index.js"中注册的一个异步函数,一直追踪下去可以看到最终返回是在这里: return new Promise((resolve, reject) => { // 用webpack模块执行处理后的配置 webpack(webpackConfig, (err, stats) => { stopSpinner(false); if (err) { return reject(err); } if (stats.hasErrors()) { return reject(`Build failed with errors.`); } if (!args.silent) { const targetDirShort = path.relative(api.service.context, targetDir); log(formatStats(stats, targetDirShort, api)); if (args.target === "app" && !isLegacyBuild) { if (!args.watch) { done( `Build complete. The ${chalk.cyan( targetDirShort )} directory is ready to be deployed.` ); info( `Check out deployment instructions at ${chalk.cyan( `https://cli.vuejs.org/guide/deployment.html` )}\n` ); } else { done(`Build complete. Watching for changes...`); } } } // test-only signal if (process.env.VUE_CLI_TEST) { console.log("Build complete."); } resolve(); }); 可以看到其最终调用了一个通过"const webpack = require("webpack")"引入的"webpack()",但点击跳转之后是一个"types.d.ts"文件,从这之后我就不明白后续是如何构建的,有没有了解"vue-cli"或"webpack"的大佬能说一下"webpack()"拿到配置后是如何构建的吗
二、Entry 和 Context配置入口context和entrywebpack 在构建打包时,通过 context 和 entry 这两个配置来找到打包入口路径。在配置入口时其实做了两件事:确认入口文件位置,告诉 webpack 从哪个文件开始打包描述 chunk name。如果传入一个字符串或字符串数组,那么默认的 chunk name 为 “main”,如果传入的是一个对象,则每个属性的 key 会是 chunk 的名称,该属性的值描述了 chunk 的入口点contextcontext 可以理解为配置资源入口的基础目录,在配置时要求必须使用绝对路径。如:现有的目录结构入口为 /src/script/index.js,则我们可以通过下面的配置来指定入口目录const path = require('path'); module.exports = { context: path.resolve(__dirname, './src/script'), entry: './index.js' }; module.exports = { context: path.resolve(__dirname, './src'), entry: './script/index.js' };这样配置后,命令行执行打包,发现依然成功的找到了入口并顺利打包(我们使用 hello world 的那个 demo 来执行现有配置)配置 context 的目的可以使 entry 的写法更加简洁,尤其是在多入口文件的情况下。不过 context 是可以省略的,默认值为当前工程的根目录。entry在 webpack 配置中有多种方式定义 entry 属性,如:字符串、数组、对象、函数,接下来我们展示下每种类型如何配置字符串类型直接定义入口名称module.exports = { entry: './src/script/index.js', }; // entry 单入口语法,是下面的简写 module.exports = { entry: { main: './src/script/index.js', }, }; 数组类型传入一个数组的作用是将多个文件预先合并,最终将多个依赖的内容绘制在一个 chunk 中,在打包时 webpack 会将数组中的最后一个元素作为实际的入口路径,其余文件会预先构建到入口文件。module.exports = { entry: ['lodash', './src/script/index.js'], };这种配置和下面是等效的// index.js import * from 'lodash' // webpack.config.js module.exports = { entry: './src/script/index.js', }这种写法会将 lodash 打包到我们的 bundle.js 中。这种写法类似于在 index.js 中引入 lodash,在控制台执行打包命令我们来看下生成的文件,从下面两张图可以看到在 index 中我们没有引入 lodash,但打包的文件中已经引入了 lodash对象类型如果要定义多入口文件则需要使用对象的形式,通过这种方式可以自定义 chunk name,其中对象的key即为 chunk name,对象的 value 为入口路径。在使用对象描述入口时,我们可以使用以下属性dependOn: 当前入口所依赖的入口。它们必须在该入口被加载前被加载filename: 指定要输出的文件名称import: 启动时需加载的模块library: 指定 library 选项,为当前 entry 构建一个 libraryruntime: 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunkpublicPath: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址多入口配置本质上打包后生成多个js文件module.exports = { entry: { index: ['lodash', './src/script/index.js'], vendor: './vendor' } }函数类型使用函数类型定义入口时,只要返回上面介绍的几种形式即可,如// 返回字符串 module.exports = { entry: () => './src/script/index.js' } // 返回对象 module.exports = { entry: () => ({ index: ['lodash', './src/script/index.js'], vendor: './vendor' }) }传入函数的优点是我们可以通过函数中增加一些逻辑来动态改变打包入口总结本章我们梳理了 webpack 入口配置的几种方式,包括字符串、对象、数组、函数几种三、Entry 配置实例entry 配置实例webpack 的 entry 配置在实际的应用中可以分几个场景。单页应用多页应用分离应用程序和第三方库下面我们来介绍下这几种应用单页应用对于单页应用,我们一般来定义单一入口即可module.exports = { entry: './src/index.js', };通过单一入口打包文件,可以将所有入口文件依赖的 框架、引入的工具库、各个页面内容打包到一起。这样的好处是将所有内容都打包成一个 js 文件,依赖关系清晰。但是这种方式也有个弊端,即所有模块都打到一个包中,当应用规模上升到一定程度后导致打包资源体积过大,导致页面首次加载速度变慢多页应用对于多页应用的场景,为了尽可能减少打包资源的体积,我们希望每个页面都只加载各自必要的逻辑,而不是将所有内容都打包到一个 bundle 中,我们来看下多应用的配置const path = require('path'); module.exports = { entry: { index: './src/index.js', hello: './src/hello.js' }, output: { path: path.resolve(__dirname, 'dist') } };打包后的文件如下,可以看到打包的内容中包含了 index.js 和 hello.js 两个文件。分离应用程序和第三方库在一个应用中,我们使用的框架、库、第三方依赖等往往很少会有改动,如果我们将所有内容都打包到一个 bundle 文件,一旦业务代码有一点点变更,那用户就要重新下载整个资源,这对于页面的性能是很不友好的。为了解决这个问题,我们可以使用应用程序和第三方库分离的方式来打包文件。也就是将业务代码和不频繁变更的第三方依赖分 bundle 打包,示例如下webpack.config.js module.exports = { mode: 'development', entry: { index: './src/index.js', vendor: ['lodash'] } };index.jsimport * as _ from 'lodash'在上面的配置中,index.js 仍然和之前一样不做任何处理,只是我们新添加了一个 chunk name 为 vendor 的入口,并通过数组的形式将第三方依赖添加进去,执行打包命令我们看到输出了两个打包文件。其实上面的代码虽然打包成功了,也成功提取了 vender 文件,但是打开打包后的 dist/index.js 我们发现 lodash 还是被打到文件中了,对于这种情况我们可以配合使用 optimization.splitChunks,将 vender 和 index 中的公共代码提取出来,这个方法我们后面的文章在详细介绍。通过上面的配置,我们可以业务依赖的第三方模块抽取成一个独立的 bundle,由于这个 bundle 不经常变动,因此可以有效的利用客户端缓存,在用户后续请求页面时加快整体渲染速度。
五、CSS-Loadercss-loadercss-loader 会对 import 和 url() 进行处理,就像 js 解析 import/require() 一样。使用index.html<!DOCTYPE html> <html lang="en"> <head> <title>css-loader</title> </head> <body> </body> </html>index.jssrc/index.js 文件中使用 import 导入 index.css 并输出导入的样式内容。import indexCss from "./index.css"; console.log(indexCss.toString())index.csssrc/index.css 文件中使用 import 导入 index.css。body { background-color: aqua; width: 500px; height: 500px; border: 1px solid red; }安装 css-loadernpm install css-loader -D安装成功后,将 css-loader 配置到 webpack.config.js 中module.exports = { ... module: { rules: [ { test: /\.(css)$/, use: [ { loader: 'css-loader' } ] } ] } };执行打包命令,此时在浏览器中打开 dist/index.html 发现页面没有正确展示我们定义的样式,但是控制台中输出了我们定义的样式,到了这一步,css-loader 我们就正确引入并使用了,没有正确展示效果的原因是 css-loader 对 index.js 中的 import 进行处理,默认生成一个数组存放处理后的样式字符串,并将其导出。而 style-loader 负责将 css 插入到 html 中,style-loader 的使用我们在下一节展示。配置项css-loader 包含下面几个配置项url允许启用/禁用处理 CSS 函数 url 和 image-set。如果设置为false, css-loader 将不会解析 url 或 image-set 中指定的任何路径。还可以通过传递函数来根据资源路径动态地控制这种行为。从版本4.0.0开始,绝对路径是基于服务器根目录进行解析的。src/index.cssbody { background: url(./img/1.png); }webpack.config.jsmodule.exports = { ... { loader: 'css-loader', options: { url: false, // url: true } } };在控制台我们在 index.js 中输出了 index.css 导出的字符串,我们来看下 url 设置为 false 和 true 的 background 区别。当 url 设置为 false 时, url 中的图片地址没做任何处理,当 url 值为 true 时,编译后地址为图片的路径,并且 dist 文件夹下会生成一张图片。import允许启用/禁用 @import 处理。src/index.css@import url('./main.css'); body { border: 1px solid red; }src/main.cssbody { width: 200px; height: 200px; }webpack.config.jsmodule.exports = { ... { loader: 'css-loader', options: { import: false // import: true } } };为了方便看到样式,我这安装了 style-loader 并添加到了 loader 中,我们来看下 import 设置为 false 和 true 的区别。当设置为 false 时 index.css 中的 @import 没有解析导致运行代码时找不到 main.css。modules查询参数 modules 会启用 CSS 模块规范。默认情况下,这将启用局部作用域 CSS。(你可以使用 :global(...) 或 :global 关闭选择器 and/or 规则。详情可查看 moduleswebpack.config.js{ loader: 'css-loader', options: { modules: true } }sourceMap设置 sourceMap 选项查询参数来引入 source map。例如 extract-text-webpack-plugin 能够处理它们。默认情况下取决于compiler.devtool 值,值为 false 和 eval 时,不会生成 source map,一般情况下不启用它,因为它们会导致运行时的额外开销,并增加了 bundle 大小 (JS source map 不会)。此外,相对路径是错误的,你需要使用包含服务器 URL 的绝对公用路径。webpack.config.js{ ... loader: 'css-loader', options: { sourceMap: true } }esModulecss-loader 中有时生成 esModule 模块化的形式是有益的,比如 module-concatenation 和 tree-shaking 时必须使用 esModule 模式才会生效。如果想启用 CommonJS 模块语法,则 esModule 设置为 false。webpack.config.js{ ... options: { esModule: true } }importLoaders在 src/index.css 中使用的 @import './main.css',importLoaders 选项可以定义在 @import 时使用哪些插件编译。webpack.config.js{ ... use: [ "style-loader", { loader: "css-loader", options: { // 0 => no loaders (default); // 1 => postcss-loader; // 2 => postcss-loader, sass-loader importLoaders: 2, }, }, "postcss-loader", "sass-loader", ], } exportType允许将样式导出为带有模块的数组、字符串或可构造样式表(如CSSStyleSheet)。默认值是 'array'。webpack.config.js{ ... use: [ { loader: 'css-loader', options: { exportType: 'string' } } ] } }src/index.jsimport indexCss from "./index.css"; console.log(indexCss)打包后,执行 dist/index.html 可以看到控制台输出了 index.css 中定义的样式字符串。总结本节我们介绍了 css-loader 的使用方法和 css-loader 包含的几个常用参数配置。css-loader 可以将 js 中的 import 导入样式文件进行编译并且拿到导出内容供其他插件使用。六、Style-Loaderstyle-loaderstyle-loader 一般和 css-loader 配合使用,css-loader 识别模块,通过特定的语法规则进行内容转换最后导出,style-loader 将 css-loader 导出的内容插入到 DOM。使用index.html<!DOCTYPE html> <html lang="en"> <head> <title>style-loader</title> </head> <body> </body> </html>index.jssrc/index.js 文件中使用 import 导入 index.css。import indexCss from './index.css';index.css设置一个长宽均为 200 像素的带红色边框的正方形body { border: 1px solid red; width: 200px; height: 200px; }安装 style-loadernpm install style-loader -D安装成功后,将 style-loader 配置到 webpack.config.js 中,配置中我们使用 css-loader 和 style-loader 两个加载器,webpack 中 loader 的解析一般由右向左,由下向上解析,所以 webpack 会先执行 css-loader,css-loader 导出内容传给 style-loader,最后在执行 style-loader。module.exports = { ... module: { rules: [ { test: /\.css$/, use: ['style-loader','css-loader'] } ] } };执行打包命令,此时在浏览器中打开 dist/index.html 发现页面可以正常展示我们设置的样式,我们在控制台可以看到样式被插入到 head 标签中。配置项style-loader 包含下面几个配置项injectType设置样式如何注入 DOM,默认为 styleTag,即使用多个<style></style> 模式。styleTagsrc/index.css@import url('style.css'); .bar { color: blue; }src/style.css.foo { color: red; }webpack.config.jsmodule.exports = { ... { test: /\.(css)$/, use: [ { loader: 'style-loader', options: { injectType: "styleTag" } }, 'css-loader' ] } };index.jsimport index from "./index.css"; const divElement = document.createElement("div"); divElement.className = "foo"; divElement.innerHTML = 'style-loader' document.body.appendChild(divElement)执行打包命令,在浏览器中打开 dist/index.html 文件,我们可以看到 head 中插入了两个 style 标签,div 中的文字颜色可以正常显示。singletonStyleTag将多个样式文件内容在一个 style 标签中插入 DOM。webpack.config.jsmodule.exports = { ... { test: /\.(css)$/, use: [ { loader: 'style-loader', options: { injectType: "singletonStyleTag" } }, 'css-loader' ] } };lazyStyleTag<style></style> 按需注入到 DOM 。建议遵循 .lazy.css 惰性样式的命名约定和 .css 基本style-loader用法。当使用 lazyStyleTag 时,可以通过 style-loader 的 style.use()、style.unuse() 按需使用。src/style.lazy.css.foo { color: red; }src/index.jsimport styles from "./style.lazy.css"; styles.use(); const divElement = document.createElement("div"); divElement.className = "foo"; divElement.innerHTML = 'style-loader' document.body.appendChild(divElement)webpack.config.jsmodule.exports = { ... { test: /\.(css)$/, use: [ { loader: 'style-loader', options: { injectType: "lazyStyleTag" } }, 'css-loader' ] } };打包后,可以看到样式被插入到 DOM,并且颜色已生效,如果 index.js 中没有调用 styles.use(),则样式不会被插入到 DOM。attributes将指定的属性值附加到 <style> 或 <link> 标签webpack.config.js{ loader: 'style-loader', options: { attributes: {id: 'styleLoader'} } } insert默认情况下 style-loader 会将<style>、<link> 标签插入到 <head> 标签尾部,设置 insert 后,可以将样式标签插入到其他位置。webpack.config.js{ loader: 'style-loader', options: { insert: 'body' } }styleTagTransform当插入 style 标签到 DOM 时转换标签和 css,可以设置自定义方法解析 style 标签插入方式。webpack.config.js{ ... loader: 'style-loader', options: { styleTagTransform: function(css, style) { style.innerHTML = `${css}.modify{}\n` document.head.appendChild(style) } }打包后,在浏览器中打开 dist/index.html,我们可以看到 css 样式后面添加了我们在方法中书写的内容。esModulestyle-loader 中生成 esModule 模块化的形式是有益的,比如 tree-shacking 时必须使用 esModule 模式才会生效。如果想启用 CommonJS 模块语法,则 esModule 设置为 false。webpack.config.js{ ... options: { esModule: true } }base当使用一个或多个 DllPlugin 时,此设置主要用作 css 冲突的解决方法。允许您通过指定大于 DllPlugin1 使用的范围的 css 模块 ID 基数来防止 app 的 css(或DllPlugin2 的 css)覆盖 DllPlugin1 的 csswebpack.dll1.config.js{ ... use: ["style-loader", "css-loader"], }webpack.dll2.config.js{ ... use: [ { loader: "style-loader", options: { base: 1000 } }, "css-loader", ], }webpack.app.config.js{ ... use: [ { loader: "style-loader", options: { base: 2000 } }, "css-loader", ], }总结本节我们介绍了 style-loader 的使用方法和 style-loader 包含的几个常用参数配置。style-loader 一般和 css-loader 配合使用。七、Postcss-Loaderpostcss-loader在使用 postcss-loader 之前,我们先来了解下 PostCSS。以下摘自 PostCSS 简介。PostCSS 是一个允许使用 JS 插件转换样式的工具。 这些插件可以检查(lint)你的 CSS,支持 CSS Variables 和 Mixins, 编译尚未被浏览器广泛支持的先进的 CSS 语法,内联图片,以及其它很多优秀的功能。PostCSS 的 Autoprefixer 插件是最流行的 CSS 处理工具之一。由此我们可以知道通过使用 PostCSS 和 相应的插件,我们可以完成样式格式化、自动根据浏览器的支持情况增加样式前缀、使用先进的 CSS 特性等很多优秀的功能,截止到目前,PostCSS 有 200 多个插件。你可以在 插件列表 找到他们。如果我们想在 webpack 中使用 PostCSS 及 相应插件完成我们想要的功能,这时就需要通过 postcss-loader 来处理。使用index.html<!DOCTYPE html> <html lang="en"> <head> <title>postcss-loader</title> </head> <body> </body> </html>index.jssrc/index.js 文件中使用 import 导入 index.css 并输出导入的样式内容。import indexCss from "./index.css";index.cssbody { display: flex; }安装 postcss-loader 和 postcssnpm install postcss-loader postcss -D安装成功后,将 postcss-loader 配置到 webpack.config.js 中,postcss-loader 通过加载插件转换 css 内容,转换后的内容虽然是 .css 文件,但是仍需传递给 css-loader 做后续处理。module.exports = { ... module: { rules: [ { test: /\.(css)$/, use: ['style-loader', 'css-loader', 'postcss-loader'] } ] } };以上只是将 postcss-loader 配置到了 webpack 中,执行打包命令可以正常打包,但 css 内容不会发生变化,postcss-loader 需要通过插件来达到我们想要的效果,下面我们以自动添加前缀为例看下效果。安装 autoprefixernpm install autoprefixer -Dpostcss.config.js项目根目录下新建 postcss.config.js 文件,配置 autoprefixer 插件,和要对应的浏览器版本。其中 browsers 的配置可以配置到 package.json 或者 .browserslistrc 文件下,如果配置到 postcss.config.js 中打包时会有警告,不过我们为了演示 autoprefixer 效果,不对此做处理。module.exports = { plugins: [ require('autoprefixer')({ 'browsers': ['> 1%', 'last 2 versions'] }) ] };配置完成后,执行打包命令,打包成功后在浏览器中打开 dist/index.html 文件,在控制台中我们可以看到,样式代码已经自动增加前缀。配置项postcss-loader 包含下面几个配置项executepostcssOptionssourceMapimplementationexecute默认:undefined值类型:Boolean作用:在 CSS-in-JS 中,如果您想要处理在 JavaScript 中书写的样式,需要使用 postcss-js 解析器,添加 execute 选项并设置为 true。webpack.config.jsmodule.exports = { ... { loader: "postcss-loader", options: { postcssOptions: { parser: "postcss-js", }, execute: true, }, } };postcssOptions默认:undefined值类型:Object | Function作用:允许设置 PostCSS options 和插件。支持所有PostCSS选项。webpack.config.jsmodule.exports = { ... { loader: "postcss-loader", options: { // object postcssOptions: { ... } // function postcssOptions: (loaderContext) => { return { ... } } }, } };sourceMap默认:取决于 devtool 选项值类型:Boolean作用:是否开启 sourceMapwebpack.config.jsmodule.exports = { ... { loader: "postcss-loader", options: { sourceMap: true }, } }; implementation默认:postcss值类型:Function | String作用:implementation 选项决定使用哪个 PostCSS 实现。覆盖本地安装的 postcss 的 peerDependency 版本webpack.config.jsmodule.exports = { ... { loader: "postcss-loader", options : { implementation : require ( "postcss" ) } } };总结本节我们介绍了 postcss-loader 的使用方法,postcss-loader 主要是 PostCSS 在 webpack 环境下的使用方法,通过加载不同的插件来达到处理样式文件的效果,PostCSS 支持的插件非常丰富,本文只是通过自动添加前缀的例子做一个展示,想看其他插件的使用方法可以去 PostCSS 官网查看。八、Sass-Loadersass-loaderSass sass 是一种 css 的预编译语言。它提供了 变量(variables)、嵌套(nested rules)、 混合(mixins)、 函数(functions)等功能,并且完全兼容 css,sass 能够帮助复杂的样式表更有条理,并且易于在项目内部或跨项目共享设计。当 css 变得越来越臃肿、 越来越复杂、越来越难以维护时 sass 为我们提供了 css 中不存在的特性辅助我们编写健壮、 可维护的 css 代码。在使用 sass 之前,需要在项目中安装它,而 sass-loader 的作用是加载 sass/scss 文件并将其编译为 css,通过将 style-loader 和 css-loader 与 sass-loader 链式调用,可以立刻将样式作用在 DOM 元素。使用index.html<!DOCTYPE html> <html lang="en"> <head> <title>sass-loader</title> </head> <body> <p>hello sass</p> </body> </html>index.jssrc/index.js 文件中使用 import 导入 index.scss。import indexCss from "./index.scss";index.scsssass 和 scss 其实是同一种东西,我们平时都称之为 sass,不过两者之间写法也存在些区别,感兴趣的小伙伴儿可自行查阅,我们本案例都以 scss 为例。$primary-color: #f00; body { color: $primary-color; }安装 sass-loader 和 sassnpm install sass-loader sass --save-dev安装成功后,将 sass-loader 配置到 webpack.config.js 中。module.exports = { ... module: { rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] } ] } };执行打包命令后,在浏览器打开 dist/index.html 文件,我们可以看到我们在 index.scss 文件中定义的颜色常量被正确的显示到了标签上面,在控制台可以看到,color 的颜色属性已由定义的变量转换成了定义的颜色值。配置项sass-loader 可以通过指定 options 参数,向 sass 传递选项参数。implementationsassOptionssourceMapadditionalDatawebpackImporterwarnRuleAsWarningimplementation默认值:sass值类型:Object | String作用:sass-loader 要使用的 sass 实现。默认情况下,sass-loader 会根据你的依赖解析需要使用的实现。 只需将必需的实现添加到 package.json(sass 或 node-sass 包)中并安装依赖项即可。package.json// sass-loader 将会使用 sass 实现 { "devDependencies": { "sass-loader": "^7.2.0", "sass": "^1.22.10" } }package.json// sass-loader 将会使用 node-sass 实现 { "devDependencies": { "sass-loader": "^7.2.0", "node-sass": "^5.0.0" } }同时安装 node-sass 和 sass 的情况下,sass-loader 默认会选择 sass。 为了避免这种情况,你可以使用 implementation 选项。webpack.config.jsmodule.exports = { ... { loader: "sass-loader", options: { // Object implementation: require('sass') // String implementation: require.resolve('sass') }, } }; sassOptions默认值:sass 实现的默认值值类型:Object | Function作用:设置 sass 实现的启动选项。在使用他们之前,请查阅有关文档:Dart Sass 文档提供了所有可用的 sass 选项。webpack.config.jsmodule.exports = { ... { { loader: "sass-loader", options: { // Object sassOptions: { includePaths: ['absolute/a', 'absolute/b'], }, // Function sassOptions: (loaderContext) => { // 有关可用属性的更多信息 https://webpack.js.org/api/loaders/ ... return { includePaths: ['absolute/a', 'absolute/b'], }; }, }, } } }; sourceMap默认值:取决于 complier.devtool值,值为 false 和 eval 时,不会生成 source map。值类型:Boolean作用:是否开启 sourceMapwebpack.config.jsmodule.exports = { ... { loader: "sass-loader", options: { sourceMap: true }, } }; additionalData默认值:undefined值类型: String | Function作用:在实际的文件之前要添加的 sass / scss 代码。下面的示例中,width、value 可以在 index.scss 中直接引用。webpack.config.jsmodule.exports = { ... { loader: "sass-loader", options : { // String additionalData: '$width:' + process.env.NODE_ENV + ';', // Function sync additionalData: (content, loaderContext) => { ... return '$value: 100px;' + content; } // Function async additionalData: async (content, loaderContext) => { ... return '$value: 100px;' + content; } } } }; webpackImporter默认值:true值类型: Boolean作用:开启 / 关闭默认的 Webpack importer。在某些情况下,可以提高性能。但是请谨慎使用,因为 aliases 和以 〜 开头的 @import 规则将不起作用。 你可以传递自己的 importer 来解决这个问题(参阅 importer docs)。webpack.config.jsmodule.exports = { ... { loader: "sass-loader", options : { webpackImporter: false, } } };warnRuleAsWarning默认值:false (在下一个大版本发布中它将默认设置为 true)值类型: Boolean作用:将 @warn 规则视为 webpack 警告而不是日志。index.scss$known-prefixes: webkit, moz, ms, o; @mixin prefix($property, $value, $prefixes) { @each $prefix in $prefixes { @if not index($known-prefixes, $prefix) { @warn "Unknown prefix #{$prefix}."; } -#{$prefix}-#{$property}: $value; } #{$property}: $value; } body { @include prefix('display', 'flex', 'a'); }webpack.config.jsmodule.exports = { ... { loader: "sass-loader", options : { warnRuleAsWarning: true, } } };在上面的例子中,我们 $prefixes 值传入 a,执行打包命令,虽然可以打包成功,但是控制台会输出警告,如果我们将 $prefixes 值传入 $known-prefixes 中定义的 o,则控制台不会显示警告。总结本节我们介绍了 sass-loader 的使用方法和一些配置参数。通过使用 sass-loader 和 sass 可以让我们让我们的样式表更有条理并且易于维护。
公告公告 这个代码是"index.html" 的代码,其中引入了"header.html" 文件,这个"header.html" 里面也是包含"tailwindcss" 的类名,但是使用"webpack" 打包的时候,是不会被打包进去的,这里应该是被当做 字符串处理了。 webpack config配置 const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: { index: "./src/ts/index.ts", }, output: { path: path.resolve(__dirname, "dist"), filename: "./js/[name].js", assetModuleFilename: "./imgs/[name][ext][query]", }, plugins: [ new MiniCssExtractPlugin({ filename: "./css/style.css", }), new HtmlWebpackPlugin({ filename: "index.html", template: "./src/index.html", chunks: ["index"], }), ], module: { rules: [ { test: /\.scss$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: "../", }, }, { loader: "css-loader", options: { importLoaders: 1 }, }, "sass-loader", "postcss-loader", ], }, { test: /\.(png|svg|jpg|gif|webp)$/, type: "asset/resource", }, { test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/, }, ], }, resolve: { extensions: [".tsx", ".ts", ".js"], }, mode: "production", devServer: { client: { overlay: false, logging: "none", }, open: true, hot: true, port: 9000, }, }; 请问: webpack 如何将"raw-loader" 引入的文件里面的tailwindscss 也一起打包到新的css文件中!
九、Svg-URL-Loadersvg-url-loadersvg-url-loader 可以将 svg 文件加载为 utf-8 编码的 data-uri 字符串。url-loader 也可以加载 svg 文件,和 svg-url-loader 的区别是 url-loader 将 svg 文件加载为 base64 编码的字符串。utf-8 编码相对于 base64 编码有一些优势。编译结果字符串更短(对于 2K 大小的图标,可以缩短约 2 倍);使用 gzip 压缩时,生成的字符串将被更好地压缩;浏览器解析 utf-8 编码的字符串比 base64 编码的字符串更快;使用index.html<!DOCTYPE html> <html lang="en"> <header> <title>svg-url-loader</title> </header> <body> <img id="svg-loader" /> </body> </html>index.jsimport svgContent from './img/headIcon.svg'; window.document.getElementById('svg-loader').src = svgContent;安装 svg-url-loadernpm install svg-url-loader --save-dev安装成功后,将 svg-url-loader 配置到 webpack.config.js 中。module.exports = { ... module: { rules: [ { test: /\.svg/, use: { loader: "svg-url-loader", }, } ] } };执行打包命令后,在浏览器打开 dist/index.html 文件,我们可以看到 svg 图片被展示在页面是上,打开 dist/main-[hash].js 文件可以看到 svg 图片被编译成了 utf-8 编码的字符串。配置项svg-url-loader 包含下面参数。limitstripdeclarationsiesafeencodinglimit默认值:无值类型:Number作用:当设置 limit,如果源文件的内容大于这个限制,svg-url-loader 将不编码源文件。如果文件大于 limit 设置的限制,将使用 file-loader 加载文件,svg-url-loader 中设置的参数会传递给 file-loader。webpack.config.jsmodule.exports = { ... { test: /\.svg/, use: { loader: "svg-url-loader", options: { limit: 1024 // 文件大小 1M } } } };stripdeclarations默认值:true值类型:Boolean作用:它将在下一个主要版本中被删除,删除所有 XML 声明。例如:svg 图片开头的 <?xml version="1.0" encoding="UTF-8"?>。webpack.config.jsmodule.exports = { ... { test: /\.svg/, use: { loader: "svg-url-loader", options: { stripdeclarations: false } } } }; iesafe默认值:无值类型:Boolean作用:当 iesafe 选项设置为 true,svg-url-loader 在编译文件时,如果文件包含一个样式元素并且编码大小超过 4kB,则无论指定的限制如何,都使用 file-loader 编译。因为 ie 浏览器包括 ie11 已经停止解析 svg 数据中的样式元素和大小超过 4kb 的文件,会导致所有样式的形状都是黑色填充。webpack.config.jsmodule.exports = { ... { loader: "svg-url-loader", options: { iesafe: true }, } };encoding默认值:"none"值类型:"none" | "base64"作用:设置 svg-url-loader 构造数据 URI 时要使用的编码。webpack.config.jsmodule.exports = { ... { loader: "svg-url-loader", options: { encoding: "base64" }, } };总结本节我们介绍了 svg-url-loader 的使用方法和一些配置参数。svg-url-loader 主要用来编译 svg 格式文件,默认采用 utf-8 编码。十、Svg-Sprite-Loadersvg-sprite-loadersvg-sprite-loader 作用是合并多个单个的 svg 图片为一个 sprite 雪碧图,并把合成好的内容,插入到 html 内,其原理是利用 svg 的 symbol 元素,将每个 svg 图片 包括在 symbol 中,通过 use 元素使用该 symbol。svg 元素参考。使用index.html在 index.html 中,通过使用 svg 的 use 元素渲染两张 svg 图片,分别对应 img 文件夹下的 headIcon.svg 和 home.svg。<!DOCTYPE html> <html> <header> <title>svg-sprite-loader</title> </header> <body> <svg> <use xlink:href="#headIcon"></use> </svg> <svg> <use xlink:href="#home"></use> </svg> </body> </html>index.js导入 index.html 中 use 元素加载的两张 svg 图片。import svgContent from './img/headIcon.svg'; import svgHome from './img/home.svg';安装 svg-sprite-loadernpm install svg-sprite-loader --save-dev安装成功后,将 svg-sprite-loader 配置到 webpack.config.js 中。module.exports = { ... module: { rules: [ { test: /\.svg/, use: { loader: "svg-sprite-loader" }, } ] } };执行打包命令后,在浏览器打开 dist/index.html 文件,我们可以看到 svg 图片被展示在页面上,在控制台查看 elements 选项,可以看到两个 svg 图片被包裹在 两个 symbol 标签中,使用时通过 use 标签传入 symbol 元素 id 来显示不同的 svg 图片。配置项svg-sprite-loader 包含下面基础参数配置。symbolIdsymbolRegExpesModulesymbolId默认值:[name]值类型:String | Function作用:设置 svg 标签中 symbol 元素的 id 值。html<svg> <use xlink:href="#icon-headIcon"></use> </svg> webpack.config.jsmodule.exports = { ... { test: /\.svg/, use: { loader: "svg-sprite-loader", options: { // string symbolId: 'icon-[name]' // function symbolId: filePath => path.basename(filePath) } } } }; symbolRegExp默认值:''值类型:String作用:传递给 symbolId 插值器以支持 loader-utils 名称插值器中的 [N] 模式。esModule默认值:true值类型:Boolean作用:是否使用 esModule 语法。如果使用 CommonJS 语法则参数设置为 false。webpack.config.jsmodule.exports = { ... { loader: "svg-sprite-loader", options: { esModule: true }, } };svg-sprite-loader 还支持运行时配置和提取配置,想了解的小伙伴儿可自行查阅文档。总结本节我们介绍了 svg-sprite-loader 的使用方法和一些配置参数。svg-sprite-loader 主要用来将从 css/scss/sass/less/styl/html 导入的图像生成外部 sprite 文件,通过使用 svg 的 use 元素展示 图像内容。达到统一管理的目的。十一、VUE-Loadervue-loaderVue Loader 是一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFCs)的格式撰写 Vue 组件。Vue Loader 还提供了很多酷炫的特性:允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;使用 webpack loader 将 <style> 和 <template> 中引用的资源当作模块依赖来处理;为每个组件模拟出 scoped CSS;在开发过程中使用热重载来保持状态;使用在下面的例子中,正常要使用 Vue.js 将组件渲染到页面上。但是这样会增加一些额外的代码,容易混淆,所以下面的例子只是完成打包不报错即认为 vue-loader 起到了作用。index.html<!DOCTYPE html> <html> <head> <title>vue-loader</title> </head> <body> </body> </html>index.vue<template> <div class="demo">{{ msg }}</div> </template> <script> export default { data () { return { msg: 'Hello world!' } } } </script> <style> .demo { color: blue; } </style>安装 vue-loadervue-loader 需要配合 vue-template-compiler 一起使用。npm install vue-loader vue-template-compiler --save-dev安装成功后,将 vue-loader 配置到 webpack.config.js 中。module.exports = { ... // webpack.config.js const { VueLoaderPlugin } = require('vue-loader'); module.exports = { ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', }, // 它会应用到普通的 `.css` 文件 // 以及 `.vue` 文件中的 `<style>` 块 { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] }, plugins: [ // 请确保引入这个插件! // 这个插件是必须的!它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。 new VueLoaderPlugin() ] };执行打包命令后,控制台显示编译成功。配置项vue-loader 参数配置。transformAssetUrlscompilercompilerOptionstranspileOptionsoptimizeSSRhotReloadproductionModeshadowModecacheDirectory / cacheIdentifierprettifyexposeFilenamevue-loader 的配置项官网显示很清楚,大家可以去官网查看,vue-loader配置。总结vue-loader 的总结大部分源自官网介绍,通过上面的使用我们可以完成一个简单的 vue-loader 配置及使用。十二、VUE-Style-Loadervue-style-loadervue-style-loader 是基于 style-loader 的分支,功能与 style-loader 类似,都可以与 css-loader 链接将 style 标签注入到文档中,vue-style-loader 一般不需要自己配置加载,因为他已经作为依赖项包含在 vue-loader 中。vue-style-loader 除了将 style 注入到文档中,还做了一些服务端渲染的支持,所以如果我们 vue 项目中需要做服务端渲染,可能就要使用 vue-style-loader 来插入样式了。使用vue-style-loader 的使用方法与 style-loader 类似。都是与 css-loader 链接起来使用。index.html<!DOCTYPE html> <html> <head> <title>vue-style-loader</title> </head> <body> </body> </html>index.jsimport indexCss from "./index.css"; const divElement = document.createElement("div"); divElement.className = "demo"; divElement.innerHTML = 'vue-style-loader'; document.body.appendChild(divElement);index.css.demo { color: red; }安装 vue-style-loadernpm install vue-style-loader --save-dev安装成功后,将 vue-style-loader 配置到 webpack.config.js 中。module.exports = { ... rules: [ { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ] } ] };打包成功后,打开 dist/index.html 可以看到浏览器中文字颜色已经变成红色,在控制台 element 中可以看到 index.css 中的样式属性已经被包裹了 style 标签并插入到 head 中。配置项vue-style-loader 参数配置。manualInjectssrIdmanualInject默认值:无值类型:Boolean作用:当 manualInject 参数值为 true 时,导入的样式对象会提供一个__inject__方法,然后可以在适当的时间手动调用该方法。此方法接收一个对象参数,最后将样式文件内容绑定到传入的对象上。注意:只有运行环境为 Node.js 且 manualInject 为 true 时,样式对象才会提供__inject__方法。webpack.config.jsmodule.exports = { ... { test: /\.svg/, use: [ { loader: 'vue-style-loader', options: { manualInject: true, } }, ] } }; ssrId默认值:无值类型:Boolean作用:向 style 标签添加 data-vue-ssr-id 属性,可以用作预渲染避免样式重复注入。webpack.config.jsmodule.exports = { ... { test: /\.svg/, use: [ { loader: 'vue-style-loader', options: { ssrId: true } }, ] } };区别于 style-loader如果你正在构建一个 Vue SSR 应用程序,你可能也应该使用这个加载器来处理从 JavaScript 文件导入的 CSS不支持 url 模式和引用计数模式。还删除了 singleton 和insertAt 查询选项。不支持样式懒加载。如果您需要这些功能,您可能应该使用原始功能 style-loader。总结本章节我们介绍了 vue-style-loader 的使用、配置和它与 style-loader 的区别,vue-style-loader 支持 vue 中的 ssr(服务端渲染),所以如果需要支持服务端渲染的 vue 项目,就需要用到 vue-style-loader 了。但是如果是一般的项目,style-laoder 的功能会更多些。
二、Tree Shakingtree-shaking所谓 tree-shaking 就是 “摇树” 的意思,可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的静态结构特性,例如 import 和 export。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。 webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。例子tree shaking 会在编译过程中将未使用的代码进行标记,在 uglify 时移除被标记的无效代码,在 mode 设置为 production 时,webpack 会开启压缩和 tree-shaking 等优化,下面的例子如果配置为生产模式,打包后未使用的引用会被移除掉。1、将 mode 设置为 none(不使用任何打包优化),optimization 配置中增加 usedExports 。index.js// add 只是导入没有使用 import { add } from './util.js'; const divElement = document.createElement('div'); divElement.innerHTML = addRes; divElement.className = 'demo'; document.body.appendChild(divElement);util.js// 导出 add 函数 export const add = (a, b) => { return a + b }webpack.config.js... module.exports = { ... mode: 'none', // 不使用任何默认优化选项 optimization: { // 告诉 Webpack 去决定每一个模块所用到的导出 usedExports: true, }, };执行打包命令后,查看 dist/index.js 中的代码。可以看到 index.js 中只做了导入但是没有调用的方法 add 被打了 unused harmony export 的注释。2、package.json 中增加 sideEffects: false "sideEffects": false,webpack.config.js 中移除 optimization 配置,index.js 与 util.js 中内容不变,执行打包命令后,可以看到打包后的 index.js 中 add 函数已经被移除。sideEffects 与 usedExports在了解上面两个参数区别之前,我们先来看下函数的副作用。函数副作用是指函数在正常工作任务之外对外部环境所施加的影响。具体地说,函数副作用是指函数被调用,完成了函数既定的计算任务,但同时因为访问了外部数据,尤其是因为对外部数据进行了写操作,从而一定程度地改变了系统环境。在上面例子中我们在 index.js 中导入的 util.js 中导出了一个 add 函数,我们来看下这个函数。export const add = (a, b) => { return a + b }add 函数中接收了 a 和 b 两个参数,函数体中返回了 a 与 b 的和,此函数返回的结果只依赖于传入的值并且没有其他影响,如修改全局变量等操作,则此函数即为无副作用函数。export const add = (a, b) => { window.userName = '***' return a + b }上面的 add 函数除了返回了 a 与 b 的和的同时还修改了 window对象中属性的值,则此函数即为有副作用的函数。简单了解了函数副作用,我们来看下 sideEffects 和 usedExports(更多被认为是 tree shaking)的两种不同的优化方式。usedExports 依赖于 terser 去检测语句中的副作用,对未使用的函数增加标记,之后交由压缩工具去移除死代码。webpack.config.js... module.exports = { ... mode: 'none', // 不使用任何默认优化选项 optimization: { // 告诉 Webpack 去决定每一个模块所用到的导出 usedExports: true, // 开启压缩 minimize: true }, };index.js 与 util.js 内容保持不变,在 webpack.config.js 的 optimization 配置中增加参数 minimize 为 true,打包成功后查看输出文件,可以看到之前被打了 unused harmony export 注释的代码被移除掉了。下面我们来写一个带有副作用的方法,看看 usedExports 会如何处理。index.jsimport './util.js'; const divElement = document.createElement('div'); divElement.innerHTML = 'demo'; divElement.className = 'demo'; document.body.appendChild(divElement);util.jsArray.prototype.custom = function () { console.log('custom'); }; export const add = (a, b) => { return a + b; };util.js 中修在 Array 原型链上增加了方法,所以是 util.js 是有副作用的,在 index.js 中我们不只导入 add 方法而是导入整个 util.js 文件,执行打包命令后可以查看 dist/index.js 文件中 add 方法已被删除,有副作用的部分依然在打包文件中。sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。通过 package.json 的 "sideEffects" 属性,来实现这种方式。如果所有代码都不包含副作用,我们就可以将 sideEffects 标记为 false,来告诉 webpack 它可以跳过安全检测删除未用到的 export。依然使用上面包含副作用的代码。来看下增加 sideEffects: false 的效果。package.json{ // ... "sideEffects": false, // ... }webpack.config.jsmodule.exports = { mode: 'none', entry: { index: './src/index.js', }, output: { path: path.join(__dirname, 'dist'), filename: '[name].js', } };执行打包命令后查看 dist/index.js 文件,可以看到包含副作用的代码也被移除,即 util.js 导入部分被全部移除。项目中包含副作用的函数被移除在打包后会导致部分功能不可用,所以 sideEffects 支持传入参数,告知 webpack 传入的文件包含副作用不要移除。上面的例子中修改 package.json 的配置,将包含副作用的文件传入进去。此数组支持简单的 glob 模式匹配相关文件。其内部使用了 glob-to-regexp(支持:*,**,{a,b},[a-z])。如果匹配模式为 *.css,且不包含 /,将被视为 **/*.css。package.json{ // ... "sideEffects": ["./src/util.js" ], // ... }执行打包命令后查看输出文件,可以看到只有导入但是没使用的 add 函数和 Array 方法都被保留了下来。tree shaking 开启条件tree shaking 需要使用 ES2015 模块语法(即 import 和 export)才能生效,有时我们会发现只引用了某个库中的一个方法,却把整个库都加载进来了,同时 bundle 的体积也并没有因为 tree shaking 而减少。这可能是该库不是使用 ESModule 模式导出的,所以在使用某个库时,我们尽量引入 es 版,按需加载,这样 tree shaking 才能将无用代码删除掉。总结为了利用 tree shaking 的优势, 我们必须使用 ES2015 模块语法(即 import 和 export)。确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档)。在项目的 package.json 文件中,添加 "sideEffects" 属性。使用 mode 为 "production" 的配置项以启用更多优化项,包括压缩代码与 tree shaking。二、代码分离及懒加载代码分离及懒加载前端性能优化一直是围绕在每一个前端周围的话题,减少网络请求、减少加载 bundle 体积、外部资源放在 CDN 等等。对于性能优化 webpack 也提供了一些手段。下面让我们来了解下代码分离、缓存和懒加载。代码分离代码分离是 webpack 最引人注目的特性之一,试想如果项目中所有代码都打包到一个 bundle 中,那 bundle 的体积将会变大,这对首次访问页面来说,加载资源的请求时间会变长,将影响用户体验。所以前端性能优化的一个方向是将代码按照一定规则拆分到不同的 bundle 中,触发不同的功能加载不同的资源,这样除了减少资源体积外还能增快请求响应速度。不过拆分的粒度大小还是要看实际的项目需求,无限拆分资源包也会造成资源请求过多。所以对于代码分离我们要结合项目情况合理使用,这会极大影响加载时间。常用的代码分离方法有三种:入口分离:使用 entry 配置手动地分离代码。去重分离:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。动态导入:通过模块的内联函数调用来分离代码。入口分离入口分离即从 webpack 入口文件配置处分离 bundle。这种分离方式根据项目的多个入口拆分出多个 bundle,这是所有分离方式中最简单的分离方式,不过这种方式也存在弊端即 输出的 bundle 会同时包含导入的三方依赖代码(重复代码),在后面的分离方式讲解中会解决这个问题。我们先来看看入口分离的例子。src/index.jsindex.js 文件中引入依赖包 lodash 的 join 方法。创建 div 标签并将内容添加到 body 中。import { join } from 'lodash'; const divElement = document.createElement('div'); divElement.innerHTML = join(['hello', 'index'], '-'); document.body.appendChild(divElement);src/main.jsmain.js 文件中同样引入依赖包 lodash 的 join 方法。创建 div 标签并将内容添加到 body 中,只是展示内容与 index.js 不一致。import { join } from 'lodash'; const divElement = document.createElement('div'); divElement.innerHTML = join(['hello', 'main'], '-'); document.body.appendChild(divElement);webpack.config.js在 entry 配置中传入两个入口,chunk 的名字分别是 index 和 main。const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); ... module.exports = { mode: 'development', entry: { index: './src/index.js', main: './src/main.js' }, output: { path: path.join(__dirname, 'dist'), filename: '[name].js', }, plugins: [ ... new HtmlWebpackPlugin({ template: './index.html', }), ], };执行打包命令后,在 dist 文件夹下可以看到,同时输出了 mian.js 和 index.js 文件,通过配置 entry 实现了代码分离。我们分别打开 dist/index.js 和 dist/main.js 后可以看到两个 bundle 中都包含了 lodash 的源码。去重分离在入口分离的基础上,我们继续优化将重复的部分单独分离出一个 bundle,在 entry 中配置 dependOn 选项,这样可以在多个 chunk 之间共享模块。webpack.config.js在 entry 配置中将两个入口文件按照对象的形式定义,除定义入口之外,增加了一个 dependOn 选项,传入的 vendor 为共享模块的 chunk 名称。const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); ... module.exports = { ... entry: { index: { import: './src/index.js', dependOn: 'vendor', }, main: { import: './src/main.js', dependOn: 'vendor', }, vendor: 'lodash' }, };执行打包命令后,在 dist 文件夹下除了两个入口文件外还多了一个 vendor.js 文件,文件的内容即为 lodash 源码。HtmlWebpackPlugin 插件将三个 js 文件都注入到了 index.html 中。项目可以正常运行。通过在入口配置 dependOn 属性,虽然可以实现公共代码抽离,但是还存在一个问题是,在我们的项目中会有很多公共代码,难道我们要手动的都添加到 dependOn 中吗?这将会增加非常多的工作量并且容易出错。这时我们可以考虑使用 webpack 的 SplitChunksPlugin 插件了。SplitChunksPluginSplitChunksPlugin 插件不需要单独安装,是 webpack 提供的开箱即用的插件。我们通过 optimization 配置即可。默认情况下,SplitChunksPlugin 它只会影响到按需加载的 chunks,不过我们可以通过传入 chunks 属性不同变量来决定影响哪些 chunk 。webpack 将根据以下条件自动拆分 chunks:新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹新的 chunk 体积大于 20kb(在进行压缩和 gzip 之前的体积)当按需加载 chunks 时,并行请求的最大数量小于或等于 30当加载初始化页面时,并发请求的最大数量小于或等于 30我们使用 SplitChunksPlugin 来拆分下上个例子中的 lodash,关于 SplitChunksPlugin 的各项配置参数含义,感兴趣的伙伴可以查看官网webpack.config.js... module.exports = { entry: { index: './src/index.js', main: './src/main.js', }, optimization: { splitChunks: { // 设置为 all 意味着 chunk 可以在异步和非异步 chunk 之间共享 chunks: 'all', }, }, ... };执行打包命令后,在 dist 文件夹下可以看到除了 index.js 和 main.js 外还输出了一个文件,打开文件可以看到内容正是 lodash。动态导入动态导入也叫资源异步加载,当模块数量过多时,可以把一些暂时用不到的模块延迟加载,以此来减少用户初次加载页面的体积,后续模块等到恰当的时机再去加载,因此也把这种加载方式叫懒加载。在 webpack 中提供了两种类似的技术来实现动态加载。import 函数及 require.ensure。require.ensure 是 Webpack 1 支持的异步加载方式,从 Webpack 2 开始引入了 import 函数,并且官方更推荐使用 import 函数的方式实现异步加载,因此下面我们只介绍 import 函数的使用。通过 import 函数加载的模块会被异步的进行加载,并且返回一个 Primise 对象。main.jsexport const add = (a, b) => { console.log(a + b); }index.jsindex 文件中通过 import 函数导入 main 文件内容并赋值给 addFunc,在 index 中通过 setTimeout 定时任务 2 秒钟后执行 addFunc 回调函数,在回调函数返回值中通过 ES6 的对象解构语法获取到 add 函数。const addFunc = import('./main'); setTimeout(() => { addFunc.then(({add}) => { add(3, 8); }); }, 2000);webpack.config.js... module.exports = { mode: 'development', entry: { index: './src/index.js' }, output: { path: path.join(__dirname, 'dist'), filename: '[name].js', }, ... };通过 webpack.config.js 配置我们可以看到入口文件只有 index.js,执行打包命令后在生成的 dist 文件夹中可以看到除了入口文件 index.js 外,还生成了一个 src_main_js.js 的文件,内容即为 src/main.js 中内容对应的编译后代码。通过 import 函数异步加载文件也可以实现拆分代码的功能,这种方式在实际的项目开发中非常实用,非主页面加载的内容都可以通过 import 函数动态加载,在点击到对应页面时在加载相应资源。上面的例子中还可以进一步优化下异步加载文件的 chunk 名称。其他配置不变,我们修改下 import 导入函数部分。index.jsconst addFunc = import(/* webpackChunkName: "main" */ './main'); setTimeout(() => { addFunc.then(({add}) => { add(3, 8); }); }, 2000);通过在 import 函数中增加注释 /* webpackChunkName: "main" */ 其中的 main 即为生成的 chunk 名称。这样在项目中通过定义语义化的名称,可以增加代码的可读性。总结上面的示例中,我们总结了代码分离的几种方式,分别是入口分离、去重分离、动态导入,其中的动态导入就是我们常说的懒加载。代码分离对于减少主包体积,优化项目加载速度,减少白屏时间来说将非常有用。
一、配置参数详解output 输出配置详解output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。我们可以通过在配置中指定一个 output 对象,来配置这些处理过程:const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js' }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/assets/', library: 'DemoLibrary', // 导出名称 libraryTarget: 'window' // 挂载目标 } };output 对象中可以包含数十个配置项,其中大部分开发中使用频率不高,我们在本章内容中只介绍几个常用配置,对其他配置感兴趣的同学可以查看官网 output配置filenamefilename 决定了每个输出 bundle 的名称。这些 bundle 将写入到 output.path 选项指定的目录下。对于单个入口起点,filename 会是一个静态名称。 filename 支持以字符串和函数的形式定义参数。// 字符串形式 module.exports = { ... output: { filename: 'bundle.js', } }; // 函数形式 module.exports = { ... output: { filename: (pathData) => { console.log(pathData) return '[name].js'; } } };字符串形式的 filename,会在输出文件中生成 bundle.js,函数形式的 filename 会在输出文件中生成 index.js (以 chunk name 命名),在控制台输出下 pathData,我们可以看到返回了一个包含 chunk 内容等信息的对象。filename 可以不仅仅是 bundle 的名字,还可以使用像 'js/[name]/bundle.js' 这样的文件路径,即便路径中的目录不存在也没关系,webpack 会在输出资源时创建该目录。例子如下:module.exports = { ... output: { filename: 'js/[name]/bundle.js' } }; 当通过多个入口起点(entry point)、代码拆分(code splitting)或各种插件(plugin)创建多个 bundle,应该使用以下一种替换方式,来赋予每个 bundle 一个唯一的名称上面的配置除了可以对不同的 bundle 进行名称区分,还能起到一个控制客户端缓存的作用,表中的[chunkhash] 和 [contenthash] 都与文件内容直接相关,在 filename 中使用了这些变量后,当对文件内容做了修改,可以引起 bundle 文件名的修改,从而用户在下一次请求文件资源时会重新加载文件,而不会直接命中缓存资源。在实际的工程中,我们一般使用较多的是[name],一般与定义的 chunk 一一对应,可读性较高,为了控制客户端缓存,我们一般还加上 [contenthash],如:module.exports = { ... output: { filename: '[name]-[contenthash].js' } };打包结果如下pathpath 可以指定资源输出位置,要求必须使用绝对路径,如const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', }, output: { path: path.resolve(__dirname, 'dist') } };上述配置将工程的dist目录设置为资源的输出目录,在 webpack 4 之后,output.path 已经默认为 dist 目录,除非我们需要修改他,否则可以不用单独配置。publicPathpublicPath 从功能上来说,用于指定资源的请求位置。页面中的资源分为两种,一种是由 HTML 页面直接请求的,比如通过 script 标签加载 js,通过 link 标签加载 css。另一种是由 js 或 css 请求的,如加载图片字体文件等。 publicPath 就用来指定第二种间接资源的请求位置。如果指定了一个错误的值,则在加载这些资源时会收到 404 错误。publicPath 有以下三种形式相对于 HTML相对于 HOST相对于 CDN相对于 HTML在请求资源时,会以当前 html 页面所在路径加上 publicPath 的相对路径来构成实际请求的 URL,如// 假设当前 html 页面地址为 http://demo.com/webpack/index.html // 需要请求文件名为 demo.png module.exports = { ... output: { publicPath: '' // 实际请求路径 http://demo.com/webpack/demo.png publicPath: './css' // 实际请求路径 http://demo.com/webpack/css/demo.png publicPath: '../assets/' // 实际请求路径 http://demo.com/assets/demo.png } };相对于 HOST若 publicPath 的值以 “/” 开始,则代表此时 publicPath 是以当前页面的域名加上 publicPath 的相对路径来构成实际请求的 URL,如// 假设当前 html 页面地址为 http://demo.com/webpack/index.html // 需要请求文件名为 demo.png module.exports = { ... output: { publicPath: '/' // 实际请求路径 http://demo.com/demo.png publicPath: '/css' // 实际请求路径 http://demo.com/css/demo.png publicPath: '../assets/' // 实际请求路径 http://demo.com/assets/demo.png } }; 相对于 CDN上面两种配置都是相对路径,我们也可以使用绝对路径的形式配置 publicPath,这种情况一般发生在将静态资源放在 CDN 上面,如// 假设当前 html 页面地址为 http://demo.com/webpack/index.html // 需要请求文件名为 demo.png module.exports = { ... output: { publicPath: 'http://cdn.example.com/assets/' // 实际请求路径 http://cdn.example.com/assets/demo.png publicPath: 'https://cdn.example.com/assets/' // 实际请求路径 https://cdn.example.com/assets/demo.png publicPath: '//cdn.example.com/assets/' // 实际请求路径 //cdn.example.com/assets/demo.png } };webpack-dev-server 也会默认从 publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。librarylibrary 的作用是将打包的内容生成一个库,可以供其他工程加载使用。这一点在目前流行的微前端架构实战上面很有用,如子应用通过输出类库的形式将内容输出到一个对象上,这样主应用就可以通过加载 js 的方式去引入子应用,并且可以通过子应用输出的对象名称来加载子应用的内容。library 具体的使用方法,我们来看下面的例子:webpack.config.jsmodule.exports = { ... entry: './src/index.js', output: { library: 'DemoLibrary' } };src/index.js 的入口中导出了如下函数export function hello(webpack) { console.log(`hello ${webpack}`); }此时,变量 DemoLibrary 将与入口文件所导出的文件进行绑定,下面是如何使用打包生成的index.js文件:index.html<!DOCTYPE html> <html lang="en"> <head> <title>测试DemoLibrary库</title> </head> <body> <script src='./dist/index.js'></script> <script> DemoLibrary.hello('webpack'); </script> </body> </html>在浏览器中可以看到成功输出 hello webpack。library 的类型可以为字符串、数组、和对象,字符串的参数类型则直接指代库的名称,与对象中设置 name 属性作用相同。如果 entry 入口设置为 object,所有入口都可以通过 library 的 array 语法暴露:module.exports = { // … entry: { a: './src/a.js', b: './src/b.js', }, output: { filename: '[name].js', library: ['DemoLibrary', '[name]'], // [name] 为 chunk name }, };假设 a.js 与 b.js 导出名为 hello 的函数,下面就是如何使用这些库的方法:<!DOCTYPE html> <html lang="en"> <head> <title>测试DemoLibrary库</title> </head> <body> <script src='./dist/a.js'></script> <script src='./dist/b.js'></script> <script> DemoLibrary.a.hello('webpack'); DemoLibrary.b.hello('webpack'); </script> </body> </html>请注意,如果你打算在每个入口点配置 library 配置项的话,以上配置将不能按照预期执行。这里是如何 在每个入口点下 做的方法:module.exports = { // … entry: { main: { import: './src/index.js', library: { // `output.library` 下的所有配置项可以在这里使用 name: 'MyLibrary', type: 'umd', umdNamedDefine: true, }, }, another: { import: './src/another.js', library: { name: 'AnotherLibrary', type: 'commonjs2', }, }, }, };library 包含以下可配置参数这里我们说下 type 类型,在实际的使用中,我们可能根据工程运行环境的需要,而需要将类库暴露为不同的类型,如 支持 esModule、amd、cmd、umd 等,type 配置就可以帮我们完成不同输出方式。type 类型默认包括 'var'、'module'、'assign'、'assign-properties'、'this'、'window'、'self'、'global'、'commonjs'、'commonjs2'、'commonjs-module'、'commonjs-static'、'amd'、'amd-require'、'umd'、'umd2'、'jsonp' 以及 'system',除此之外也可以通过插件添加。官方文档对每种类型给了详细说明和事例,具体我们可查看官方文档,output.target.type 配置总结以上为我们在实际开发中使用的 output 配置,包含 path、filename、publicPath、library,日常使用中可能还会用到 libraryTarget ,不过 webpack 未来会放弃对 output.libraryTarget 的支持,所以可以使用 output.library.type 替代 output.libraryTarget。二、输出配置实例output 输出配置实例到目前为止,我们都是在 index.html 文件中手动引入打包生成的资源,然而随着应用程序增长,并且一旦开始在文件名中使用 hash 并输出 多个 bundle,如果继续手动管理 index.html 文件,就会变得困难起来。然而,通过一些插件可以使这个过程更容易管控。HtmlWebpackPlugin 可以帮我们解决这个问题。设置 HtmlWebpackPlugin继续使用之前的工程文件,目录结构为:首先安装插件,并且调整 webpack.config.js 文件:安装 html-webpack-plugin 插件npm install --save-dev html-webpack-pluginwebpack.config.jsconst path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'development', entry: { index: './src/index.js', hello: './src/hello.js' }, output: { filename: '[name].js' }, plugins: [ new HtmlWebpackPlugin({ title: '管理输出', }), ], };执行构建命令 npm run build,我们看下打包后的结果,我们可以看到,打包文件包含两个入口文件对应的 js 文件,还包含一个 index.html 文件在 dist 目录下我们看下打包的 index.html 文件,我们可以看到 HtmlWebpackPlugin 创建了一个全新的 index.html 文件,所有的 bundle 会自动添加到 html 中。清理 /dist 文件夹你可能已经注意到,由于遗留了之前指南中的代码示例,我们的 /dist 文件夹显得相当杂乱。webpack 将生成文件并放置在 /dist 文件夹中,但是它不会追踪哪些文件是实际在项目中用到的。通常比较推荐的做法是,在每次构建前清理 /dist 文件夹,这样 /dist 文件夹中只有最近一次生成的文件。让我们使用 output.clean( webpack 5.20.0 及以上版本支持)配置项实现这个需求。webpack.config.jsmodule.exports = { ... output: { clean: true } };现在,执行 npm run build,检查 /dist 文件夹。如果一切顺利,现在只会看到构建后生成的文件,而没有旧文件!总结本章我们介绍了一个优化开发效率的插件和一个配置项,使用 HtmlWebpackPlugin 插件可以动态的生成 index.html 文件,以及动态的向 index.html 文件插入 bundle。了解了如何在编译时清空 dist 文件内容。
一、资源处理流程资源处理流程借用 webpack 官网对 webpack 的描述: webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle什么是bundle代码分离是 webpack 的特性之一,使用 entry 配置入口起点,会将代码分成源代码和分发代码其中源代码是开发编辑的代码,分发代码是经过 webpack 构建,可能经过转义、压缩或优化后的代码,这些代码存放于 bundle 中,可以被浏览器等环境直接运行什么是dependency graph看了上面的内容,其实我们还是不清楚 webpack 到底做了哪些事情使浏览器不支持的语法变得可以执行,而去查看源码,会发现源码中代码对我们不是特别友好,经常左跳右跳无法持续跟踪,所以针对 webpack 打包流程,总结了下面的一个简版打包代码,让我们能大体看清 webpack 做了哪些操作,以下只是简版 demo,没有强行靠近 webpack 打包结果,让我们更能清晰的梳理流程。打包步骤总结我们工程的依赖路径 index.js -> hello.js -> message.js根据 webpack.config.js 中定义的 entry 解析入口(index.js)文件,找到他的依赖递归的构建依赖关系图将所有内容打包到 webpack.config.js 定义的 output 文件在整理打包内容之前,我们先来看下我们现在项目的结构,项目名称为 webpack_demo,其中包含 webpack.config.js,package.json,dist/main.js,src/index.js,src/hello.js,src/message.js,src/bundler.js,src/complier.js。webpack.config.js 定义对象,导出项目入口出口文件const path = require('path') module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist'), filename: 'main.js' } }src/index.js 代码中导入 hello.js方法并执行import hello from './hello.js' console.log(hello())src/hello.js 代码中导入 message.js中的message参数import { message } from './message.js' export default function() { return 'hello ' + message; }src/message.js 中定义了一个message变量export const message = 'world!!!'上面的代码层层引用下来,可以在控制台输出 ‘hello world!!!’,就代表打包成功了。上面的环境已经定义完成,接下来让我们按照步骤完成打包操作:1.在src/complier.js文件中创建complier构造函数,在构造函数中获取webpack.config.js中定义的入口出口参数module.exports = class Complier { constructor(options) { const { entry, output } = options this.entry = entry console.log(options) this.output = output } }2.在 src/bundler.js 文件中引入 webpack.config.js 文件,创建 Complier 的实例化对象并传入 optionsconst complier = require('./complier') const options = require('../webpack.config') new complier(options)在命令行执行 node src/bundler.js 后,在控制台会打印出 options 内容3.拿到配置参数后,开始根据入口文件解析文件内容,解析单流程整体为:根据入口文件名称通过 nodejs 提供的方法 fs.readFileSync 读取到文件内容使用 @babel/parser 将文件内容转换成ast语法树ast 语法树中 node 节点中包含了文件依赖的文件名称,使用 @babel/traverse 方法提取出依赖的文件名称并存储到一个数组中通过 @babel/core 中的 babel.transformFromAst 方法将 ast 转换成目标浏览器可执行的代码将上述获取的参数返回个对象,对象包含文件名,依赖数组,文件可执行代码,这个对象即为一个依赖图谱中的一个节点遍历入口文件的依赖数组,由于数组中是文件名,则递归执行上述方法,直到找到所有依赖返回所有依赖对象根据上面总结内容我们在 src/complier.js 中创建一个 createAsset 方法const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core') ... // 开始编译,构建ast语法树 filename: ./src/index.js createAsset(filename) { // 1 // content 内容即为index.js中书写的内容 const content = fs.readFileSync(filename, 'utf-8') // 2 // ast 内容为对象,具体内容可以console.log(ast)查看 // https://astexplorer.net/ 在官网输入index.js内容即可看到对应的树 const ast = parser.parse(content, { sourceType: 'module' }) // 创建依赖对象 const dependencies = {} // 3 // 获取抽象语法树中的依赖文件名 traverse(ast, { ImportDeclaration: ({node}) => { // 获取文件的路径名如 './src/index.js' dirname='./src' const dirname = path.dirname(filename) const absPath = path.join(dirname, node.source.value) // node.source.value: .hello.js // absPath: ./src/index.js dependencies[node.source.value] = absPath } }) // 4 // 将ast转换成可执行代码 // https://www.babeljs.cn/docs/babel-core 将index.js内容直接放在官网即可看到转译后代码 const { code } = babel.transformFromAst(ast, null, { presets: ['@babel/preset-env'] }) // 5 return { filename, dependencies, code } }入口文件的依赖关系已经定义好,接下来根据入口文件的 dependencies ,递归遍历出所有子依赖,在 src/complier.js 文件中定义 run 方法 // 拿到参数、执行、分析入口文件 run() { // 拿到入口文件的依赖 const mainAsset = this.createAsset(this.entry) const queue = [mainAsset] // 6 // 遍历对象 for (const asset of queue) { // 遍历文件的依赖文件,递归创建依赖图 Object.values(asset.dependencies).forEach(filename => { const child = this.createAsset(filename) queue.push(child) }) } // 7 return queue }命令行执行 node src/bundler.js 看下 queue 的内容如下4.依赖树已经拿到,接下来在 src/bundler.js 中获取 complier 中返回的 queue// 获取dependence graph const graph = new complier(options).run()5.在 src/bundler.js 中创建函数 bundle,解析 graph 树,定义 require 函数,定义 modules,通过 eval 函数执行依赖树中的 code,在此我们可以知道 webpack 重写了 require 函数,所以 babel 中转换的函数可以正常执行function bundle(graph){ // 得到依赖文件名的对象 let modules = {}; graph.forEach(item => { // 将文件名作为key, value为依赖文件,code为文件名对应的函数 modules[item.filename] = { dependencies: item.dependencies, code: item.code } }) modules = JSON.stringify(modules) const result = `(function(graph){ function require(filepath) { function localRequire(relativePath) { // 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径 return require(graph[filepath].dependencies[relativePath]) } var exports = {} function fn(require, exports, code) { eval(code) } fn(localRequire, exports, graph[filepath].code) return exports } require('${entry}') })(${modules})` return result } const graph = new complier(options).run() // 执行bundle函数 const result = bundle(graph)命令行输出 result 内容,粘贴内容到浏览器控制台并回车执行,发现我们预期的 'hello world!!!' 已经可以正常打印6.以上我们已经拿到了编译后的代码,最后将它输出到 dist/main.js 中,在 src/bundler.js 中创建方法 createFile(),使用 fs 对象的 writeFileSync 将内容输出,在命令行执行命令后可以看到 src/main.js 中输出了对应内容function createFile(code) { fs.writeFileSync(path.join(output.path, output.filename), code) }7.下面是 src/complier.js和 src/bundler.js文件全部内容complier.js// 文件操作模块,读取文件内容 const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core') module.exports = class Complier { constructor(options) { const { entry, output } = options this.entry = entry this.output = output } // 拿到参数、执行、分析入口文件 run() { const mainAsset = this.createAsset(this.entry) const queue = [mainAsset] for (const asset of queue) { Object.values(asset.dependencies).forEach(filename => { const child = this.createAsset(filename) queue.push(child) }) } console.log(queue) return queue } // 开始编译,构建ast语法树 filename: ./src/index.js createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8') const ast = parser.parse(content, { sourceType: 'module' }) // 创建依赖 const dependencies = {} traverse(ast, { ImportDeclaration: ({node}) => { // 获取文件的路径名如 './src/index.js' dirname='./src' const dirname = path.dirname(filename) const absPath = path.join(dirname, node.source.value) dependencies[node.source.value] = absPath } }) // 将ast转换成代码 // https://www.babeljs.cn/docs/babel-core const { code } = babel.transformFromAst(ast, null, { presets: ['@babel/preset-env'] }) return { filename, dependencies, code } } }bundler.js// 引入配置 const fs = require('fs'); const path = require('path') const options = require('../webpack.config') const complier = require('./complier') const { entry, output } = options function bundle(graph){ // 得到以依赖文件名的对象 let modules = {}; graph.forEach(item => { modules[item.filename] = { dependencies: item.dependencies, code: item.code } }) modules = JSON.stringify(modules) const result = `(function(graph){ function require(filepath) { function localRequire(relativePath) { // 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径 return require(graph[filepath].dependencies[relativePath]) } var exports = {} function fn(require, exports, code) { eval(code) } fn(localRequire, exports, graph[filepath].code) return exports } require('${entry}') })(${modules})` return result } function createFile(code) { fs.writeFileSync(path.join(output.path, output.filename), code) } const graph = new complier(options).run() const result = bundle(graph) createFile(result) 总结:通过上面的 demo,我们已经可以大概了解 webpack 的编译流程,当然 webpack 的源码功能强大且复杂,感兴趣的小伙伴儿可以自行研究。