很多人都或多或少使用过 webpack,但是很少有人能够系统的学习 webpack 配置,遇到错误的时候就会一脸懵,不知道从哪查起?性能优化时也不知道能做什么,网上的优化教程是不是符合自己的项目?等一系列问题!本文从最基础配置一步步到一个完善的大型项目的过程。让你对 webpack 再也不会畏惧,让它真正成为你的得力助手!
本文从下面几个课题来实现
我把每一课都切成了不同的分支,大家可以根据课时一步步学习
npm i -g webpack-box
webpack-box dev # 开发环境 webpack-box build # 生产环境 webpack-box dll # 编译差分包 webpack-box dev index # 指定页面编译(多页面) webpack-box build index # 指定页面编译(多页面) webpack-box build index --report # 开启打包分析 webpack-box build:ssr # 编译ssr webpack-box ssr:server # 在 server 端运行
在 package.json 中使用
{ "scripts": { "dev": "webpack-box dev", "build": "webpack-box build", "dll": "webpack-box dll", "build:ssr": "webpack-box build:ssr", "ssr:server": "webpack-box ssr:server" } }
使用
npm run build --report # 开启打包分析
扩展配置
box.config.js
module.exports = function (config) { /** * @param {object} dll 开启差分包 * @param {object} pages 多页面配置 通过 box run/build index 来使用 * @param {function} chainWebpack * @param {string} entry 入口 * @param {string} output 出口 * @param {string} publicPath * @param {string} port */ return { entry: ‘src/main.js‘, output: ‘dist‘, publicPath: ‘/common/‘, port: 8888, dll: { venders: [‘vue‘, ‘react‘] }, pages: { index: { entry: ‘src/main.js‘, template: ‘public/index.html‘, filename: ‘index.html‘, }, index2: { entry: ‘src/main.js‘, template: ‘public/index2.html‘, filename: ‘index2.html‘, } }, chainWebpack(config) { } } }
想要学好 webpack
,我们首先要了解 webpack
的机制,我们先从js加载css开始学习。
我们从下面这个小练习开始走进 webpack
吧
在 index.js
中引入 index.css
const css = require(‘./index.css‘)
console.log(css)
css 文件并不能被 js 识别,webpack 也不例外,上述的写法不出意外会报错
我们如何让 webpack 识别 css 呢,答案就在 webpack 给我们提供了 loader 机制,可以让我们通过loader 将任意的文件转成 webpack 可以识别的文件
本章主要讲解
package.json
{ "scripts": { "dev": "cross-env NODE_ENV=development webpack", // 开发环境 "build": "cross-env NODE_ENV=production webpack" // 生产环境 }, "dependencies": { "cross-env": "^6.0.3", // 兼容各种环境 "css-loader": "^3.2.0", "rimraf": "^3.0.0", // 删除文件 "webpack": "^4.41.2" }, "devDependencies": { "webpack-cli": "^3.3.10" } }
webpack.config.js
const path = require(‘path‘); const rimraf = require(‘rimraf‘); // 删除 dist 目录 rimraf.sync(‘dist‘); // webpack 配置 module.exports = { entry: ‘./src/index‘, mode: process.env.NODE_ENV, output: { filename: ‘bundle.js‘, path: path.resolve(__dirname, ‘dist‘) } };
src/index.js
const css = require(‘css-loader!./index.css‘); const a = 100; console.log(a, css);
src/index.css
body { width: 100%; height: 100vh; background-color: orange; }
我删掉了一些注释跟一些干扰内容,这样看起来会更清晰一点
bundle
是一个立即执行函数,可以认为它是把所有模块捆绑在一起的一个巨型模块。webpack
将所有模块打包成了 bundle
的依赖,通过一个对象注入0 模块
就是入口webpack
通过 __webpack_require__
引入模块__webpack_require__
就是我们使用的 require
,被 webpack
封装了一层dist/bundle.js
(function(modules) { function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = (installedModules[moduleId] = { i: moduleId, l: false, exports: {} }); modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); module.l = true; return module.exports; } return __webpack_require__((__webpack_require__.s = 0)); })({ ‘./src/index.js‘: function(module, exports, __webpack_require__) { eval(` const css = __webpack_require__("./src/style/index.css") const a = 100; console.log(a, css) `); }, ‘./src/style/index.css‘: function(module, exports, __webpack_require__) { eval(` exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false); exports.push([module.i, "body { width: 100%; height: 100vh; background-color: orange; }", ""]); `); }, 0: function(module, exports, __webpack_require__) { module.exports = __webpack_require__(‘./src/index.js‘); } });
如果我们把 index.js 的 require 改成 import 会发生什么?
我们知道 import
跟 require
的区别是,import
是动态加载只有在用到的时候才会去加载,而 require
只要声明了就会加载,webpack
遇到了 require
就会把它当成一个模块加载到 bundle
的依赖里
那么问题来了,如果我们使用了 import 去引用一个模块,它是如何加载的呢?
src/index.js
// const css = require(‘css-loader!./index.css‘); const css = import(‘css-loader!./index.css‘); const a = 100; console.log(a, css);
除了正常的 bundle
之外,我们还可以看见一个 0.boundle.js
0.boundle.js
就是我们的动态加载的 index.css
模块
|-- bundle.js
|-- 0.boundle.js
0.boundle.js
这个文件就是把我们 import
的模块放进了一个单独的 js
文件中
(window[‘webpackJsonp‘] = window[‘webpackJsonp‘] || []).push([ [0], { ‘./node_modules/css-loader/dist/runtime/api.js‘: function( module, exports, __webpack_require__ ) { ‘use strict‘; eval(` ... `); }, ‘./src/style/index.css‘: function(module, exports, __webpack_require__) { eval(` exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false)); exports.push([module.i, \`body { width: 100%; height: 100vh; background-color: orange; },"\`] `); } } ]);
我们再看下 dist/bundle.js
方便理解,我把大部分代码和注释都删掉了
原理很简单,就是利用的 jsonp 的实现原理加载模块,只是在这里并不是从 server 拿数据而是从其他模块中
window
上注册一个 webpackJsonp
数组,window[‘webpackJsonp‘] = window[‘webpackJsonp‘] || []import
时,webpack
会调用 __webpack_require__.e(0)
方法,也就是 requireEnsure
webpack
会动态创建一个 script
标签去加载这个模块,加载成功后会将该模块注入到 webpackJsonp
中webpackJsonp.push
会调用 webpackJsonpCallback
拿到模块__webpack_require__
获取模块(function(modules) { function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var moduleId, chunkId, i = 0, resolves = []; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if ( Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId] ) { resolves.push(installedChunks[chunkId][0]); } // 模块安装完 installedChunks[chunkId] = 0; } for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) { // 执行所有 promise 的 resolve 函数 resolves.shift()(); } } function jsonpScriptSrc(chunkId) { return __webpack_require__.p + ‘‘ + ({}[chunkId] || chunkId) + ‘.bundle.js‘; } function __webpack_require__(moduleId) { // ... } __webpack_require__.e = function requireEnsure(chunkId) { var promises = []; // ... var script = document.createElement(‘script‘); var onScriptComplete; script.charset = ‘utf-8‘; script.timeout = 120; script.src = jsonpScriptSrc(chunkId); onScriptComplete = function(event) { // 处理异常,消除副作用 // ... }; var timeout = setTimeout(function() { onScriptComplete({ type: ‘timeout‘, target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; document.head.appendChild(script); // ... // 动态加载模块 return Promise.all(promises); }; var jsonpArray = (window[‘webpackJsonp‘] = window[‘webpackJsonp‘] || []); // 重写数组 push 方法 jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); return __webpack_require__((__webpack_require__.s = 0)); })({ ‘./src/index.js‘: function(module, exports, __webpack_require__) { eval(` const css = __webpack_require__.e(0).then(__webpack_require__.t.bind(null, "./src/style/index.css", 7)) const a = 100; console.log(a, css) `); }, 0: function(module, exports, __webpack_require__) { eval(`module.exports = __webpack_require__("./src/index.js");`); } });
我们用 webpack-chain 来写 webpack 的配置,原因是 webpack-chain 的方式更加灵活
官方解释
webpack-chain
尝试通过提供可链式或顺流式的API
创建和修改webpack
配置。API
的Key
部分可以由用户指定的名称引用,这有助于跨项目修改配置方式的标准化。
const path = require(‘path‘); const rimraf = require(‘rimraf‘); const Config = require(‘webpack-chain‘); const config = new Config(); const resolve = src => { return path.join(process.cwd(), src); }; // 删除 dist 目录 rimraf.sync(‘dist‘); config // 入口 .entry(‘src/index‘) .add(resolve(‘src/index.js‘)) .end() // 模式 // .mode(process.env.NODE_ENV) 等价下面 .set(‘mode‘, process.env.NODE_ENV) // 出口 .output.path(resolve(‘dist‘)) .filename(‘[name].bundle.js‘); config.module .rule(‘css‘) .test(/\.css$/) .use(‘css‘) .loader(‘css-loader‘); module.exports = config.toConfig();
至此课时 1 已经结束了,我们主要做了以下事情
学习一个工具我们不仅要看懂它的配置,还要对它的原理一起了解,只有学到框架的精髓,我们才能应对如今大前端如此迅猛的发展。
本章提要:
│── build │ │── base.js // 公共部分 │ │── build.js │ └── dev.js │── config │ │── base.js // 基础配置 │ │── css.js // css 配置 │ │── HtmlWebpackPlugin.js // html 配置 │ └── MiniCssExtractPlugin.js // 提取css │── public // 公共资源 │ └── index.html // html 模版 └── src // 开发目录 │── style │ └── index.css └── main.js // 主入口
package.json
{ "scripts": { "dev": "cross-env NODE_ENV=development node build/dev.js", "build": "cross-env NODE_ENV=production node build/build.js" }, "dependencies": { "cross-env": "^6.0.3", "css-loader": "^3.2.0", "cssnano": "^4.1.10", "ora": "^4.0.3", "rimraf": "^3.0.0", "webpack": "^4.41.2" }, "devDependencies": { "extract-text-webpack-plugin": "^3.0.2", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.8.0", "vue-cli-plugin-commitlint": "^1.0.4", "webpack-chain": "^6.0.0", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.9.0" } }
build/base.js
const { findSync } = require(‘../lib‘); const Config = require(‘webpack-chain‘); const config = new Config(); const files = findSync(‘config‘); const path = require(‘path‘); const resolve = p => { return path.join(process.cwd(), p); }; module.exports = () => { const map = new Map(); files.map(_ => { const name = _.split(‘/‘) .pop() .replace(‘.js‘, ‘‘); return map.set(name, require(_)(config, resolve)); }); map.forEach(v => v()); return config; };
build/build.js
const rimraf = require(‘rimraf‘); const ora = require(‘ora‘); const chalk = require(‘chalk‘); const path = require(‘path‘); // 删除 dist 目录 rimraf.sync(path.join(process.cwd(), ‘dist‘)); const config = require(‘./base‘)(); const webpack = require(‘webpack‘); const spinner = ora(‘开始构建项目...‘); spinner.start(); webpack(config.toConfig(), function(err, stats) { spinner.stop(); if (err) throw err; process.stdout.write( stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + ‘\n\n‘ ); if (stats.hasErrors()) { console.log(chalk.red(‘构建失败\n‘)); process.exit(1); } console.log(chalk.cyan(‘build完成\n‘)); });
build/dev.js
const config = require(‘./base‘)(); const webpack = require(‘webpack‘); const chalk = require(‘chalk‘); const WebpackDevServer = require(‘webpack-dev-server‘); const port = 8080; const publicPath = ‘/common/‘; config.devServer .quiet(true) .hot(true) .https(false) .disableHostCheck(true) .publicPath(publicPath) .clientLogLevel(‘none‘); const compiler = webpack(config.toConfig()); // 拿到 devServer 参数 const chainDevServer = compiler.options.devServer; const server = new WebpackDevServer( compiler, Object.assign(chainDevServer, {}) ); [‘SIGINT‘, ‘SIGTERM‘].forEach(signal => { process.on(signal, () => { server.close(() => { process.exit(0); }); }); }); // 监听端口 server.listen(port); new Promise(() => { compiler.hooks.done.tap(‘dev‘, stats => { const empty = ‘ ‘; const common = `App running at: - Local: http://127.0.0.1:${port}${publicPath}\n`; console.log(chalk.cyan(‘\n‘ + empty + common)); }); });
config/css.js
module.exports = (config, resolve) => { return (lang, test) => { const baseRule = config.module.rule(lang).test(test); const normalRule = baseRule.oneOf(‘normal‘); applyLoaders(normalRule); function applyLoaders(rule) { rule .use(‘extract-css-loader‘) .loader(require(‘mini-css-extract-plugin‘).loader) .options({ publicPath: ‘./‘ }); rule .use(‘css-loader‘) .loader(‘css-loader‘) .options({}); } }; };
config/MiniCssExtractPlugin.js
const MiniCssExtractPlugin = require(‘mini-css-extract-plugin‘); module.exports = (config, resolve) => { return () => { config .oneOf(‘normal‘) .plugin(‘mini-css-extract‘) .use(MiniCssExtractPlugin); }; };
config/HtmlWebpackPlugin.js
const HtmlWebpackPlugin = require(‘html-webpack-plugin‘); module.exports = (config, resolve) => { return () => { config.plugin(‘html‘).use(HtmlWebpackPlugin, [ { template: ‘public/index.html‘ } ]); }; };
public/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>learn_webpack</title> <body></body> </html>
src/style/index.css
.test { width: 200px; height: 200px; color: red; background-color: orange; }
src/main.js
require(‘./style/index.css‘); const h2 = document.createElement(‘h2‘); h2.className = ‘test‘; h2.innerText = ‘test‘; document.body.append(h2);
本章提要:
增加以下文件
│──── config // 配置目录 │ │── babelLoader.js // babel-loader 配置 │ │── ForkTsChecker.js // ts 静态检查 │ │── FriendlyErrorsWebpackPlugin.js // 友好错误提示 │ └── style │──── src // 开发目录 │ │── style │ │ │── app.css │ │ │── index.less // 测试 less │ │ │── index.scss // 测试 sass │ │ └── index.postcss // 测试 postcss │ └── ts │ └── index.ts // 测试 ts │── babel.js │── postcss.config.js // postcss 配置 │── tsconfig.json // ts 配置 └──── dist // 打包后的目录 │── app.bundle.js │── app.css └── index.html
config/babelLoader.js
module.exports = (config, resolve) => { const baseRule = config.module.rule(‘js‘).test(/.js│.tsx?$/); const babelPath = resolve(‘babel.js‘); const babelConf = require(babelPath); const version = require(resolve(‘node_modules/@babel/core/package.json‘)) .version; return () => { baseRule .use(‘babel‘) .loader(require.resolve(‘babel-loader‘)) .options(babelConf({ version })); }; };
这里我们使用 babel
插件 @babel/preset-typescript
将 ts
转成 js,并使用
ForkTsCheckerWebpackPlugin
、ForkTsCheckerNotifierWebpackPlugin
插件进行错误提示。
babel.js
module.exports = function(api) { return { presets: [ [ ‘@babel/preset-env‘, { targets: { chrome: 59, edge: 13, firefox: 50, safari: 8 } } ], [ ‘@babel/preset-typescript‘, { allExtensions: true } ] ], plugins: [ ‘@babel/plugin-transform-typescript‘, ‘transform-class-properties‘, ‘@babel/proposal-object-rest-spread‘ ] }; };
const ForkTsCheckerWebpackPlugin = require(‘fork-ts-checker-webpack-plugin‘); const ForkTsCheckerNotifierWebpackPlugin = require(‘fork-ts-checker-notifier-webpack-plugin‘); module.exports = (config, resolve) => { return () => { config.plugin(‘ts-fork‘).use(ForkTsCheckerWebpackPlugin, [ { // 将async设为false,可以阻止Webpack的emit以等待类型检查器/linter,并向Webpack的编译添加错误。 async: false } ]); // 将TypeScript类型检查错误以弹框提示 // 如果fork-ts-checker-webpack-plugin的async为false时可以不用 // 否则建议使用,以方便发现错误 config.plugin(‘ts-notifier‘).use(ForkTsCheckerNotifierWebpackPlugin, [ { title: ‘TypeScript‘, excludeWarnings: true, skipSuccessful: true } ]); }; };
config/FriendlyErrorsWebpackPlugin.js
const FriendlyErrorsWebpackPlugin = require(‘friendly-errors-webpack-plugin‘); module.exports = (config, resolve) => { return () => { config.plugin(‘error‘).use(FriendlyErrorsWebpackPlugin); }; };
module.exports = (config, resolve) => { const createCSSRule = (lang, test, loader, options = {}) => { const baseRule = config.module.rule(lang).test(test); const normalRule = baseRule.oneOf(‘normal‘); normalRule .use(‘extract-css-loader‘) .loader(require(‘mini-css-extract-plugin‘).loader) .options({ hmr: process.env.NODE_ENV === ‘development‘, publicPath: ‘/‘ }); normalRule .use(‘css-loader‘) .loader(require.resolve(‘css-loader‘)) .options({}); normalRule.use(‘postcss-loader‘).loader(require.resolve(‘postcss-loader‘)); if (loader) { const rs = require.resolve(loader); normalRule .use(loader) .loader(rs) .options(options); } }; return () => { createCSSRule(‘css‘, /\.css$/, ‘css-loader‘, {}); createCSSRule(‘less‘, /\.less$/, ‘less-loader‘, {}); createCSSRule(‘scss‘, /\.scss$/, ‘sass-loader‘, {}); createCSSRule(‘postcss‘, /\.p(ost)?css$/); }; };
module.exports = { plugins: { ‘postcss-px-to-viewport‘: { unitToConvert: ‘px‘, viewportWidth: 750, unitPrecision: 5, propList: [‘*‘], viewportUnit: ‘vw‘, fontViewportUnit: ‘vw‘, selectorBlackList: [], minPixelValue: 1, mediaQuery: false, replace: true, exclude: [], landscape: false, landscapeUnit: ‘vw‘, landscapeWidth: 568 } } };
src/style/index.less
/* index.less */ .test { width: 300px; }
dist/app.css
/* index.css */ .test { width: 36.66667vw; height: 26.66667vw; color: red; background-color: orange; } /* app.css */ .test { font-size: 8vw; } /* index.less */ .test { width: 40vw; } /* index.scss */ .test { height: 40vw; } /* index.postcss */ .test { background: green; height: 26.66667vw; }
自动添加 css 前缀
postcss.config.js
module.exports = { plugins: { autoprefixer: { overrideBrowserslist: [ ‘> 1%‘, ‘last 3 versions‘, ‘iOS >= 8‘, ‘Android >= 4‘, ‘Chrome >= 40‘ ] } } };
/* index.css */ .test { width: 200px; height: 200px; color: red; display: flex; background-color: orange; }
/* index.css */ .test { width: 26.66667vw; height: 26.66667vw; color: red; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; background-color: orange; }
config.devtool(‘cheap-source-map‘);
└── dist
│── app.bundle.js
│── app.bundle.js.map
│── app.css
│── app.css.map
└── index.html
在源文件下会有一行注释,证明开启了 sourcemap
/*# sourceMappingURL=app.css.map*/
本章讲解
module.exports = (config, resolve) => { return () => { config .optimization .runtimeChunk({ name: "manifest" }) } }
babel-plugin-import
插件按需引入一些组件库将公共的包提取到 chunk-vendors
里面,比如你require(‘vue‘),webpack 会将 vue 打包进 chunk-vendors.bundle.js
module.exports = (config, resolve) => { return () => { config .optimization.splitChunks({ chunks: ‘async‘, minSize: 30000, minChunks: 1, maxAsyncRequests: 3, maxInitialRequests: 3, cacheGroups: { vendors: { name: `chunk-vendors`, test: /[\\/]node_modules[\\/]/, priority: -10, chunks: ‘initial‘ }, common: { name: `chunk-common`, minChunks: 2, priority: -20, chunks: ‘initial‘, reuseExistingChunk: true } } }) config.optimization.usedExports(true) } }
config/optimization.js
config.optimization.usedExports(true);
src/treeShaking.js
export function square(x) { return x * x; } export function cube(x) { return x * x * x; }
在 main.js 中只引用了 cube
import { cube } from ‘./treeShaking‘;
console.log(cube(2));
{ "./src/treeShaking.js": function( module, __webpack_exports__, __webpack_require__ ) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "square", function() { return square; }); __webpack_require__.d(__webpack_exports__, "cube", function() { return cube; }); function square(x) { return x * x; } function cube(x) { return x * x * x; } } }
这里只导出了 cube 函数,并没有将 square 导出去
当然你可以看见 square 函数还是在 bundle 里面,但是在压缩的时候就会被干掉了,因为它并没有被引用
{ "./src/treeShaking.js": function( module, __webpack_exports__, __webpack_require__ ) { "use strict"; __webpack_require__.d(__webpack_exports__, "a", function() { return cube; }); function square(x) { return x * x; } function cube(x) { return x * x * x; } } }
只有当函数给定输入后,产生相应的输出,且不修改任何外部的东西,才可以安全做shaking的操作
如何使用tree-shaking?
其实在 webpack4
我们根本不需要做这些操作了,因为 webpack
在生产环境已经帮我们默认添加好了,开箱即用!
CompressionWebpackPlugin.js
const CompressionWebpackPlugin = require(‘compression-webpack-plugin‘); module.exports = (config, resolve) => { return () => { config.plugin(‘CompressionWebpackPlugin‘).use(CompressionWebpackPlugin, [ { algorithm: ‘gzip‘, test: /\.js(\?.*)?$/i, threshold: 10240, minRatio: 0.8 } ]); }; };
本章内容
webpack loader
是 webpack
为了处理各种类型文件的一个中间层,webpack
本质上就是一个 node
模块,它不能处理 js
以外的文件,那么 loader
就帮助 webpack
做了一层转换,将所有文件都转成字符串,你可以对字符串进行任意操作/修改,然后返回给 webpack
一个包含这个字符串的对象,让 webpack
进行后面的处理。如果把 webpack
当成一个垃圾工厂的话,那么 loader
就是这个工厂的垃圾分类!
这里并不是纯粹意义上的可选链,因为 babel
跟 ts
都已经支持了,我们也没有必要去写一个完整的可选链,只是来加深一下对 loader
的理解, loader
在工作当中能帮助我们做什么?
用途
当我们访问一个对象属性时不必担心这个对象是 undefined
而报错,导致程序不能继续向下执行
解释
在 ?
之前的所有访问链路都是合法的,不会产生报错
const obj = { foo: { bar: { baz: 2 } } } console.log(obj.foo.bar?.baz) // 2 // 被转成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz console.log(obj.foo.err?.baz) // undefined // 被转成 obj && obj.foo && obj.foo.err && obj.foo.err.baz
配置loader,options-chain-loader
config/OptionsChainLoader.js
module.exports = (config, resolve) => { const baseRule = config.module.rule(‘js‘).test(/.js|.tsx?$/); const normalRule = baseRule.oneOf(‘normal‘); return () => { normalRule .use(‘options-chain‘) .loader(resolve(‘options-chain-loader‘)) } }
其实就是正则替换,loader
将整个文件全部转换成字符串,content
就是整个文件的内容,对 content
进行修改,修改完成后再返回一个新的 content
就完成了一个 loader
转换。是不是很简单?
下面的操作意思就是,我们匹配 obj.foo.bar?.
并把它转成 obj && obj.foo && obj.foo.bar && obj.foo.bar.
options-chain-loader.js
module.exports = function(content) { return content.replace(new RegExp(/([\$_\w\.]+\?\.)/,‘g‘),function(res) { let str = res.replace(/\?\./,‘‘); let arrs = str.split(‘.‘); let strArr = []; for(let i = 1; i <= arrs.length; i++) { strArr.push(arrs.slice(0,i).join(‘.‘)); } let compile = strArr.join(‘&&‘); const done = compile + ‘&&‘ + str + ‘.‘ return done; }); };
本章内容
cache-loader
主要是将打包好的文件缓存在硬盘的一个目录里,一般存在 node_modules/.cache
下,当你再次 build
的时候如果此文件没有修改就会从缓存中读取已经编译过的文件,只有有改动的才会被编译,这样就大大降低了编译的时间。尤其是项目越大时越明显。
此项目使用前后数据对比 3342ms --> 2432ms 效果还是比较明显
这里只对 babel 加入了 cache-loader,因为我们的 ts/js 都是由 babel 进行编译的,不需要对 ts-loader 缓存(我们也没有用到)
config/cacheLoader.js
module.exports = (config, resolve) => { const baseRule = config.module.rule(‘js‘).test(/.js|.tsx?$/); const babelPath = resolve(‘babel.js‘) const babelConf = require(babelPath); const version = require(resolve(‘node_modules/@babel/core/package.json‘)).version return () => { baseRule .exclude .add(filepath => { // 不缓存 node_modules 下的文件 return /node_modules/.test(filepath) }) .end() .use(‘cache-loader‘) .loader(‘cache-loader‘) .options({ // 缓存位置 cacheDirectory: resolve(‘node_modules/.cache/babel‘) }) } }
DllPlugin 是将第三方长期不变的包与实际项目隔离开来并分别打包,当我们 build 时再将已经打包好的 dll 包引进来就 ok 了
我提取了两个包 vue、react,速度差不多提升了 200ms,从 2698ms 到 2377ms
build/dll.js
const path = require("path"); const dllPath = path.join(process.cwd(), ‘dll‘); const Config = require(‘webpack-chain‘); const config = new Config(); const webpack = require(‘webpack‘) const rimraf = require(‘rimraf‘); const ora = require(‘ora‘) const chalk = require(‘chalk‘) const BundleAnalyzerPlugin = require(‘../config/BundleAnalyzerPlugin‘)(config) BundleAnalyzerPlugin() config .entry(‘dll‘) .add(‘vue‘) .add(‘react‘) .end() .set(‘mode‘, "production") .output .path(dllPath) .filename(‘[name].js‘) .library("[name]") .end() .plugin(‘DllPlugin‘) .use(webpack.DllPlugin, [{ name: "[name]", path: path.join(process.cwd(), ‘dll‘, ‘manifest.json‘), }]) .end() rimraf.sync(path.join(process.cwd(), ‘dll‘)) const spinner = ora(‘开始构建项目...‘) spinner.start() webpack(config.toConfig(), function (err, stats) { spinner.stop() if (err) throw err process.stdout.write(stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + ‘\n\n‘) if (stats.hasErrors()) { console.log(chalk.red(‘构建失败\n‘)) process.exit(1) } console.log(chalk.cyan(‘build完成\n‘)) })
const webpack = require(‘webpack‘) module.exports = (config, resolve) => { return () => { config.plugin(‘DllPlugin‘) .use(webpack.DllReferencePlugin, [{ context: process.cwd(), manifest: require(resolve(‘dll/manifest.json‘)) }]) } }
测试效果变差了 ??,线程数越小编译速度越快
config/threadLoader.js
module.exports = (config, resolve) => { const baseRule = config.module.rule(‘js‘).test(/.js|.tsx?$/); return () => { const useThreads = true; if (useThreads) { const threadLoaderConfig = baseRule .use(‘thread-loader‘) .loader(‘thread-loader‘); threadLoaderConfig.options({ workers: 3 }) } } }
注意
弃用
npm run build & npm run dev & npm run dll改成
box build & box dev & box dlllink
npm link 将 box 命令链接到全局本章内容
box build # 不加参数则会编译所有页面,并清空 dist
box dev # 默认编译 index 页面
参数
# index2 是指定编译的页面。不会清空 dist # report 开启打包分析 box build index2 --report box dev index2 --report
分成三个命令,进行不同操作
bin/box.js
#!/usr/bin/env node const chalk = require(‘chalk‘) const program = require(‘commander‘) const packageConfig = require(‘../package.json‘); const { cleanArgs } = require(‘../lib‘) const path = require(‘path‘) const __name__ = `build,dev,dll` let boxConf = {} let lock = false try { boxConf = require(path.join(process.cwd(), ‘box.config.js‘))() } catch (error) { } program .usage(‘<command> [options]‘) .version(packageConfig.version) .command(‘build [app-page]‘) .description(`构建开发环境`) .option(‘-r, --report‘, ‘打包分析报告‘) .option(‘-d, --dll‘, ‘合并差分包‘) .action(async (name, cmd) => { const options = cleanArgs(cmd) const args = Object.assign(options, { name }, boxConf) if (lock) return lock = true; if (boxConf.pages) { Object.keys(boxConf.pages).forEach(page => { args.name = page; require(‘../build/build‘)(args) }) } else { require(‘../build/build‘)(args) } }) program .usage(‘<command> [options]‘) .version(packageConfig.version) .command(‘dev [app-page]‘) .description(`构建生产环境`) .option(‘-d, --dll‘, ‘合并差分包‘) .action(async (name, cmd) => { const options = cleanArgs(cmd) const args = Object.assign(options, { name }, boxConf) if (lock) return lock = true; require(‘../build/dev‘)(args) }) program .usage(‘<command> [options]‘) .version(packageConfig.version) .command(‘dll [app-page]‘) .description(`编译差分包`) .action(async (name, cmd) => { const options = cleanArgs(cmd) const args = Object.assign(options, { name }, boxConf) if (lock) return lock = true; require(‘../build/dll‘)(args) }) program.parse(process.argv).args && program.parse(process.argv).args[0]; program.commands.forEach(c => c.on(‘--help‘, () => console.log())) if (process.argv[2] && !__name__.includes(process.argv[2])) { console.log() console.log(chalk.red(` 没有找到 ${process.argv[2]} 命令`)) console.log() program.help() } if (!process.argv[2]) { program.help() }
box.config.js
module.exports = function (config) { return { entry: ‘src/main.js‘, // 默认入口 dist: ‘dist‘, // 默认打包目录 publicPath: ‘/‘, port: 8888, pages: { index: { entry: ‘src/main.js‘, template: ‘public/index.html‘, filename: ‘index.html‘, }, index2: { entry: ‘src/main.js‘, template: ‘public/index2.html‘, filename: ‘index2.html‘, } }, chainWebpack(config) { } } }
如果把 webpack 当成一个垃圾工厂,loader 就是垃圾分类,将所有垃圾整理好交给 webpack。plugin 就是如何去处理这些垃圾。
webpack 插件写起来很简单,就是你要知道各种各样的钩子在什么时候触发,然后你的逻辑写在钩子里面就ok了
apply
函数是 webpack 在调用 plugin 的时候执行的,你可以认为它是入口compiler
暴露了和 webpack 整个生命周期相关的钩子Compilation
暴露了与模块和依赖有关的粒度更小的事件钩子本节概要
我们今天写一个 copy 的插件,在webpack构建完成之后,将目标目录下的文件 copy 到另一个目录下
const fs = require(‘fs-extra‘) const globby = require(‘globby‘) class CopyDirWebpackPlugin { constructor(options) { this.options = options; } apply(compiler) { const opt = this.options compiler.plugin(‘done‘, (stats) => { if (process.env.NODE_ENV === ‘production‘) { (async ()=>{ const toFilesPath = await globby([`${opt.to}/**`, ‘!.git/**‘]) toFilesPath.forEach(filePath => fs.removeSync(filePath)) const fromFilesPath = await globby([`${opt.from}/**`]) fromFilesPath.forEach(fromPath => { const cachePath = fromPath fromPath = fromPath.replace(‘dist‘, opt.to) const dirpaths = fromPath.substring(0, fromPath.lastIndexOf(‘/‘)) fs.mkdirpSync(dirpaths) fs.copySync(cachePath, fromPath) }) console.log(` 完成copy ${opt.from} to ${opt.to}`) })() } }); } } module.exports = CopyDirWebpackPlugin
将打包出来的 dist 目录下的内容 copy 到 dist2 目录下
const CopyPlugin = require(‘../webapck-plugin-copy‘); module.exports = ({ config }) => { return () => { config.plugin(‘copy-dist‘) .use(CopyPlugin, [{ from: ‘dist‘, to: ‘dist2‘ }]) } }
ssr 就是服务端渲染,做 ssr 的好处就是为了处理 spa 的不足,比如 seo 优化,服务端缓存等问题。
今天主要用 react 的 ssr 来做一个简单的实例,让大家更清晰的入门
本章概要
老规矩,先来一个 box build:ssr
命令让程序可以执行
执行 box build:ssr
会调用 build/ssr
执行编译
program .usage(‘<command> [options]‘) .version(packageConfig.version) .command(‘build:ssr [app-page]‘) .description(`服务端渲染`) .action(async (name, cmd) => { const options = cleanArgs(cmd); const args = Object.assign(options, { name }, boxConf); if (lock) return; lock = true; require(‘../build/ssr‘)(args); });
与其他的编译没有什么区别,值得住的是
.libraryTarget(‘umd‘)
.globalObject(‘this‘)
build/ssr.js
module.exports = function(options) { const path = require(‘path‘); const Config = require(‘webpack-chain‘); const config = new Config(); const webpack = require(‘webpack‘); const rimraf = require(‘rimraf‘); const ora = require(‘ora‘); const chalk = require(‘chalk‘); const PATHS = { build: path.join(process.cwd(), ‘static‘), ssrDemo: path.join(process.cwd(), ‘src‘, ‘ssr.jsx‘) }; require(‘../config/babelLoader‘)({ config, tsx: true })(); require(‘../config/HtmlWebpackPlugin‘)({ config, options: { publicPath: ‘/‘, filename: ‘client.ssr.html‘ } })(); config .entry(‘ssr‘) .add(PATHS.ssrDemo) .end() .set(‘mode‘, ‘development‘) // production .output.path(PATHS.build) .filename(‘[name].js‘) .libraryTarget(‘umd‘) .globalObject(‘this‘) .library(‘[name]‘) .end(); rimraf.sync(path.join(process.cwd(), PATHS.build)); const spinner = ora(‘开始构建项目...‘); spinner.start(); webpack(config.toConfig(), function(err, stats) { spinner.stop(); if (err) throw err; process.stdout.write( stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + ‘\n\n‘ ); if (stats.hasErrors()) { console.log(chalk.red(‘构建失败\n‘)); process.exit(1); } console.log(chalk.cyan(‘build完成\n‘)); }); };
因为我们是用 react 写的,避免不了会用到 jsx 语法,所以我们需要在 babel-loader
中使用 @babel/preset-react
npm i @babel/preset-react -D
config/babelLoader.js
if (tsx) { babelConf.presets.push(‘@babel/preset-react‘); }
区分服务端跟客户端分别渲染
const React = require("react"); const ReactDOM = require("react-dom"); const SSR = <div onClick={() => alert("hello")}>Hello world</div>; if (typeof document === "undefined") { console.log(‘在服务端渲染‘) module.exports = SSR; } else { console.log(‘在客户端渲染‘) const renderMethod = !module.hot ? ReactDOM.render : ReactDOM.hydrate; renderMethod(SSR, document.getElementById("app")); }
module.exports = function (options) { const express = require("express"); const { renderToString } = require("react-dom/server"); const chalk = require(‘chalk‘) const SSR = require("../static/ssr"); const port = process.env.PORT || 8080; server(port); function server(port) { const app = express(); app.use(express.static("static")); app.get("/", (req, res) => res.status(200).send(renderMarkup(renderToString(SSR))) ); const empty = ‘ ‘ const common = `App running at: - Local: http://127.0.0.1:${port}\n` console.log(chalk.cyan(‘\n‘ + empty + common)) app.listen(port, () => process.send && process.send("online")); } function renderMarkup(html) { return `<!DOCTYPE html> <html> <head> <title>Webpack SSR Demo</title> <meta charset="utf-8" /> </head> <body> <div id="app">${html}</div> <script src="./ssr.js"></script> </body> </html>`; } }
至此 ssr 已经结束了,其实所有看起来很高大上的技术都是从一点一滴积累起来的,只要我们明白原理,你也能做出更优秀的框架
这个可能大概写了两个多星期,每天写一点点积少成多,自我感觉提升了很大,如果有兴趣跟我一起学习的同学可以来加我进群,我在群里会每天组织不同的课题来学习。
原文:https://www.cnblogs.com/cangqinglang/p/11997184.html