开篇
很多人都或多或少使用过 webpack,但是很少有人能够系统的学习 webpack 配置,遇到错误的时候就会一脸懵,不知道从哪查起?性能优化时也不知道能做什么,网上的优化教程是不是符合自己的项目?等一系列问题!本文从最基础配置一步步到一个完善的大型项目的过程。让你对 webpack 再也不会畏惧,让它真正成为你的得力助手!
本文从下面几个课题来实现
- 课题 1:初探 webpack?探究 webpack 打包原理
- 课题 2:搭建开发环境跟生产环境
- 课题 3:基础配置之loader
- 课时 4:webpack性能优化
- 课时 5:手写loader实现可选链
- 课时 6:webpack编译优化
- 课时 7:多页面配置
- 课时 8:手写一个webpack插件
- 课时 9:构建 ssr
项目地址
github.com/luoxue-vict…
我把每一课都切成了不同的分支,大家可以根据课时一步步学习
脚手架
npm i -g webpack-box复制代码
使用
在 package.json 中使用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 # 编译ssrwebpack-box ssr:server # 在 server 端运行复制代码
使用{"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.jsmodule.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) {}}}复制代码
课题 1:初探 webpack?探究 webpack 打包原理
想要学好 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 可以识别的文件
本章主要讲解
- webpack 基础配置
- 解析 bundle 如何加载模块
- 动态 import 加载原理
- 使用 webpack-chain 重写配置
- 课时 1 小结
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 基础配置
webpack.config.jsconst 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')}};复制代码
css 引入到 js
src/index.jsconst css = require('css-loader!./index.css');const a = 100;console.log(a, css);复制代码
测试 css
src/index.cssbody {width: 100%;height: 100vh;background-color: orange;}复制代码
解析 bundle 如何加载模块
我删掉了一些注释跟一些干扰内容,这样看起来会更清晰一点
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');}});复制代码
动态 import 加载原理
如果我们把 index.js 的 require 改成 import 会发生什么?
我们知道 import 跟 require 的区别是,import 是动态加载只有在用到的时候才会去加载,而 require 只要声明了就会加载,webpack 遇到了 require 就会把它当成一个模块加载到 bundle 的依赖里
那么问题来了,如果我们使用了 import 去引用一个模块,它是如何加载的呢?
require 改成 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.js0.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拿到模块- 模块加载完(then)再使用
__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-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 小结
至此课时 1 已经结束了,我们主要做了以下事情
- webpack 基础配置
- 将 css 通过 css-loader 打包进 js 中
- 解析 bundle 如何加载模块的
- webpack 如何实现的动态加载模块
学习一个工具我们不仅要看懂它的配置,还要对它的原理一起了解,只有学到框架的精髓,我们才能应对如今大前端如此迅猛的发展。
课题 2:搭建开发环境跟生产环境
本章提要:
- 目录
- 实现可插拔配置
- 构建生产环境
- 构建开发环境(devServer)
- 提取 css
- 自动生成 html
- 项目测试
目录
│── 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
build/base.js{"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"}}复制代码
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.jsconst 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'));});复制代码
构建开发环境(devServer)
build/dev.jsconst 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));});});复制代码
提取 css
config/css.jscss 提取 loader 配置
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({});}};};复制代码
css 提取插件 MiniCssExtractPlugin
config/MiniCssExtractPlugin.jsconst MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = (config, resolve) => {return () => {config.oneOf('normal').plugin('mini-css-extract').use(MiniCssExtractPlugin);};};复制代码
自动生成 html
config/HtmlWebpackPlugin.jsconst HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = (config, resolve) => {return () => {config.plugin('html').use(HtmlWebpackPlugin, [{template: 'public/index.html'}]);};};复制代码
项目测试
测试 html 模板
public/index.html<!DOCTYPE html><html><head><meta charset="UTF-8"><title>learn_webpack</title><body></body></html>复制代码
测试 css 模板
src/style/index.css.test {width: 200px;height: 200px;color: red;background-color: orange;}复制代码
程序入口
src/main.jsrequire('./style/index.css');const h2 = document.createElement('h2');h2.className = 'test';h2.innerText = 'test';document.body.append(h2);复制代码
课题 3:基础配置之loader
本章提要:
- 配置 babel
- 使用 babel 配置 ts
- ts 静态类型检查
- 友好错误提示插件
- 配置样式,style,css、less、sass、postcss 等
- postcss 配置
- 编译前后 css 对比
- 配置 autoprefixer
- 开启 source map
目录
增加以下文件│──── 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复制代码
配置 babel
config/babelLoader.jsmodule.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 配置 ts
这里我们使用babel插件@babel/preset-typescript将ts转成js,并使用ForkTsCheckerWebpackPlugin、ForkTsCheckerNotifierWebpackPlugin插件进行错误提示。
babel.jsmodule.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']};};复制代码
ts 静态类型检查
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.jsconst FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');module.exports = (config, resolve) => {return () => {config.plugin('error').use(FriendlyErrorsWebpackPlugin);};};复制代码
配置样式,style,css、less、sass、postcss 等
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$/);};};复制代码
postcss 配置
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}}};复制代码
编译前后 css 对比
src/style/index.less
dist/app.css/* index.less */.test {width: 300px;}复制代码
/* 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;}复制代码
配置 autoprefixer
自动添加 css 前缀
postcss.config.jsmodule.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;}复制代码
开启 source map
config.devtool('cheap-source-map');复制代码
在源文件下会有一行注释,证明开启了 sourcemap└── dist│── app.bundle.js│── app.bundle.js.map│── app.css│── app.css.map└── index.html复制代码
/*# sourceMappingURL=app.css.map*/复制代码
课时 4:webpack性能优化
本章讲解
- 分离 Manifest
- Code Splitting(代码分割)
- Bundle Splitting(打包分割)
- Tree Shaking(删除死代码)
-
分离 Manifest
module.exports = (config, resolve) => {return () => {config.optimization.runtimeChunk({name: "manifest"})}}复制代码
Code Splitting
使用动态 import 或者 require.ensure 语法,在第一节已经讲解
使用
babel-plugin-import插件按需引入一些组件库Bundle Splitting
将公共的包提取到
chunk-vendors里面,比如你require(‘vue’),webpack 会将 vue 打包进 chunk-vendors.bundle.jsmodule.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)}}复制代码
Tree Shaking
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));复制代码
未使用 Tree Shaking
{"./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;}}}复制代码
使用了 Tree Shaking
这里只导出了 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?确保代码是es6格式,即 export,import
- package.json中,设置 sideEffects
- 确保 tree-shaking 的函数没有副作用
- babelrc中设置presets [[“@babel/preset-env”, { “modules”: false }]] 禁止转换模块,交由webpack进行模块化处理
- 结合uglifyjs-webpack-plugin
其实在 webpack4 我们根本不需要做这些操作了,因为 webpack 在生产环境已经帮我们默认添加好了,开箱即用!
开启 gzip
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}]);};};复制代码
课时 5:手写loader实现可选链
本章内容
- 什么是 webpack loader
- 可选链介绍
- loader 实现可选链
什么是 webpack loader
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.bazconsole.log(obj.foo.err?.baz) // undefined// 被转成 obj && obj.foo && obj.foo.err && obj.foo.err.baz复制代码
loader 实现可选链
配置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.jsmodule.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;});};复制代码
课时 6:webpack编译优化
本章内容
- cache-loader
- DllPlugin
- threadLoader
cache-loader
cache-loader主要是将打包好的文件缓存在硬盘的一个目录里,一般存在node_modules/.cache下,当你再次build的时候如果此文件没有修改就会从缓存中读取已经编译过的文件,只有有改动的才会被编译,这样就大大降低了编译的时间。尤其是项目越大时越明显。
此项目使用前后数据对比 3342ms —> 2432ms 效果还是比较明显
这里只对 babel 加入了 cache-loader,因为我们的 ts/js 都是由 babel 进行编译的,不需要对 ts-loader 缓存(我们也没有用到)
config/cacheLoader.jsmodule.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')).versionreturn () => {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
DllPlugin 是将第三方长期不变的包与实际项目隔离开来并分别打包,当我们 build 时再将已经打包好的 dll 包引进来就 ok 了
我提取了两个包 vue、react,速度差不多提升了 200ms,从 2698ms 到 2377ms打包 dll
build/dll.jsconst 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 errprocess.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'))})复制代码
将 dll 包合并
const webpack = require('webpack')module.exports = (config, resolve) => {return () => {config.plugin('DllPlugin').use(webpack.DllReferencePlugin, [{context: process.cwd(),manifest: require(resolve('dll/manifest.json'))}])}}复制代码
threadLoader
测试效果变差了 😅,线程数越小编译速度越快
config/threadLoader.jsmodule.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 })}}}复制代码
课时 7:多页面配置
注意
弃用npm run build & npm run dev & npm run dll改成box build & box dev & box dlllinknpm link 将 box 命令链接到全局
本章内容
- 使用
- 改造为脚手架
- 多页面配置
使用
box build # 不加参数则会编译所有页面,并清空 distbox dev # 默认编译 index 页面复制代码
参数# index2 是指定编译的页面。不会清空 dist# report 开启打包分析box build index2 --reportbox dev index2 --report复制代码
改造为脚手架
分成三个命令,进行不同操作
- build
- dev
- dll
bin/box.js
#!/usr/bin/env nodeconst 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 = falsetry {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) returnlock = 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) returnlock = 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) returnlock = 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) {}}}复制代码
课时 8:手写一个webpack插件
如果把 webpack 当成一个垃圾工厂,loader 就是垃圾分类,将所有垃圾整理好交给 webpack。plugin 就是如何去处理这些垃圾。
webpack 插件写起来很简单,就是你要知道各种各样的钩子在什么时候触发,然后你的逻辑写在钩子里面就ok了
apply函数是 webpack 在调用 plugin 的时候执行的,你可以认为它是入口compiler暴露了和 webpack 整个生命周期相关的钩子Compilation暴露了与模块和依赖有关的粒度更小的事件钩子
本节概要
- 实现一个 CopyPlugin
- 使用
实现一个 CopyPlugin
我们今天写一个 copy 的插件,在webpack构建完成之后,将目标目录下的文件 copy 到另一个目录下const fs = require('fs-extra')const globby = require('globby')class CopyDirWebpackPlugin {constructor(options) {this.options = options;}apply(compiler) {const opt = this.optionscompiler.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 = fromPathfromPath = 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'}])}}复制代码
课时 9:构建 ssr
ssr 就是服务端渲染,做 ssr 的好处就是为了处理 spa 的不足,比如 seo 优化,服务端缓存等问题。
今天主要用 react 的 ssr 来做一个简单的实例,让大家更清晰的入门
本章概要
- 创建 box build:ssr
- 编译 ssr
- 编译 jsx 语法
- 入口区分服务端/客户端
- 服务端渲染
-
创建 box build: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);});复制代码
编译 ssr
与其他的编译没有什么区别,值得住的是
target 指定为 umd 模式
- globalObject 为 this
入口改为 ssr.jsx
.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'));});};复制代码
编译 jsx 语法
因为我们是用 react 写的,避免不了会用到 jsx 语法,所以我们需要在
babel-loader中使用@babel/preset-reactnpm 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"));}复制代码
服务端渲染
将打包出来的 static 文件夹作为一个服务
- 访问 http://127.0.0.1:8080,进入服务端渲染的页面
再执行一遍 ssr.js 进行事件绑定
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 已经结束了,其实所有看起来很高大上的技术都是从一点一滴积累起来的,只要我们明白原理,你也能做出更优秀的框架
完结
这个可能大概写了两个多星期,每天写一点点积少成多,自我感觉提升了很大,如果有兴趣跟我一起学习的同学可以来加我进群,我在群里会每天组织不同的课题来学习。
接下来的课题大概是:
- 手写 vue-next 源码
- ts 从入门到放弃
- node 入门到哭泣
哈哈,开玩笑,大概就是这样,半个月差不多一个专题,如果你有好的专题也可以一起来讨论
最后两件小事
- 有想入群的学习前端进阶的加我微信
luoxue2479回复加群即可 - 有写错的地方和更好的建议可以在下面
留言,一起讨论 - 想看更多干货可以关注我的公众号【前端技匠】

