[toc]
为什么要自己配置 webpack
目前社区提供的基于webpack的前端打包方案很多 umi create-react-app 等,但是查看对应的文档后,它们所能配置的MPA(多页面应用),均不是我想要的;
既然很多整合的打包方案基于webpack,又得知在webpack4 以后配置简化了很多,并且构建速度更快,为什么不按照自己的需求配置一套webpack的MPA打包方案呢?了解了webpack后,再使用 umi之类二次封装的工具会更得心应手吧.
本文仅用于记录学习,文章中出现的代码托管在: frmachao/webpack-examples
网站目录结构
后端工程和前端工程 在同一个工程中,前端编译TS/JSX文件后交给服务端使用
这可能不是一个好的方式
|--
|--build
|--build.js # 构建脚本 例如:node build/build.js all --build
|--rules.js #
|--utils.js
|--webpack.common.js
|--webpack.dev.js
|--webpack.prod.js
|--dist # 前端打包后的资源没有`html`文件,`html`交给服务端处理
|--mpa
|--page1
|--index.js
|--page2
|--index.js
|--verdor.js
|--spa1
|--xxx
|--server # 网站后端
|--dist # 后端构建
|--src
|--middlewares
|--controllers
|--models
|--app.ts
|--public
|--view
|--site # 网站前端
|--mpa #多页面应用资源目录
|--page1
|--index.tsx
|--page2
|--spa1 # 单页面应用
|--spa2
|--xxxx
遇到的问题
CleanWebpackPlugin is not a constructor- 将引用方式改为
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
- 将引用方式改为
- 新版本的CleanWebpackPlugin 不知道为啥 不能清除dist 目录,暂时先用旧版本的
- css module 和 antd 冲突
- 解决
- eslint 与路径 别名冲突
- 解决
- **当仓库根目录存在两个文件夹时 eslint 不生效 **
自己加的一些插件
当我想通过 npm run watch-mpa 来执行开发时构建所有的多页面应用,那么就需要方便的拿到 命令行中参数
"build": "node build/build.js all --build",
"build-mpa": "node build/build.js site/mpa --build",
"watch-mpa": "node build/build.js site/mpa --watch",
dotenv 用于从文件中加载环境变量。环境变量用于程序运行时动态加载参数,除了环境变量,我们还可以在启动Node.js 程序时直接指定命令行参数:
node index.js --beep=boop -t -z 12 -n5 foo bar
console.log(process.argv);
// ['/bin/node', '/tmp/index.js', '--beep=boop', '-t', '-z', '12', '-n5', 'foo', 'bar']
process.argv 变量是一个数组,数组前两项分别是 node 程序位置和js脚本位置,数组中随后的元素都是我们启动Node.js后的参数,这些参数以空格分隔成数组
虽然从 process.argv 中可以得到启动参数列表,但是我们仍需要对参数进行进一步解析处理才行。
minimist minimist 是一个专门用于处理Node.js启动参数的库,可以将 process.argv 中的参数列表转换成更加易于使用的格式:
node index.js --beep=boop -t -z 12 -n5 foo bar
const argv = require('minimist')(process.argv.slice(2));
console.dir(argv);
// { _: [ 'foo', 'bar' ], beep: 'boop', t: true, z: 12, n: 5 }
配置typescript支持
babel 7 @babel/preset-typescript + babel-loader 的方案:
优点:
- 编译速度块,可以使用babel生态中的插件 比如 antd 的 babel-plugin-import(当初折腾fuse-box配置时就是因为它放弃了..)
缺点:
- 有四种语法在 babel 中是无法编译的
- Namespace语法不推荐,改用标准的 ES6 module(import/export)。
- 不支持x 语法转换类型,改用x as newtype。
- const 枚举
- 历史遗留风格的 import/export 语法。比如:import foo = require(...) 和 export = foo。
第1条和第4条不用,而且已经过时了。
第2条缺陷改一下语法就好了,这个语法会直接提示语法报错,很好改,问题不大。
第3条缺陷已经没有了。
- 没有了类型检查
VSCode自带 TS 检测,个人项目来说所以这也不是问题
使用中遇到的问题:
- ts 无法使用 JSX,除非提供了 "--jsx" 标志
- 定义 tsconfig 文件 在里面声明对jsx的支持,注意:我们并不使用tsc来编译,仅仅是开发时让ide有提示
- webpack 使用 @babel/preset-typescript 编译ts 打包时,会找不到别名的路径
- 自动解析确定的扩展名 这个大坑
参考:
- TypeScript 和 Babel:一场美丽的婚姻
- babel-JS装饰器语法支持
- 使用@babel/preset-typescript取代awesome-typescript-loader和ts-loader
babel 配置
当前使用的babel版本:
"@babel/core": "^7.9.0", 一定要看跟自己使用版本对应的文档!! babel-preset-env
- 使用babel 的目的:
- 使用最新的 TS/JS 语法,甚至一些处于提案阶段的特性
- babel转译后兼容更多的浏览的版本
babel 这块的配置,我当时主要卡在 @babel/preset-env 的useBuiltIns 选项 和@babel/plugin-transform-runtime 插件。因为babel 在转译高版本JS代码时,只会转译语法不会处理新的API,看下面的例子:
// src/index.js
const add = (a, b) => a + b
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()[object Object]
babel执行后
// dist/index.js
"use strict";
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();
Babel 把 ES6 的标准分为 syntax 和 built-in 两种类型。syntax 就是语法,像 const、=> 这些默认被 Babel 转译的就是 syntax 的类型。而对于那些可以通过改写覆盖的语法就认为是 built-in,像 includes 和 Promise 这些都属于 built-in。而 Babel 默认只转译 syntax 类型的,对于 built-in 类型的就需要通过 @babel/polyfill 来完成转译。@babel/polyfill 实现的原理也非常简单,就是覆盖那些 ES6 新增的 built-in。示意如下:
Object.defineProperty(Array.prototype, 'includes',function(){
...
})
babel在7.4版本中废弃 @babel/polyfill ,而是通过 core-js 替代;
在 @babel/preset-env 中通过 useBuiltIns 参数来控制 built-in 的注入;
useBuiltIns 这个选项配置@babel/preset-env 处理 polyfills 的方式,它可以设置为 'entry'、'usage' 和 false 。默认值为 false,不注入垫片。
- 'entry' 时,只需要在整个项目的入口处,导入 core-js 即可。
- 'usage' 时,就不用在项目的入口处,导入 core-js了,Babel 会在编译源码的过程中根据 built-in 的使用情况来选择注入相应的实现
// src/index.js
const add = (a, b) => a + b
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()
// dist/index.js
"use strict";
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es7.array.includes");
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();
继续优化 引入 @babel/plugin-transform-runtime 插件: 在介绍 @babel/plugin-transform-runtime 的用途之前,先前一个例子:
// src/index.js
const add = (a, b) => a + b
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise(resolve=>resolve(10))
class Person {
static a = 1;
static b;
name = 'morrain';
age = 18
}
// dist/index.js
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.define-property");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
return resolve(10);
});
var Person = function Person() {
_classCallCheck(this, Person);
_defineProperty(this, "name", 'morrain');
_defineProperty(this, "age", 18);
};
_defineProperty(Person, "a", 1);
_defineProperty(Person, "b", void 0);
在编译的过程中,对于 built-in 类型的语法通过 require("core-js/modules/xxxx") polyfill 的方式来兼容,对于 syntax 类型的语法在转译的过程会在当前模块中注入类似 _classCallCheck 和 _defineProperty 的 helper 函数来实现兼容。对于一个模块而言,可能还好,但对于项目中肯定是很多模块,每个模块模块都注入这些 helper 函数,势必会造成代码量变得很大。
而 @babel/plugin-transform-runtime 就是为了复用这些 helper 函数,缩小代码体积而生的。当然除此之外,它还能为编译后的代码提供一个沙箱环境,避免全局污染。
- @ babel/runtime + @babel/plugin-transform-runtime
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
@babel/plugin-transform-runtime 是编译时使用的,安装为开发依赖,而 @babel/runtime 其实就是 helper 函数的集合,需要被引入到编译后代码中,所以安装为生产依赖
// 修改babel 配置
const presets = [
[
'@babel/env',
{
debug: true,
useBuiltIns: 'usage',
corejs: 3,
targets: {}
}
]
]
const plugins = [
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-transform-runtime'
]
]
- 之前的例子,再次编译后,可以看到,之前的 helper 函数,都变成类似require("@babel/runtime/helpers/classCallCheck") 的实现了。
// dist/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
return resolve(10);
});
var Person = function Person() {
(0, _classCallCheck2["default"])(this, Person);
(0, _defineProperty2["default"])(this, "name", 'morrain');
(0, _defineProperty2["default"])(this, "age", 18);
};
(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);
最终项目中的babel配置:
{
test: /\.(js|jsx|tsx)$/,
// include: [path.join(__dirname, '../site')],
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
options: {
cacheDirectory: true,
// presets执行顺序 倒叙
presets: [
[
"@babel/preset-env",
process.env.NODE_ENV === "production"
? {
targets: {
// 浏览器占有率 >5% 最新的两个版本 火狐最新版 ie9 以下
browsers: [
"> 5%",
"last 2 versions",
"Firefox ESR",
"not ie <= 9",
],
},
useBuiltIns: "usage",
corejs: { version: 3, proposals: true },
}
: {
targets: { browsers: ["last 2 Chrome versions"] }, // 想让开发环境构建的更快一些 只针对 chrome 最新的两个版本编译
},
],
"@babel/preset-react",
["@babel/preset-typescript"],
],
plugins: [
// 开启对处于实验性质 ES 提案支持 ,忘记当初为什么配置这个了
["@babel/plugin-proposal-decorators", { "legacy": true }],// 装饰器支持
["@babel/plugin-proposal-class-properties", { "loose": true }],// 类内部静态属性支持
// 配置antd desgin 按需加载
[
"import",
{
libraryName: "antd",
libraryDirectory: "es",
style: "css", // `style: true` 会加载 less 文件
},
],
// 避免在编译后的输出中重复代码 以及 避免全局变量污染
"@babel/plugin-transform-runtime",
// 懒加载 前端路由按需加载
"@babel/plugin-syntax-dynamic-import"
],
},
},
},
参考:
使用ESLint+Prettier规范React+Typescript项目
参考:
webpack loaders中的include/exclude有什么用?
其实就是官方文档中有写,甚至就在配置的开头,唉 看东西要有耐心啊,完整的看完文档真的是基本要求
rules: [
// 模块规则(配置 loader、解析器等选项)
{
test: /\.jsx?$/,
include: [
path.resolve(__dirname, "app")
],
exclude: [
path.resolve(__dirname, "app/demo-files")
],
// 这里是匹配条件,每个选项都接收一个正则表达式或字符串
// test 和 include 具有相同的作用,都是必须匹配选项
// exclude 是必不匹配选项(优先于 test 和 include)
// 最佳实践:
// - 只在 test 和 文件名匹配 中使用正则表达式
// - 在 include 和 exclude 中使用绝对路径数组
// - 尽量避免 exclude,更倾向于使用 include
issuer: { test, include, exclude },
// issuer 条件(导入源)
enforce: "pre",
enforce: "post",
// 标识应用这些规则,即使规则覆盖(高级选项)
loader: "babel-loader",
// 应该应用的 loader,它相对上下文解析
// 为了更清晰,`-loader` 后缀在 webpack 2 中不再是可选的
// 查看 webpack 1 升级指南。
options: {
presets: ["es2015"]
},
// loader 的可选项
},
]
webpack 打包优化
runtimeChunk? 这个我没搞懂 它是不是真的需要
构建脚本 ./build/build.js
const cp = require('child_process');
const path = require('path');
const fs = require('fs');
const { getEntry } = require('./utils')
const buildRoot = path.join(__dirname)
const projectRoot = path.join(buildRoot, '..')
const distRoot = path.join(projectRoot, '/dist')
/**
* 开发中产出dist目录交给服务端使用 node build.js [site/map|spa1|all] --watch
* 构建生产环境 node build.js all --build
*/
const argv = require('minimist')(process.argv.slice(2));
let hasWatch = argv.watch ? '--watch' : ''
isBuild = argv.build ? `${buildRoot}/webpack.prod.js` : `${buildRoot}/webpack.dev.js`
fileName = argv._[0];
isHashName = argv.build ? '[name][hash]' : '[name]'
if (argv.build) {
process.env.NODE_ENV = 'production'
}
if (fileName === 'all') {
fs.readdirSync(`${projectRoot}/site`).forEach(file => {
const blockFiles = ['.DS_Store', 'common', 'mpa']
if (blockFiles.indexOf(file) === -1) {
buildSPA(file)
}
});
buildMPA()
return false;
}
if (fileName === 'site/mpa') {
buildMPA()
return false;
}
buildSPA(fileName)
function buildMPA() {
let ENTRY_FILESNAME = {};
OUT_PUT_STR = `${distRoot}/mpa/${isHashName}.js --output-public-path=/fe-static/mpa/`;
if (!fs.readdirSync(`${projectRoot}/site/mpa`)) {
console.log('err:there is no such project。');
return false;
}
// 遍历文件夹目录 获取到多页面文件夹名称
// getEntry(): 处理成 <name> = <request> 的键值对 来动态生成 <entry>
// 最终像这样:datum/index=/Users/ma/DEV/webSite/site/mpa/datum/index.tsx game/index=/Users/ma/DEV/webSite/site/mpa/game/index.tsx
fs.readdirSync(`${projectRoot}/site/mpa`).forEach(file => {
const blockFiles = ['.DS_Store']
if (blockFiles.indexOf(file) === -1) {
ENTRY_FILESNAME[`${file}/index`] = (`${projectRoot}/site/mpa/${file}/index.tsx`);
}
});
buildOne(getEntry(ENTRY_FILESNAME), OUT_PUT_STR);
}
function buildSPA(name) {
console.log('buildSPA===', name)
let ENTRY_FILESNAME = {};
let OUT_PUT_STR = `${distRoot}/${name}/${isHashName}.js --output-public-path=/fe-static/${name}/`;
if (!fs.readdirSync(`${projectRoot}/site`).find(file => file === name)) {
console.log(`err!!!!!!!!:${projectRoot}/site is no such project。`);
return false;
}
ENTRY_FILESNAME[`index`] = (`${projectRoot}/site/${name}/index.tsx`);
buildOne(getEntry(ENTRY_FILESNAME), OUT_PUT_STR);
}
function buildOne(ENTRY_FILESNAME_STR, OUT_PUT_STR) {
console.log('ENTRY_FILESNAME_STR===========>', ENTRY_FILESNAME_STR)
// -o <output> 它将被映射到 output.path 和 output.filename 的配置选项
const cpBuild = cp.exec(
`npx webpack ${hasWatch} ${ENTRY_FILESNAME_STR} --config ${isBuild} -o ${OUT_PUT_STR}`,
(error, stdout, stderr) => {
if (error) {
console.error('error:', error);
}
}
)
cpBuild.stdout.on('data', (data) => {
console.log('on stdout: ' + data);
});
cpBuild.stderr.on('on data', (data) => {
if (data) {
console.log('on stderr: ' + data);
}
});
}
如何构建前端工程
开发中产出dist目录交给服务端使用 node build.js [`site/map`|`spa1`|`all`] --watch
构建生产环境 node build.js all --build
'site/mpa' 是约定的多页面工程目录