做前端这些年我越来越觉得webpack像一门很熟又不熟的手艺。天天在用打包、热更新、上线都靠它可一旦遇到性能问题或者特殊需求很多人第一反应是去网上复制一段配置而不是自己动手查清楚每一行在干什么。这个系列做到第13期我把主题定为013-webpack新东方——不是那个培训机构而是我给自己定的一条规矩新是重新认识东方代表不盲从别人给的配方把webpack配置和打包优化这件事的主动权拿回自己手里。这篇文章不打算讲高深原理也不搞逐行源码分析。我想做的是把从一个真实项目出发从零手动搭建一套webpack配置、做体积分析和构建提速、处理开发环境和生产环境的差异以及最后折腾出的那一堆坑完整地分享出来。里面所有配置都是实测跑过、验证过的按照这篇的思路走你至少能有一套属于自己的、可解释每个选项来由的webpack配置。如果你现在处于用脚手架一把梭、配置全靠复制的阶段这篇文章应该能帮你过渡到我清楚自己的构建系统每一环在干嘛的状态。1. 为什么放着脚手架不用非要自己手动写配置1.1 脚手架的黑盒省事的代价先说说大多数人日常的状态。创建一个新项目一把create-react-app或者vue-cli敲下去没几分钟一个能跑的前端项目就起来了。webpack在里面扮演的角色被完全封装起来隐藏在那几千行复杂配置里。你根本不需要知道entry在哪、output怎么生成、loader为什么长那个样子开发服务器就自动跑起来了。这种省事当然好但它有一个隐性代价当默认配置不够用的时候你不知道去哪找需要改的那一行。我见过很多做了一两年前端的朋友项目要求部署到子路径下出现所有静态资源404的问题第一反应是去改publicPath可改完路径又不对最后只能在publicPath、base标签、nginx的location之间反复试错。这种试出来能跑和想清楚为什么这样写之间隔着一条不小的认知鸿沟。老实说脚手架不存在好不好之说它只是把决策封装起来了。如果你只做简单的单页应用CRA那套默认配置是够用的。但一旦遇到多入口、CDN路径、细粒度分包、构建时间过长这些问题配置的主动权就很重要了。如果再拿不到主动权就只能做一件很尴尬的事eject。1.2 从eject到失控我的一次真实翻车经历eject就是把脚手架封装的webpack配置全部弹出来变成项目里可改的源码。我还在用CRA的时候有个项目需要做多页面、还要针对不同的后端环境切换API域名复杂一点就超出默认配置能力了。于是我很自然地在命令行敲了yarn eject。确实配置被弹出来了几百行我也能改任何地方了。但问题在于那是webpack团队生成的庞大配置各种env判断、几十个loader组合串在一起根目录一个几十行的webpack配置文件加上内部的eval加载流程踩错一个地方就得在配置迷宫里绕很久。那个项目后面每次构建出问题查错成本都特别高。那次经历之后我彻底想明白一件事自己写的配置哪怕丑一点也比看不懂的复杂配置强一万倍。因为你清楚每一行的来由出问题时有明确的排查方向。这也是我写下013-webpack新东方的初衷——把配置从外包团队手里接回来变成自己能掌控的东西。1.3 新东方的态度配置的主动权要自己拿着新东方里面的东方在我这里不是一个地理概念而是一种态度不拿着大洋彼岸的工具模板当圣旨而是自己去验证、去推演建立自己的配置方法论。很多人在学习webpack时有个误区一上来就背配置项entry、output、module.rules、plugins死记硬背结果还是不会用。我的经验是反过来——先明确你想解决什么再有针对性地去查某个配置项该怎么写。配置是写给构建流程看的说明书不是为了炫技也不是为了好看。了解每一种loader为什么存在、每个优化项解决什么问题比记住几百个配置项更有价值。关于这个自己掌控配置的态度后面所有章节都是从实际操作展开的先搭一个能跑的骨架再做优化再处理环境差异最后聊聊我踩过的坑。你会发现一旦主动权在自己手里webpack从一个黑盒工具变成了一件随手可调的家伙事。2. 从零搭配置骨架入口到产物的完整链路在开始之前先交代一下选型。下面的示例我都用webpack 5 webpack-cli 4来做。2.1 版本选型为什么直接上webpack 5webpack 4曾经很流行社区里有大量基于4的配置文章。但如果你现在新起项目我的建议是直接用5。原因很直接webpack 5内置了cache持久化缓存第二次构建速度提升非常明显这在webpack 4里只能靠第三方插件实现。内置了资源模块asset、asset/resource不再需要单独装file-loader和url-loader。模块联邦Module Federation是5才有的能力微前端场景下极其实用。webpack 5在Tree Shaking和副作用处理上做得更彻底配合sideEffects: false效果更好。版本上不要用5刚发布的早期小版本我当时用的webpack5.88.2已经比较稳定。webpack-cli是命令行工具用来执行webpack命令装一个最新版就行。这两个装好了构建的基本能力就有了。2.2 entry与output多入口项目的基本盘拿一个典型的多页面项目举例src目录下有login和dashboard两个页面各自有独立的JS入口还共享一些公共代码。我的入口配置是module.exports { entry: { login: ./src/login/index.js, dashboard: ./src/dashboard/index.js }, output: { filename: [name].[contenthash:8].js, path: path.resolve(__dirname, dist), clean: true } };entry里的键名就是output.filename里的[name]这个映射关系我是用了很久才自然记住的。多入口的意义在于不同页面的代码可以独立加载和缓存避免用户打开登录页还要下载整包后台代码。output里有三个关键点[contenthash:8]文件指纹根据文件内容生成哈希内容没变哈希就不变这样浏览器能继续用缓存。path产物目录必须用绝对路径。clean: true每次构建前清空dist目录这个配置是webpack 5新增的以前要靠CleanWebpackPlugin。在这里踩过一个很基础的坑path一开始我写过path.join(__dirname, ../dist)后来把项目目录结构调整过配置没改构建产物就跑到预想外的地方去了。从此之后我养成了先确认__dirname的定位习惯在配置里用console.log输出当前路径确认再往下写。2.3 loader的串联逻辑让不同类型文件各就各位webpack默认只认识JS其他类型的文件都需要loader来翻译。关键是理解loader的执行顺序同一个rule里的loader从右往左、从下往上执行像流水线一样先处理格式转换再做开发适配。我常用的loader组合是这样的module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: { loader: babel-loader, options: { presets: [babel/preset-env, babel/preset-react] } } }, { test: /\.css$/, use: [style-loader, css-loader] }, { test: /\.(png|jpg|jpeg|gif|svg)$/i, type: asset, parser: { dataUrlCondition: { maxSize: 8 * 1024 } } } ] }解释一下每个部分的逻辑babel-loader是用来做JS语法转换的。比如你用ES6语法要兼容旧浏览器就需要把代码转成ES5。exclude: /node_modules/是性能关键第三方包基本都已经转译过没必要再走一遍babel否则构建时间会成倍增加。css-loader负责解析CSS里的import和url()如果你写background: url(./img.png)css-loader会把相对路径解析到打包体系里。style-loader则负责把解析好的CSS通过style标签注入页面。顺序必须是[style-loader, css-loader]因为先由css-loader处理CSS语法再由style-loader把结果插入DOM。图片资源就交给type: asset这是webpack 5的资源模块。maxSize: 8 * 1024的含义是小于8KB的图片自动转成data URI减少HTTP请求大于8KB的则输出为独立的文件。8KB这个值可以根据项目情况调整但阈值太低会请求爆炸太高会导致HTML体积膨胀。这里有个我后来才想明白的细节loader看似是一个文件交给一个loader实际是每个文件都要经过这条规则链里的所有loader只是每个loader只处理自己能识别的内容。把loader想成流水线上的工人就很好理解为什么顺序那么重要了。CSS如果先过style-loader再过css-loaderCSS语法还没解析就被插入页面结果必然是报错或者样式不生效。2.4 plugin与mode补齐Webpack不做的事loader解决的是文件类型识别plugin解决的是构建流程中那些额外要做的事。最常见的需求是自动生成HTML、拷贝静态资源、压缩产物。基础配置里我至少会放这两个pluginconst HtmlWebpackPlugin require(html-webpack-plugin); module.exports { mode: development, plugins: [ new HtmlWebpackPlugin({ template: ./public/index.html, filename: index.html }) ] };HtmlWebpackPlugin的作用是自动生成一个HTML文件并把构建出来的JS和CSS以script和link标签的形式注入进去。没有它你得手动在HTML里写死dist/js/login.xxxx.js一旦哈希变了就得手工改那日子就没法过了。再聊聊mode。mode有三个取值development、production、none。它不只是个开关会直接影响webpack的默认行为development开启process.env.NODE_ENV为development不压缩代码带完整的错误提示。production自动启用TerserPlugin做代码压缩并开启一系列生产环境优化。none不预设任何优化像一张白纸。我踩过的教训是用development模式构建产物交到测试环境结果线上报错一大堆未捕获的语法错误排查半天发现压缩根本没开。后来我养成了规矩发布前必须用production模式过一遍构建本地看产物大小和报错确认无异常再发。3. 打包优化不是玄学从体积分析到构建提速配置骨架跑通以后接下来进入这个系列的重头戏优化。很多人一提到webpack优化就想到那几板斧但每板斧应该什么时候用、参数怎么调、为什么有效就说不清楚了。这里我按先诊断、再拆包、后提速的顺序来。3.1 先看体检报告bundle分析器的使用与解读优化不应该是瞎猜。我每次都先看一份体检报告也就是webpack-bundle-analyzer生成的产物依赖分析图。它会以交互式树状图展示每个chunk里包含哪些模块、各自占多大体积、模块之间的依赖关系长什么样。安装和启动方式npm install -D webpack-bundle-analyzer在webpack配置里加上const { BundleAnalyzerPlugin } require(webpack-bundle-analyzer); module.exports { plugins: [ process.env.ANALYZE true new BundleAnalyzerPlugin() ].filter(Boolean) };通过环境变量控制是否启用这样日常构建不会每次弹浏览器只在需要的时候带上--env ANALYZEtrue。看分析报告时我主要关注三件事有没有一个特别巨大的模块被多次引用比如moment.js体积本身就不小如果它还带了完整的语言包那就得考虑按需引入或者换dayjs。公共依赖是否被正确抽离如果多个入口重复打了同样的依赖说明splitChunks还没生效。是否存在循环引用导致的模块重复这个问题比较隐蔽但分析图上会清晰显示两个chunk互相引用。有一次项目首屏体积极大一查发现图表库echarts被完整塞进了主入口而我只在一个弹窗里用到折线图。后来改成按需引入之后主包体积直接掉了大半。这就是报告的价值——它用直观的方式告诉你体积都花在哪了。3.2 代码分割splitChunks的取舍逻辑代码分割是打包优化里最值得花时间琢磨的部分。webpack 5的splitChunks取代了旧版的CommonsChunkPlugin核心逻辑是把共享的、体积较大的模块从业务代码里抽出来单独成一个chunk利用浏览器缓存避免重复下载。下面是我在项目里用的一个相对理性的配置optimization: { splitChunks: { chunks: all, minSize: 20000, maxInitialRequests: 30, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: vendors, priority: 10, chunks: all }, commons: { name: commons, minChunks: 2, priority: 5, reuseExistingChunk: true } } } }这个配置有几个容易理解偏的点chunks: all表示对同步和异步加载的模块都做分割。如果把异步模块排除在外动态import的代码就永远不会被单独抽取。minSize: 20000的意思是只有超过20KB的模块才考虑拆分。太小就拆会搞出很多碎文件HTTP请求变多反而得不偿失。cacheGroups里priority越大优先级越高。vendor专门处理node_modules里的第三方依赖commons处理在多个入口之间被引用两次以上的业务模块。我把name写成固定字符而不是[name]的原因是给chunk固定的名字能保证长期构建哈希稳定不然每次依赖位置变化chunk文件名就跟着变缓存命中率会受影响。有一件很多人忽略的事splitChunks不是配置完就万事大吉分割粒度要配合业务模块体积来看。如果你的项目只有几十KB硬拆出七八个chunk效果适得其反。所以做代码分割前先回答一个问题拆出来的包是否真的会被独立加载、独立缓存3.3 Tree Shaking的生效条件与副作用声明Tree Shaking是我以前最容易以为会了、实际没生效的一项。它听起来玄乎核心原理就一句话通过ES Module的静态语法分析把import了但没被用到的代码在打包时删除掉。它要真正生效至少满足三个条件{ name: my-app, sideEffects: false }第一必须是ES Module语法。require和module.exports是CommonJS动态导入无法静态分析Tree Shaking就不生效。所以你在业务代码里应该优先用import/export。第二在package.json里声明sideEffects: false告诉webpack这个包导出时没有副作用可以安全地删除未使用的导出。如果你的代码里有调用全局变量、修改外部状态的逻辑那就不能盲目设false更稳妥的做法是写成sideEffects: [ ./src/styles/**/*.css, *.css ]把CSS保留下来因为CSS被引入时有真正的副作用——将样式注入页面。第三webpack的生产模式下压缩器TerserPlugin需要参与配合因为Tree Shaking只负责标记未使用的代码可删除真正删除动作由压缩器完成。我在实现国际化功能时遇到过这类情况某个工具库里导出了十几个函数我只用到其中一个可打包后体积纹丝不动。排查半天发现那个包的主入口是CommonJS版的index.jsTree Shaking完全插不上手。换了一个有ES Module入口的版本后体积立刻降下来。选择依赖时留意包是否提供module字段指向ESM版本对打包体积的影响比想象中更大。3.4 构建提速三板斧缓存、并行与持久化优化不光要看产物体积构建时间同样是体验的一部分。在项目变大之后改一行代码要等十几秒这种体验很磨人。我的提速组合是这三件事第一开启持久化缓存。webpack 5的cache可以直接写到磁盘构建完成的数据二次构建直接复用module.exports { cache: { type: filesystem, buildDependencies: { config: [__filename] } } };这个配置的效果非常直观。我实测过一个中等体量的项目冷构建20多秒加了缓存之后热构建直接降到3秒左右。buildDependencies的作用是当配置文件本身变化时自动失效缓存防止缓存错乱。第二用thread-loader做多进程打包。它的原理是把耗时的loader主要是babel-loader放到worker池里并行运行{ test: /\.jsx?$/, exclude: /node_modules/, use: [ { loader: thread-loader, options: { workers: 2 } }, { loader: babel-loader, options: { cacheDirectory: true } } ] }这里有个必须注意的事thread-loader必须放在use数组的第一个因为loader是从右往左执行的它要最先起线程去接后面的任务。另外thread-loader只对耗时操作有意义。项目很小或者loader本身很快时起多线程反而有线程通信的开销实测下来可能更慢。要不要用看项目体量判断。第三配置好babel-loader自带的cacheDirectory: true给babel加一层文件缓存。配合前面的thread-loader整体构建速度能再上一个台阶。构建提速还有一个思路容易被忽略减少loader的处理范围。比如exclude: /node_modules/就是典型的不做无用功确保babel-loader不会去处理已经编译过的第三方包。看似简单实际对构建速度影响非常大。4. 开发与生产环境一份配置如何优雅分身真实项目里最烦的是开发环境和生产环境对同一份配置的要求不一样——开发要快、要详细报错、要有热更新生产要压缩、要指纹、要体积最小。我从一开始就把配置拆成了三份公共配置、开发配置、生产配置。这里说的都是我在拆完之后验证过的细节。4.1 devServer本地开发的那些开关开发环境的核心设施是devServer它提供本地服务器、热更新和代理能力。我用的是webpack-dev-server配置示例module.exports { devServer: { static: { directory: path.join(__dirname, dist) }, port: 3000, hot: true, historyApiFallback: true, proxy: { /api: { target: http://localhost:8080, pathRewrite: { ^/api: } } } } };几个关键选项的实际作用hot: true开启模块热替换HMR改动一个模块只重新加载那个模块不再整页刷新开发体验差别非常大。historyApiFallback: true当路由是history模式时所有不存在的URL都回退到index.html保证前端路由刷新不404。proxy开发时解决跨域问题。前端请求/api/logindevServer会把请求转发到http://localhost:8080/login。注意pathRewrite的作用是转换URL路径我遇到过明明配了代理却始终404的情况就是忘了加pathRewrite。一个真实的坑hot和liveReload是两回事。hot: true是模块热替换liveReload是整页刷新。如果两者同时开启会产生奇怪的冲突有时候代码改了页面不刷新有时候刷新了状态丢失。我的做法是开发环境只开hot关闭liveReload。4.2 source map策略要调试体验还是要线上安全source map是定位线上问题的利器但它也是双刃剑——把源码原样暴露给所有能打开DevTools的人。所以开发和生产的策略我完全是两种。开发环境我用devtool: eval-cheap-module-source-map这个选择是在调试体验和编译速度之间做的平衡。eval速度最快cheap只定位到行不定位到列module则保留原始TS/JS源码的映射关系断点时能正确显示源码而不是编译后的代码。生产环境我用devtool: hidden-source-maphidden-source-map会把source map文件生成出来但不在产物JS末尾加注释引用。这样外人拿到部署的JS文件看不到源码但当线上报错时你能拿着这份map文件在本地还原出错位置。如果确定不需要在线还原或者团队有前端监控系统生产环境可以直接设devtool: false完全移除source map体积最小。选择source map生成策略时我建议用一个表来对照选择场景devtool理由本地开发调试eval-cheap-module-source-map编译快断点能看原始源码生产环境且需要排查hidden-source-map源码不暴露但有报错还原能力生产环境且监控完备false体积最小构建最快4.3 环境变量注入一套代码对接多套环境前端的代码要对接开发、测试、预发、生产多套环境每套环境的API地址不一样。最原始的做法是打包前手动改代码这属于灾难级别的操作。合理做法是把环境变量编译时注入。我用DefinePlugin比较多它是webpack自带的用法是在配置里定义运行时可访问的全局常量const webpack require(webpack); module.exports (env) { const apiBaseUrl env.production ? https://api.example.com : https://staging.example.com; return { plugins: [ new webpack.DefinePlugin({ process.env.API_BASE_URL: JSON.stringify(apiBaseUrl) }) ] }; };要注意JSON.stringify不能省。DefinePlugin的替换是文本级别的如果直接写字符串不带引号process.env.API_BASE_URL会变成一个变量引用运行时会直接报变量未定义。实际项目里也可以结合.env文件或者用cross-env设环境变量再把process.env按需暴露给前端。核心思路是一样的构建时决定环境运行时只读常量。绝不要让前端代码在运行时动态探测环境那样不仅慢还可能暴露内部配置。4.4 产物hash策略contenthash与缓存命中率hash策略直接影响浏览器缓存的表现。webpack提供三种hashhash整次构建生成一个hash任何文件改动所有产物全变。缓存命中率最低。chunkhash每个chunk一个hash基于chunk内容生成。contenthash基于单个文件的内容生成粒度最细缓存命中率最高。我的产出文件名一直都是output: { filename: [name].[contenthash:8].js }这个contenthash:8是取哈希的前8位够唯一且不会太长。这里有个过去踩坑的经验如果只对业务文件加contenthash不对第三方库做处理那么业务代码每次改动vendor里的库可能也跟着变。原因在于splitChunks自动拆出来的vendor chunk里包含了webpack的runtime代码而runtime是和某些运行时信息绑定的内容变了hash就变。解法是在optimization里单独拆一个runtime chunkoptimization: { runtimeChunk: single }这样webpack的runtime代码独立成一个chunkvendor的hash就只取决于vendor内容本身。这个配置后文还会展开讲因为它直接关系到为什么每次发布vendor都变了这个经典问题。5. 那些年我在webpack上踩过的坑前面的章节侧重怎么做这一章聊聊做错之后怎么排查。我挑四个印象最深、也是社区里高频出现的问题按真实的排查链路还原当时的情况。5.1 publicPath配错之后资源全部404这是新手期踩的第一个大坑现象是构建成功但页面打开一堆404尤其CSS和JS资源。当时项目要部署到服务器的一个子路径下而不是根路径我没处理publicPath资源请求地址自然全部错了。publicPath决定的是运行时浏览器以什么路径去加载这些静态资源。它和output.path是两个维度output.path决定资源在磁盘的物理位置publicPath决定浏览器请求资源时的URL前缀。最常见的组合是output: { path: path.resolve(__dirname, dist), publicPath: /static/ }意思是构建产物物理放到dist目录然后在浏览器里通过example.com/static/xxx.js加载。如果部署在子路径下就要改成publicPath: /your-sub-path/还有一种省心办法用相对路径。设publicPath: ./会让浏览器根据当前页面路径去拼资源地址。但这在路由多级跳转时容易出问题不是最优解。真正稳妥的做还是明确完整的部署路径前后端配合确认一次。排查这种问题时最快的办法是打开浏览器DevTools的Network面板看失败资源的具体URL和当前页面URL自己脑补一遍浏览器拿到这个URL后会去请求什么很快就能定位是publicPath还是nginx配置的问题。5.2 CSS顺序随机变化loader顺序与抽取时机这个问题的现象很诡异样式表里的某些规则有时候生效有时候不生效刷新几次表现还不一样。一开始我以为是CSS优先级问题排查了很久才发现是打包后CSS的加载顺序变了。根源在两个方面。一个是loader顺序。CSS文件经过css-loader解析后style-loader会按JS模块的依赖顺序插入style标签。如果两个入口JS的CSS依赖顺序不稳定页面注入的CSS顺序也会跟着不稳定。另一个是抽取时机。用MiniCssExtractPlugin把CSS从JS里抽成独立文件时插件会按chunk的依赖关系合并CSS文件如果公共部分和页面部分的拆分不合理加载顺序看起来就很奇怪。我最终的解决路径分三步统一用MiniCssExtractPlugin并确认所有CSS都走同一个loader链避免有的走style-loader有的走抽取插件。检查splitChunks的cacheGroups确保公共CSS不会同时被多个chunk重复引用并产生多种顺序。在入口JS里按依赖关系明确import顺序让CSS的依赖关系可控。CSS顺序问题是最难排查的类型之一因为报错不会太明显表现出来只是样式时灵时不灵。现在想想所有loader配置都应该在项目开始时就定好避免混用两种CSS注入策略。5.3 每次发布vendor都变runtimeChunk的重要性这个问题的典型现象是业务代码只改了一行字发布后vendors.js的哈希也变了。如果vendors是长缓存核心这个现象意味着CDN缓存的命中率一直在被浪费。我前面提过runtimeChunk: single就是专门解决这个的。深入解释一下为什么webpack打包后模块的加载逻辑由一段runtime代码维护。默认情况这段runtime会被打进最后生成的chunk里比如vendors而runtime里包含了模块ID、chunkID这些容易变的信息。你改了业务代码模块的排列可能变化runtime跟着变vendor的内容也跟着变hash就没法稳定了。配置加上后面optimization: { runtimeChunk: single }webpack会把runtime单独抽出来比如生成一个runtime.jsvendor的内容不再包含runtime于是vendor的哈希只由node_modules里那几个库决定。这个方案对长期迭代的项目价值非常大我是在经历了几次一改代码、CDN缓存全部失效的教训后才把它变成标准配置的。5.4 动态import报错变量路径的webpack约束这个坑是在做按需加载组件时踩的。我原本想根据用户权限动态加载不同模块代码写成了这样const importComponent (name) import(./components/${name})结果运行到该加载的时候报错提示找不到模块而且构建时能把components下所有文件都打进一个巨大的上下文chunk里。原因很明确webpack的静态分析要求动态import的路径必须是可解析的。它会在编译时扫描可能的模块并生成一个映射表。如果模板字符串里的变量是全动态的webpack没法确定它到底引用哪些文件只能把整个匹配目录都打包进来。解决方法是缩小变量范围把动态部分限制到具体文件级别的后缀const importComponent (name) import(./components/${name}.js)这样webpack能限定扫描目录是./components/只会打包这个目录下以.js结尾的文件既保证了可解析性也不会把整个目录无脑纳入。对于更复杂的映射直接维护一张模块路径表会更清晰和安全。这类按需加载的坑在业务量上来后非常常见记住核心原则webpack需要看到足够的静态信息来确定你import的是哪些模块。最后分享一点个人体会webpack这套东西归根结底不是靠背书学会的而是靠一次次为什么这样写和为什么报了那个错折腾出来的。我自己从脚手架的一键构建走到手动配置并且能解释清楚每一项的来由花了挺长一段时间中间踩过的坑、看过的报错信息现在回想起来都变成了经验沉淀。如果你也打算从零配置一套webpack我的建议是先把自己项目的真实需求列出来——多入口还是单入口需要兼容哪些浏览器部署在什么路径有没有多环境然后照着这份清单去写配置写完一项就打一个勾。遇到报错先读全错误信息再动手别急着复制线上的配置。配置这种东西最大的技巧就是把它变成自己的而不是别人给的。