webpack.common.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import FriendlyErrorsWebpackPlugin from '@soda/friendly-errors-webpack-plugin';
  2. import BilldHtmlWebpackPlugin, { logData } from 'billd-html-webpack-plugin';
  3. import CopyWebpackPlugin from 'copy-webpack-plugin';
  4. import ESLintPlugin from 'eslint-webpack-plugin';
  5. import HtmlWebpackPlugin from 'html-webpack-plugin';
  6. import MiniCssExtractPlugin from 'mini-css-extract-plugin';
  7. import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
  8. import ComponentsPlugin from 'unplugin-vue-components/webpack';
  9. import { VueLoaderPlugin } from 'vue-loader';
  10. import { Configuration, DefinePlugin } from 'webpack';
  11. import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
  12. import { merge } from 'webpack-merge';
  13. import WindiCSSWebpackPlugin from 'windicss-webpack-plugin';
  14. import {
  15. analyzerEnable,
  16. eslintEnable,
  17. htmlWebpackPluginTitle,
  18. outputDir,
  19. outputStaticUrl,
  20. windicssEnable,
  21. } from '../constant';
  22. import { chalkINFO, chalkWARN } from '../utils/chalkTip';
  23. import { resolveApp } from '../utils/path';
  24. import devConfig from './webpack.dev';
  25. import prodConfig from './webpack.prod';
  26. console.log(chalkINFO(`读取: ${__filename.slice(__dirname.length + 1)}`));
  27. const sassRules = (isProduction: boolean, module?: boolean) => {
  28. return [
  29. isProduction
  30. ? {
  31. loader: MiniCssExtractPlugin.loader,
  32. options: {
  33. publicPath: outputStaticUrl(isProduction),
  34. },
  35. }
  36. : {
  37. loader: 'vue-style-loader',
  38. options: {
  39. sourceMap: false,
  40. },
  41. },
  42. {
  43. loader: 'css-loader', // 默认会自动找postcss.config.js
  44. options: {
  45. importLoaders: 2, // https://www.npmjs.com/package/css-loader#importloaders
  46. sourceMap: false,
  47. modules: module
  48. ? {
  49. localIdentName: '[name]_[local]_[hash:base64:5]',
  50. }
  51. : undefined,
  52. },
  53. },
  54. {
  55. loader: 'postcss-loader', // 默认会自动找postcss.config.js
  56. options: {
  57. sourceMap: false,
  58. },
  59. },
  60. {
  61. loader: 'sass-loader',
  62. options: {
  63. sourceMap: false,
  64. // 根据sass-loader9.x以后使用additionalData,9.x以前使用prependData
  65. additionalData: `@use 'billd-scss/src/index.scss' as *;@import '@/assets/constant.scss';`,
  66. },
  67. },
  68. ].filter(Boolean);
  69. };
  70. const cssRules = (isProduction: boolean, module?: boolean) => {
  71. return [
  72. isProduction
  73. ? {
  74. loader: MiniCssExtractPlugin.loader,
  75. options: {
  76. publicPath: outputStaticUrl(isProduction),
  77. },
  78. }
  79. : {
  80. loader: 'vue-style-loader',
  81. options: {
  82. sourceMap: false,
  83. },
  84. },
  85. {
  86. loader: 'css-loader', // 默认会自动找postcss.config.js
  87. options: {
  88. importLoaders: 1, // https://www.npmjs.com/package/css-loader#importloaders
  89. sourceMap: false,
  90. modules: module
  91. ? {
  92. localIdentName: '[name]_[local]_[hash:base64:5]',
  93. }
  94. : undefined,
  95. },
  96. },
  97. {
  98. loader: 'postcss-loader', // 默认会自动找postcss.config.js
  99. options: {
  100. sourceMap: false,
  101. },
  102. },
  103. ].filter(Boolean);
  104. };
  105. const commonConfig = (isProduction) => {
  106. const result: Configuration = {
  107. entry: {
  108. main: {
  109. import: './src/main.ts',
  110. },
  111. },
  112. output: {
  113. clean: true, // 在生成文件之前清空 output 目录。替代clean-webpack-plugin
  114. filename: 'js/[name]-[contenthash:6]-bundle.js', // 入口文件打包生成后的文件的文件名
  115. /**
  116. * 入口文件中,符合条件的代码,被抽离出来后生成的文件的文件名
  117. * 如:动态(即异步)导入,默认不管大小,是一定会被单独抽离出来的。
  118. * 如果一个模块既被同步引了,又被异步引入了,不管顺序(即不管是先同步引入再异步引入,还是先异步引入在同步引入),
  119. * 这个模块会打包进bundle.js,而不会单独抽离出来。
  120. */
  121. chunkFilename: 'js/[name]-[contenthash:6]-bundle-chunk.js',
  122. path: resolveApp(`./${outputDir}`),
  123. assetModuleFilename: 'assets/[name]-[contenthash:6].[ext]', // 静态资源生成目录(不管什么资源默认都统一生成到这里,除非单独设置了generator)
  124. /**
  125. * webpack-dev-server 也会默认从 publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。
  126. * 所以不管开发模式还是生产模式,output.publicPath都会生效,
  127. * output的publicPath建议(或者绝大部分情况下必须)与devServer的publicPath一致。
  128. * 如果不设置publicPath,它默认就约等于output.publicPath:"",到时候不管开发还是生产模式,最终引入到
  129. * index.html的所有资源都会拼上这个路径,如果不设置output.publicPath,会有问题:
  130. * 比如vue的history模式下,如果不设置output.publicPath,如果路由全都是/foo,/bar,/baz这样的一级路由没有问题,
  131. * 因为引入的资源都是js/bundle.js,css/bundle.css等等,浏览器输入:http://localhost:8080/foo,回车访问,
  132. * 引入的资源就是http://localhost:8080/js/bundle.js,http://localhost:8080/css/bundle.css,这些资源都
  133. * 是在http://localhost:8080/根目录下的没问题,但是如果有这些路由:/logManage/logList,/logManage/logList/editLog,
  134. * 等等超过一级的路由,就会有问题,因为没有设置output.publicPath,所以它默认就是"",此时浏览器输入:
  135. * http://localhost:8080/logManage/logList回车访问,引入的资源就是http://localhost:8080/logManage/logList/js/bundle.js,
  136. * 而很明显,我们的http://localhost:8080/logManage/logList/js目录下没有bundle.js这个资源(至少默认情况下是没有,除非设置了其他属性)
  137. * 找不到这个资源就会报错,这种情况的路由是很常见的,所以建议默认必须手动设置output.publicPath:"/",这样的话,
  138. * 访问http://localhost:8080/logManage/logList,引入的资源就是:http://localhost:8080/js/bundle.js,就不会报错。
  139. * 此外,output.publicPath还可设置cdn地址。
  140. */
  141. publicPath: outputStaticUrl(isProduction),
  142. },
  143. resolve: {
  144. // 解析路径
  145. extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.mjs'], // 解析扩展名,加上.mjs是因为vant,https://github.com/youzan/vant/issues/10738
  146. alias: {
  147. '@': resolveApp('./src'), // 设置路径别名
  148. script: resolveApp('./script'), // 设置路径别名
  149. vue$: 'vue/dist/vue.runtime.esm-bundler.js', // 设置vue的路径别名
  150. },
  151. fallback: {
  152. /**
  153. * webpack5移除了nodejs的polyfill,更专注于web了?
  154. * 其实webpack5之前的版本能用nodejs的polyfill,也是
  155. * 和nodejs正统的api不一样,比如path模块,nodejs的path,
  156. * __dirname是读取到的系统级的文件绝对路径的(即/user/xxx)
  157. * 但在webpack里面使用__dirname,读取到的是webpack配置的绝对路径/
  158. * 可能有用的polyfill就是crypto这些通用的模块,类似path和fs这些模
  159. * 块其实都是他们的polyfill都是跑在浏览器的,只是有这些api原本的一些功能,
  160. * 还是没有nodejs的能力,所以webpack5干脆就移除了这些polyfill,你可以通过
  161. * 安装他们的polyfill来实现原本webpack4之前的功能,但是即使安装他们的polyfill
  162. * 也只是实现api的功能,没有他们原本在node的能力
  163. */
  164. // path: require.resolve('path-browserify'),
  165. // path: false,
  166. // fs: false,
  167. // child_process: false,
  168. },
  169. },
  170. resolveLoader: {
  171. // 用于解析webpack的loader
  172. modules: ['node_modules'],
  173. },
  174. module: {
  175. noParse: /^(vue|vue-router)$/,
  176. // loader执行顺序:从下往上,从右往左
  177. rules: [
  178. {
  179. test: /\.vue$/,
  180. use: [
  181. {
  182. loader: 'vue-loader',
  183. },
  184. ],
  185. },
  186. // {
  187. // test: /\.jsx?$/,
  188. // exclude: /node_modules/,
  189. // use: [
  190. // // 'thread-loader',
  191. // {
  192. // loader: 'babel-loader',
  193. // options: {
  194. // cacheDirectory: true,
  195. // cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
  196. // },
  197. // },
  198. // ],
  199. // },
  200. // {
  201. // test: /\.ts$/,
  202. // exclude: /node_modules/,
  203. // use: [
  204. // {
  205. // loader: 'babel-loader',
  206. // options: {
  207. // cacheDirectory: true,
  208. // cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
  209. // },
  210. // },
  211. // {
  212. // loader: 'ts-loader',
  213. // options: {
  214. // appendTsSuffixTo: ['\\.vue$'],
  215. // // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
  216. // transpileOnly: true,
  217. // happyPackMode: false,
  218. // },
  219. // },
  220. // ],
  221. // },
  222. // {
  223. // test: /\.tsx$/,
  224. // exclude: /node_modules/,
  225. // use: [
  226. // {
  227. // loader: 'babel-loader',
  228. // options: {
  229. // cacheDirectory: true,
  230. // cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
  231. // },
  232. // },
  233. // {
  234. // loader: 'ts-loader',
  235. // options: {
  236. // appendTsxSuffixTo: ['\\.vue$'],
  237. // // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
  238. // transpileOnly: true,
  239. // happyPackMode: false,
  240. // },
  241. // },
  242. // ],
  243. // },
  244. {
  245. test: /\.css$/,
  246. oneOf: [
  247. {
  248. resourceQuery: /module/,
  249. use: cssRules(isProduction, true),
  250. },
  251. {
  252. resourceQuery: /\?vue/,
  253. use: cssRules(isProduction),
  254. },
  255. {
  256. test: /\.module\.\w+$/,
  257. use: cssRules(isProduction, true),
  258. },
  259. {
  260. use: cssRules(isProduction),
  261. },
  262. ],
  263. sideEffects: true, // 告诉webpack是有副作用的,不对css进行删除
  264. },
  265. {
  266. test: /\.(sass|scss)$/,
  267. oneOf: [
  268. {
  269. resourceQuery: /module/,
  270. use: sassRules(isProduction, true),
  271. },
  272. {
  273. resourceQuery: /\?vue/,
  274. use: sassRules(isProduction),
  275. },
  276. {
  277. test: /\.module\.\w+$/,
  278. use: sassRules(isProduction, true),
  279. },
  280. {
  281. use: sassRules(isProduction),
  282. },
  283. ],
  284. sideEffects: true,
  285. },
  286. {
  287. test: /\.(jpg|jpeg|png|gif|svg|webp)$/,
  288. type: 'asset',
  289. generator: {
  290. filename: 'img/[name]-[contenthash:6][ext]',
  291. },
  292. parser: {
  293. dataUrlCondition: {
  294. maxSize: 4 * 1024, // 如果一个模块源码大小小于 maxSize,那么模块会被作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中
  295. },
  296. },
  297. },
  298. {
  299. test: /\.(eot|ttf|woff2?)$/,
  300. type: 'asset/resource',
  301. generator: {
  302. filename: 'font/[name]-[contenthash:6][ext]',
  303. },
  304. },
  305. ],
  306. },
  307. plugins: [
  308. // 友好的显示错误信息在终端
  309. new FriendlyErrorsWebpackPlugin(),
  310. // 解析vue
  311. new VueLoaderPlugin(),
  312. // eslint-disable-next-line
  313. ComponentsPlugin({
  314. // eslint-disable-next-line
  315. resolvers: [NaiveUiResolver()],
  316. }),
  317. // windicss
  318. windicssEnable && new WindiCSSWebpackPlugin(),
  319. // 该插件将为您生成一个HTML5文件,其中包含使用脚本标签的所有Webpack捆绑包
  320. new HtmlWebpackPlugin({
  321. filename: 'index.html',
  322. title: htmlWebpackPluginTitle,
  323. template: resolveApp('./public/index.html'),
  324. hash: true,
  325. minify: isProduction
  326. ? {
  327. collapseWhitespace: true, // 折叠空白
  328. keepClosingSlash: true, // 在单标签上保留末尾斜杠
  329. removeComments: true, // 移除注释
  330. removeRedundantAttributes: true, // 移除多余的属性(如:input的type默认就是text,如果写了type="text",就移除它,因为不写它默认也是type="text")
  331. removeScriptTypeAttributes: true, // 删除script标签中type="text/javascript"
  332. removeStyleLinkTypeAttributes: true, // 删除style和link标签中type="text/css"
  333. useShortDoctype: true, // 使用html5的<!doctype html>替换掉之前的html老版本声明方式<!doctype>
  334. // 上面的都是production模式下默认值。
  335. removeEmptyAttributes: true, // 移除一些空属性,如空的id,classs,style等等,但不是空的就全删,比如<img alt />中的alt不会删。http://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_or_blank_attributes
  336. minifyCSS: true, // 使用clean-css插件删除 CSS 中一些无用的空格、注释等。
  337. minifyJS: true, // 使用Terser插件优化
  338. }
  339. : false,
  340. chunks: ['main'], // 要仅包含某些块,您可以限制正在使用的块
  341. }),
  342. // 注入项目信息
  343. new BilldHtmlWebpackPlugin({
  344. env: 'webpack5',
  345. }),
  346. // 将已存在的单个文件或整个目录复制到构建目录。
  347. new CopyWebpackPlugin({
  348. patterns: [
  349. {
  350. from: 'public', // 复制public目录的文件
  351. // to: 'assets', //复制到output.path下的assets,不写默认就是output.path根目录
  352. globOptions: {
  353. ignore: [
  354. // 复制到output.path时,如果output.paht已经存在重复的文件了,会报错:
  355. // ERROR in Conflict: Multiple assets emit different content to the same filename md.html
  356. '**/index.html', // 忽略from目录下的index.html,它是入口文件
  357. ],
  358. },
  359. },
  360. ],
  361. }),
  362. // 定义全局变量
  363. new DefinePlugin({
  364. BASE_URL: `${JSON.stringify(outputStaticUrl(isProduction))}`, // public下的index.html里面的favicon.ico的路径
  365. 'process.env': {
  366. BilldHtmlWebpackPlugin: JSON.stringify(logData()),
  367. NODE_ENV: JSON.stringify(isProduction ? 'production' : 'development'),
  368. PUBLIC_PATH: JSON.stringify(outputStaticUrl(isProduction)),
  369. VUE_APP_RELEASE_PROJECT_NAME: JSON.stringify(
  370. process.env.VUE_APP_RELEASE_PROJECT_NAME
  371. ),
  372. VUE_APP_RELEASE_PROJECT_ENV: JSON.stringify(
  373. process.env.VUE_APP_RELEASE_PROJECT_ENV
  374. ),
  375. },
  376. __VUE_OPTIONS_API__: false,
  377. __VUE_PROD_DEVTOOLS__: false,
  378. }),
  379. // bundle分析
  380. analyzerEnable &&
  381. new BundleAnalyzerPlugin({
  382. analyzerMode: 'server',
  383. generateStatsFile: true,
  384. statsOptions: { source: false },
  385. }), // configuration.plugins should be one of these object { apply, … } | function
  386. // eslint
  387. eslintEnable &&
  388. new ESLintPlugin({
  389. extensions: ['js', 'jsx', 'ts', 'tsx', 'vue'],
  390. emitError: false, // 发现的错误将始终发出,禁用设置为false.
  391. emitWarning: false, // 找到的警告将始终发出,禁用设置为false.
  392. failOnError: false, // 如果有任何错误,将导致模块构建失败,禁用设置为false
  393. failOnWarning: false, // 如果有任何警告,将导致模块构建失败,禁用设置为false
  394. cache: true,
  395. cacheLocation: resolveApp('./node_modules/.cache/.eslintcache'),
  396. }),
  397. ].filter(Boolean),
  398. };
  399. return result;
  400. };
  401. export default (env) => {
  402. return new Promise((resolve) => {
  403. const isProduction = env.production;
  404. process.env.NODE_ENV = isProduction ? 'production' : 'development';
  405. const configPromise = Promise.resolve(
  406. isProduction ? prodConfig : devConfig
  407. );
  408. configPromise.then(
  409. (config: any) => {
  410. // 根据当前环境,合并配置文件
  411. const mergeConfig = merge(commonConfig(isProduction), config);
  412. console.log(
  413. chalkWARN(
  414. `根据当前环境,合并配置文件,当前是: ${process.env.NODE_ENV!}环境`
  415. )
  416. );
  417. resolve(mergeConfig);
  418. },
  419. (err) => {
  420. console.log(err);
  421. }
  422. );
  423. });
  424. };