前言:想要搭建一个企业级前端开发脚手架是需要很多前置知识的,我们将自己的工具定位为企业级,那当然是越完美越好喽!我们的样例将基于webpack+ts+react+babel来搭建前端脚手架,接下来我们一一介绍这些前置知识。
一、webpack
概念:webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具 。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源。
学习webpack还是比较简单的,只需要明白几个概念(配置、入口、出口、loader、插件、),具体配置非常的方便,webpack
配置
webpack 开箱即用,可以无需使用任何配置文件。然而,webpack 会假定项目的入口起点为 src/index.js
,然后会在 dist/main.js
输出结果,并且在生产环境开启压缩和优化。 通常你的项目还需要继续扩展此能力,为此你可以在项目根目录下创建一个 webpack.config.js
文件,然后 webpack 会自动使用它。 如果出于某些原因,需要根据特定情况使用不同的配置文件,则可以通过在命令行中使用 --config
标志修改。也可以简写为-c
package.json
json
"scripts": {
"build": "webpack --config webpack.prod.config.js"
}
入口(Entry)
入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 默认值是 ./src/index.js
,但你可以通过在 webpack configuration 中配置 entry
属性,来指定一个(或多个)不同的入口起点。例如:
webpack.config.js
ini
module.exports = {
entry: './src/index.js'
};
输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 bundle ,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
你可以通过在配置中指定一个 output
字段,来配置这些处理过程:
webpack.config.js
lua
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
}
};
到此,我们已经可以新建一个webpack项目来实践一下了,后续概念将在实践中穿插讲解
arduino
//在命令行里输入以下命令创建webpack-demo项目
cd Desktop && mkdir webpack-demo
用vscode或者webstorm或者自己喜欢的编辑器打开webpack-demo项目文件夹,目前是空空如也~
接下来,初始化项目并安装webpack开发依赖
csharp
//进入项目文件夹根目录
cd webpack-demo
//快速生成包管理文件package.json文件
npm init -y
//安装webpack包和webpack-cli(webpack命令接口运行环境包)
npm install webpack webpack-cli --save-dev
我这里为了方便直接就在webstorm集成terminal中输入命令了哈~
可以看到,webpack包装成功后,我们package.json文件中devDependencies会出现包的名称和对应版本,我们这里是最新的版本(有新的谁还会用旧的呢😏)
我们添加源码文件夹src,并在文件夹下创建index.js
bash
mkdir src && cd src && touch index.js
我们在src/index.js中写一点测试脚本代码
javascript
class HelloWebpack {
constructor(msg) {
console.log(msg || 'Hello World', '✅北京欢迎你');
}
}
const helloWorldInstance = new HelloWebpack('hello webpack');
我们先用node执行以下我们的脚本,看是否报错:
我们现在完全没有配置webpack的配置文件和入口、出口,直接打包,验证一下,在项目根目录下的package.json中配置脚本命令如下:
package.json
bash
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack"
},
命令行运行pnpm run build命令
arduino
pnpm run build
毫无疑问的是,打包成功了,webpack真的是太简单了。
到了这里有的同学会问,上文说我们用typescript来做js的类型声明控制,webpack也可以直接这样打包吗?浏览器可不能直接执行ts文件呢?好,这个时候轮到我们loader闪亮登场~!既然我们想要ts,那索性我们的webpack配置文件也使用ts来配置,这样才对味嘛,不然显得逼格不够高。
首先我们在项目根目录下创建webpack.config.ts配置文件
arduino
touch webpack.config.ts
要使用 Typescript 来编写 webpack 配置,我们需要先安装必要的依赖,比如 Typescript 以及其相应的类型声明,类型声明可以从 DefinitelyTyped 项目中获取,依赖安装如下:
ts-node是基于tsc编译器的一款运行时TypeScript编译器,它允许TypeScript代码在运行时通过Node.js环境直接执行。 这意味着,无需先构建应用程序,因此可以快速进行开发和测试,大大提高了开发效率
@types/node 的作用就是提供这些Node.js API 的类型定义,使得在TypeScript 项目中使用Node.js API 时,可以获得更好的代码提示、类型检查和代码补全等功能。 这有助于提高代码的可读性、可维护性和安全性。
@types/webpack该包包含 webpack 的类型定义
scss
npm install --save-dev typescript ts-node @types/node @types/webpack
devDependencies家族又壮大了。 我们开始编写webpack.config.ts配置文件
javascript
import * as path from 'path';
import * as webpack from 'webpack';
const config: webpack.Configuration = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
};
export default config;
我们更改一下package.json中脚本的配置(也可以不改,因为我们还没做配置文件分离,webpack会默认加载,这里我们先验证一下)
diff
# packge.json
...
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
- "build": "webpack",
+ "build": "webpack -c webpack.config.ts",
}
...
将我们的入口文件改为ts格式,添加函数形参的类型声明。
diff
class HelloWebpack {
+ constructor(msg: string) {
console.log(msg || 'Hello World', '✅北京欢迎你');
}
}
const helloWorldInstance = new HelloWebpack('hello webpack');
运行npm run build,不出我们所料,报错了💔
解析typescript有两种loader,一个是ts-loader,另一个就是我们熟悉的babel-loader,我们这里选择babel-loader,因为babel-loader可以将js代码转换为ES5
安装babel-loader相关依赖
bash
npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-typescript
在webpack.config.ts中添加配置
diff
import * as path from 'path';
import * as webpack from 'webpack';
const config: webpack.Configuration = {
mode: 'production',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
},
+ module: {
+ rules: [
+ {
+ test: /.tsx?$/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: ['@babel/preset-env', '@babel/preset-typescript']
+ }
+ },
+ exclude: /node_modules/,
+ },
+ ],
+ },
};
export default config;
运行npm run build, perfect!!!
我们开发时可以将mode改为develoment看一下我们打包的结果,
并且我们发现dist目录还有上次打包后的文件,我们也在出口添加一下配置clean:true, resolve.extensions 尝试按顺序解析这些后缀名。如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀
diff
import * as path from 'path';
import * as webpack from 'webpack';
const config: webpack.Configuration = {
- mode: 'production',
+ mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
+ clean: true
},
module: {
rules: [
{
test: /.tsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-typescript']
}
},
exclude: /node_modules/,
},
],
},
+ resolve: {
+ extensions: ['.tsx', '.ts', '.js', '.json']
+ }
};
export default config;
运行npm run build
在这里有的同学就纳闷了,不是说ts需要tsconfig.json配置文件嘛,为什么我们这里没有呢,其实是因为我们使用的babel-loader而不是ts-loader
请注意,如果已经使用
babel-loader
转译代码,可以使用@babel/preset-typescript
以让 Babel 处理 JavaScript 和 TypeScript 文件,而不需要额外使用 loader。请记住,与ts-loader
相反,底层的@babel/plugin-transform-typescript
插件不执行任何类型检查。
ts-loader
使用 TypeScript 编译器tsc
,并依赖于tsconfig.json
配置。 babel-loader并不会执行类型检查,所以不会依赖tsconfig.json配置
我们可以修改webpack.config.ts文件验证一下~
diff
import * as path from 'path';
import * as webpack from 'webpack';
const config: webpack.Configuration = {
mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true
},
module: {
rules: [
+ {
+ test: /.tsx?$/,
+ use: 'ts-loader',
+ exclude: /node_modules/,
+ },
- {
- test: /.tsx?$/,
- use: {
- loader: 'babel-loader',
- options: {
- presets: ['@babel/preset-env', '@babel/preset-typescript']
- }
- },
- exclude: /node_modules/,
- },
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
}
};
export default config;
安装ts-loader
css
npm install --save-dev ts-loader
运行npm run build,不出所料,报错了。。。
这个时候我们需要在项目根目录下添加tsconfig.json文件
bash
touch tsconfig.json
tsconfig.json 添加配置项
json
{
"compilerOptions": {
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
自信运行npm run build,结果还是报错,(这才是我们搭建脚手架的常态,我尽量还原开发者每一个细节步骤)
如果想在 TypeScript 中继续使用像
import _ from 'lodash';
的语法,让它被作为一种默认的导入方式,需要在 tsconfig.json 中设置"allowSyntheticDefaultImports" : true
和"esModuleInterop" : true
。并且module
选项的值为commonjs
,否则 webpack 的运行会失败报错,因为ts-node
不支持commonjs
以外的其他模块规范。
我们更改我们的tsconfig.json文件
diff
{
"compilerOptions": {
"noImplicitAny": true,
- "module": "es6",
+ "module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
}
}
再次运行npm run build, 结果为successfully, god job!!!
ok,我们关于webpack对于ts的解析打包基本就over了,接下来我们来配置一下开发环境吧,这个时候需要plugins小姐姐登场了,真的是即插即用哦~
我们先在根目录下添加一个index.html模版文件
bash
touch index.html
安装html-webpack-plugin
css
npm install --save-dev html-webpack-plugin
更改模版index.html的title,通过模版语法获取配置文件中的变量
webpack.config.ts 添加配置
diff
import * as path from 'path';
import * as webpack from 'webpack';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
const config: webpack.Configuration = {
mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true
},
module: {
rules: [
{
test: /.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Hello World',
+ template: 'index.html'
+ })
+ ],
};
export default config;
执行npm run build
我们在开发中总不能每次改完代码都去重新打包一次吧,所以我们需要添加热更新。
安装 webpack-dev-server
css
npm install --save-dev webpack-dev-server
如果使用版本低于 v4.7.0 的 webpack-dev-server,还需要安装以下依赖 npm install --save-dev @types/webpack-dev-server
webpack.config.ts
diff
import * as path from 'path';
import * as webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
+import { Configuration as DevServerConfiguration } from 'webpack-dev-server';
+const config: webpack.Configuration & { devServer?: DevServerConfiguration } = {
mode: 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true
},
module: {
rules: [
{
test: /.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hello World',
template: 'index.html'
})
],
+ devtool: 'source-map',
+ devServer: {
+ static: './dist',
+ host: '0.0.0.0',
+ port: 9527,
+ open: true,
+ hot: true
+ },
};
export default config;
package.json
diff
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack",
+ "start": "webpack server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/preset-env": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@types/node": "^20.9.4",
"@types/webpack": "^5.28.5",
"babel-loader": "^9.1.3",
"html-webpack-plugin": "^5.5.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
我们添加一下tsx组件和组件的样式文件
bash
cd src && mkdir Components && cd Components && touch Hello.tsx && touch index.scss
安装react和react-Dom及其类型声明依赖
使用
@types/
前缀表示我们额外要获取React和React-DOM的声明文件。 通常当你导入像"react"
这样的路径,它会查看react
包; 然而,并不是所有的包都包含了声明文件,所以TypeScript还会查看@types/react
包。
scss
npm install --save react react-dom @types/react @types/react-dom
后续我们讲解babel时在详细解释 dependencies和devDependencies两者的真正区别(不是我们平常浅显的认为开发依赖和生产依赖)
在Hello.tsx组件中添加测试代码:
typescript
import * as React from "react";
export interface HelloProps { compiler: string; framework: string; }
export const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!</h1>;
将src/index.ts入口文件改为index.tsx后缀(webpack.config.ts入口那里也要同步修改),并添加如下代码:
javascript
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Hello } from "./Components/Hello";
ReactDOM.render(
<Hello compiler="TypeScript" framework="React" />,
document.getElementById("app")
);
重新运行npm run start
继续在Hello.tsx文件中添加样式,并在Components/index.scss添加样式代码:
src/Components/Hello.tsx
diff
import * as React from "react";
+import './index.scss'
export interface HelloProps { compiler: string; framework: string; }
export const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!</h1>;
src/Components/index.scss
css
$bgColor: #9cea18;
$fontColor: #fff;
html,body{
background-color: $bgColor;
color: $fontColor;
}
保存代码后,不出意外,又报错了,报错提示我们处理不了这种文件类型。
结合我们上面处理ts、tsx类型文件,很容易想到需要添加关于css类型的loader 安装相关依赖
lua
npm install --save-dev css-loader postcss-loader postcss postcss-preset-env sass-loader sass mini-css-extract-plugin
webpack.config.ts添加配置
diff
import * as path from 'path';
import * as webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { Configuration as DevServerConfiguration } from 'webpack-dev-server';
+import MiniCssExtractPlugin from 'mini-css-extract-plugin';
const config: webpack.Configuration & { devServer?: DevServerConfiguration } = {
mode: 'development',
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true
},
module: {
rules: [
{
test: /.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
+ {
+ test: /.s[ac]ss$/i,
+ use: [
+ // 将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载
+ MiniCssExtractPlugin.loader,
+ // 将 CSS 转化成 CommonJS 模块
+ {
+ loader: 'css-loader'
+ },
+ //处理css3不同浏览器兼容性,依赖package.json中的browserslist或者项目根目录下的.browserslistrc文件中浏览器版本配置
+ {
+ loader: 'postcss-loader',
+ options: {
+ postcssOptions: {
+ plugins: [
+ 'autoprefixer',
+ 'postcss-preset-env',
+ ],
+ },
+ },
+ },
+ // 将 Sass 编译成 CSS
+ 'sass-loader',
+ ],
+ },
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hello World',
template: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/[name].css'
})
],
devtool: 'source-map',
devServer: {
static: './dist',
host: '0.0.0.0',
port: 9527,
open: true,
hot: true
},
};
export default config;
package.json
perl
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
+ "browserslist": [
+ "> 0.001%",
+ "last 10 version",
+ "not dead"
+ ],
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack",
"start": "webpack server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/preset-env": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@types/node": "^20.9.4",
"@types/webpack": "^5.28.5",
"babel-loader": "^9.1.3",
"css-loader": "^6.8.1",
"html-webpack-plugin": "^5.5.3",
"mini-css-extract-plugin": "^2.7.6",
"postcss": "^8.4.31",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.3.0",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"dependencies": {
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Hello.tsx中添加测试代码
diff
import * as React from "react";
import './index.scss'
import {Simulate} from "react-dom/test-utils";
import dragOver = Simulate.dragOver;
export interface HelloProps { compiler: string; framework: string; }
export const Hello = (props: HelloProps) => {
return (
<>
<h1>Hello from {props.compiler} and {props.framework}!</h1>
+ <div className='box'></div>
</>
)
};
index.scss添加需要兼容浏览器一些css3属性
diff
$bgColor: #9cea18;
$fontColor: #fff;
html,body{
background-color: $bgColor;
color: $fontColor;
+ @keyframes mymove {
+ 0% {top:0;}
+ 25% {top:200px;}
+ 50% {top:100px;}
+ 75% {top:200px;}
+ 100% {top:0;}
+ }
+ .box {
+ width: 100px;
+ height: 100px;
+ background-color: #ffe600;
+ animation: mymove 10s infinite linear;
+ position: absolute;
+ transform: rotate(45deg);
+ }
}
执行npm run start
我们如果想还原回用babel-loader处理tsx文件还需要添加一个依赖
bash
npm install -D @babel/preset-react
webpack.config.ts
diff
...
{
test: /.tsx?$/,
use: {
loader: 'babel-loader',
options: {
+ presets: ['@babel/preset-env', "@babel/preset-react", '@babel/preset-typescript']
}
},
exclude: /node_modules/,
},
...
执行npm run build
我们之前一直关注运行打包成功就好了,现在我们看一下我们打完包的大小
好家伙,就写了这么几行测试代码,包就有1.14MB这么大,我们发现我们react和react-dom都没有做externals处理,如果我们开发一个library库会非常关注这个配置 webpack.config.ts
css
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
可以看到打包体积的优化效果非常的显著
但是这样配置我们目前并没有拆分development和production的webpack.config.ts文件,我们先通过NODE_ENV变量来判断一下
安装cross-env
sql
npm install -D cross-env
pacakge.json添加配置
diff
{
...
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
+ "build": "cross-env NODE_ENV=production webpack",
+ "start": "cross-env NODE_ENV=development webpack server"
},
...
}
webpack.config.ts 添加环境判断代码
diff
import * as path from 'path';
import * as webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import {Configuration as DevServerConfiguration} from 'webpack-dev-server';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
+const isProd = process.env.NODE_ENV && process.env.NODE_ENV.toLowerCase() === 'production';
const config: webpack.Configuration & { devServer?: DevServerConfiguration } = {
- mode: 'development',
+ mode: isProd ? 'production' : 'development',
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true
},
module: {
rules: [
// {
// test: /.tsx?$/,
// use: 'ts-loader',
// exclude: /node_modules/,
// },
{
test: /.tsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', "@babel/preset-react", '@babel/preset-typescript']
}
},
exclude: /node_modules/,
},
{
test: /.s[ac]ss$/i,
use: [
// 将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载
MiniCssExtractPlugin.loader,
// 将 CSS 转化成 CommonJS 模块
{
loader: 'css-loader'
},
//处理css3不同浏览器兼容性
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
'postcss-preset-env',
],
},
},
},
// 将 Sass 编译成 CSS
'sass-loader',
],
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hello World',
template: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/[name].css'
})
],
devtool: 'source-map',
devServer: {
static: './dist',
host: '0.0.0.0',
port: 9527,
open: true,
hot: true
},
+ externals: isProd ? {
+ 'react': 'React',
+ 'react-dom': 'ReactDOM'
+ }: {}
};
export default config;
配置了externals后,我们需要手动引入相关包的cdn文件或者用插件html-webpack-externals-plugin来配置,我们这里为了省事,直接在模版index.html中引入cdn
diff
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
+<script src="https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.production.min.js">+</script>
+<script src="https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
</body>
</html>
npm run build后,我们直接打开dist/index.html
可以看到通过cdn引入的react和react-dom依赖成功引入。
更多关于图片等静态资源处理的配置可以参考以下大佬文档
关于webpack的基础配置,我们讲解的还是比较细致的,这里主要是为了照顾对于前端工程化相对小白的jy们,希望大家一起学习成长~
二、pnpm
简介:pnpm 一个快速并且节省磁盘空间的包管理工具🔧 pnpm官方文档
pnpm Fast, disk space efficient package manager.
技术背景:
使用 npm 时,依赖每次被不同的项目使用,都会重复安装一次。 而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:
如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么 pnpm update 时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。 所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。 因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
优势:
(1)节省磁盘空间(硬连接存储)软、硬连接概念
(2)创建一个非扁平的 node_modules 目录
npm、yarn在安装依赖时会将所有依赖拍平放到node_modules根目录下,这就会造成依赖提升,导致幽灵依赖,而pnpm默认则通过软链接(符号链接)将项目的直接依赖项添加到模块目录的根目录中
(3)极快的安装速度
pnpm 分三个阶段执行安装:
依赖解析。 仓库中没有的依赖都被识别并获取到仓库。 目录结构计算。 node_modules 目录结构是根据依赖计算出来的。 链接依赖项。 所有以前安装过的依赖项都会直接从仓库中获取并链接到 node_modules。
这种方式比npm、yarn将所有依赖解析、获取然后安装到node_modules中快的多
pnpm安装依赖过程简图 npm、yarn安装依赖过程简图
三个方面对比可以说是完胜,最最重要的是pnpm天然支持work-sapce概念,这对于我们后面monorepo架构非常友好。
配置
pnpm-workspace.yaml
pnpm-workspace.yaml
定义了 工作空间 的根目录,并能够使您从工作空间中包含 / 排除目录 。 默认情况下,包含所有子目录。
bash
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# all packages in subdirs of components/
- 'components/**'
# exclude packages that are inside test directories
- '!**/test/**'
即使使用了自定义目录位置通配符,根目录下的package目录也总是被包含.
pnpm 从命令行、环境变量和
.npmrc
文件中获取其配置。
.npmrc
pnpm config
命令可用于更新和编辑 用户和全局.npmrc
文件的内容。四个相关文件分别为:
- 每个项目的配置文件(
/path/to/my/project/.npmrc
)- 每个工作区的配置文件(包含
pnpm-workspace.yaml
文件的目录)- 每位用户的配置文件(
~/.npmrc
)- 全局配置文件(
/etc/npmrc
)所有
.npmrc
文件都遵循 INI-formatted 列表,包含key = value
参数。
.npmrc
文件中的值可能包含使用${NAME}
语法的环境变量。 也可以使用默认值指定环境变量。 运行${NAME-fallback}
,如果NAME
不存在,命令会输出fallback
。 运行${NAME-fallback}
,如果NAME
不存在或为空,命令会输出fallback
。
关于.npmrc比较关键的几个属性设置:
依赖提升设置
hoist
- 默认值: true
- 类型: boolean
当 hoist 为 true
时,所有依赖项都会被提升到 node_modules/.pnpm/node_modules
。 这使得 node_modules
所有包都可以访问未列出的依赖项。
Node 模块设置
node-linker
- 默认值:isolated
- 类型: isolated , hoisted , pnp
定义应该使用什么链接器来安装 Node 包。
-
isolated - 依赖项从虚拟存储
node_modules/.pnpm
中建立符号链接 -
hoisted - 创建一个没有符号链接的扁平的
node_modules
。 与 npm 或 Yarn Classic 创建node_modules
一致。 当使用此设置时,Yarn 的一个库用于提升。 使用此设置的正当理由:- 您的工具不适用于符号链接。 React Native 项目很可能只有在你使用提升的
node_modules
才能工作。 - 您的项目会被部署到 serverless 服务提供商。 一些 serverless 提供商(例如 AWS Lambda)不支持符号链接。 此问题的另一种解决方案是在部署之前打包您的应用程序。
- 如果你想用
"bundledDependencies"
发布你的包。 - 如果您使用 --preserve-symlinks 标志运行 Node.js。
- 您的工具不适用于符号链接。 React Native 项目很可能只有在你使用提升的
-
pnp - 没有
node_modules
。 Plug'n'Play 是一种 Yarn Berry 使用的创新的 Node 依赖策略。 当使用pnp
作为您的链接器时,建议同时将symlink
设置为false
。
symlink
- 默认值: true
- 类型:Boolean
当 symlink
设置为 false
时,pnpm 创建一个没有任何符号链接的虚拟存储目录。 与 node-linker=pnp
一起是一个有用的设置。
使用: 安装pnpm其他安装方式详见
npm install -g pnpm
常用命令:
npm 命令 | pnpm 等效 |
---|---|
npm install |
pnpm install |
npm i <pkg> |
[pnpm add <pkg> ] |
npm run <cmd> |
[pnpm <cmd> ] |
当你使用一个未知命令时,pnpm 会查找一个具有指定名称的脚本,所以
pnpm run lint
和pnpm lint
等价。 如果没有指定名称的脚本,那么 pnpm 将以 shell 脚本的形式执行该命令
常用特殊命令:
-w, --workspace-root
在工作空间的根目录中启动 pnpm ,而不是当前的工作目录。 过滤允许您将命令限制于包的特定子集。
-F, --filter
pnpm 支持丰富选择器语法,可以通过名称或关系选择包。
可通过 --filter
(或 -F
) 标志制定选择器:
xml
pnpm --filter <package_selector> <command>
三、babel配置 在webpack实践中我们已经了解过一个babel-loadr包的简要配置,接下来我们再了解一下其他babel包的配置。
什么是 Babel?
Babel 是一个 JavaScript compiler
Babel 是一个工具链,主要用于在当前和旧的浏览器或环境中,将 ECMAScript 2015+ 代码转换为 JavaScript 向后兼容版本的代码。以下是 Babel 可以做的主要事情:
- Transform syntax(转换语法特性)
- Polyfill features that are missing in your target environment (through a third-party polyfill such as core-js (目标环境中缺少的Polyfill功能,通过第三方Polyfill,如core-js)
- Source code transformations (源码转换)
个人理解就是将es2015+ 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。例如在代码中使用了 ES6 的箭头函数或者promsie、async等新语法特性,但是这种写法会在 IE 浏览器中报错,为了让代码能在IE中正常运行,就需要将代码编译成IE支持的写法。
开始撸代码,我们已经了解了pnpm,所以以后会直接使用pnpm来管理package
bash
cd Desktop && mkdir babel-demo
安装bebel核心依赖
bash
cd babel-demo && pnpm init
scss
pnpm add --save-dev @babel/core @babel/cli @babel/preset-env
配置 Babel
Babel 是可配置的!许多其他工具都有类似的配置:ESLint (.eslintrc
), Prettier (.prettierrc
)。
所有 Babel API 可选项 都可以配置。然而,如果选项中含有 JavaScript,你可能需要使用一个 JavaScript 配置文件。
你的使用场景是什么?
- 你正在使用一个单体式仓库?
- 你想要编译
node_modules
?
babel.config.json
推荐你使用!
- 你的配置仅适用于项目的单个部分吗?
.babelrc.json
推荐你使用!
后续我们会创建一个monorepo的单体式仓库,我们在项目的根目录中创建名为 babel.config.json
(需要 v7.8.0
及以上版本)的配置文件
arduino
touch babel.config.json
babel的配置文件主要包含预设和插件两项,
1.什么是插件
通过在 配置文件 中应用插件(或 预设),可以启用 Babel 的代码转换。
使用一个插件
如果插件在 npm 中,你可以传入插件的名字,Babel 会检查它是否安装在 node_modules
中。这将被添加到 plugins 配置项,该选项接受一个数组。
perl
{
"plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]
}
插件排序
排序对于插件中的每个访问者来说都很重要。
这意味着如果两次转译都访问了 "Program" 节点,则转译将按插件或预设的顺序执行。
- 插件在预设之前运行。
- 插件排序是从第一个到最后一个。
- 预设顺序是颠倒的(最后一个到第一个)。
若要指定选项,请传递一个以对象,其中键作为选项名称。如
json
{
"plugins": [
[
"transform-async-to-module-method",
{
"module": "bluebird",
"method": "coroutine"
}
]
]
}
2.什么是预设?
插件只对单个功能进行转换,当配置插件比较多时,就可以封装成预设(presets)以此来简化插件的使用,预设简单说就是一组原先设定好的插件,是一组插件的集合,比如 @babel/preset-react 包含以下插件:
比如 es2015 是一套规范,包含很多转译插件。如果每次要开发者一个个添加并安装,配置文件很长不说,
npm install
的时间也会很长,为了解决这个问题,babel 还提供了一组插件的集合。因为常用,所以不必重复定义 & 安装。
官方预设
我们为常见环境组合了几个预设:
- @babel/preset-env 用于编译 ES2015+ 语法
- @babel/preset-typescript 用于 TypeScript
- @babel/preset-react 用于 React
- @babel/preset-flow 用于 Flow
使用预设
在 Babel 配置中,如果预设在 npm 上,你可以传入预设的名称,Babel 将检查它是否已经安装在 node_modules
中。 这将添加到 预设 配置选项中,该选项接受一个数组。
json
{
"presets": ["a", "b", "c"]
}
预设的执行顺序是倒叙的,这是为了向后兼容,也就是执行时候的顺序是c -> b -> a;
如果指定可选项,插件和预设都可以通过将名称和选项对象包装在你的配置的一个数组内来指定选项。
json
{
"presets": [
[
"@babel/preset-env",
{
"loose": true,
"modules": false
}
]
]
}
我们了解完概念后,继续撸
bash
mkdir src && cd src && touch index.js
在package.json添加脚本执行命令
diff
{
"name": "babel-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "build": "babel src --out-dir lib",
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.5",
"@babel/preset-env": "^7.23.5"
}
}
现在我们的babel.config.json是个空的,我们执行一下脚本试一下
arduino
pnpm run build
不出意外,按照常理,哈哈哈报错了
报错说解析配置json文件出错了,那我们现在有两种解决办法,一种是直接删除掉我们之前创建的空的json文件,一种是先在文件中写一对大括号,以免解析json文件出错
再次运行pnpm run build,结果当然是成功了
我们在src/index.js中写一段测试脚本代码:
javascript
const app = (appName) => {
console.log(appName)
}
app('hello babel!')
代码中用了es2015+的语法箭头函数,const,我们先执行一下build,看下结果:
代码原样输出了,首先说明我们打包是成功的啊,为什么没有被编译成es5呢,是因为我们没有配置代码编辑目标运行环境和指定插件哈 我们在package.json中添加browserslist属性
diff
...
+ "browserslist": [
+ "> 0.001%",
+ "last 10 version",
+ "not dead"
+ ],
...
安装转换箭头函数的插件@babel/plugin-transform-arrow-functions
sql
pnpm add -D @babel/plugin-transform-arrow-functions
在babel.config.json添加plugins配置
diff
{
+ "plugins": ["@babel/plugin-transform-arrow-functions"]
}
运行pnpm run build
可以看到我们的代码已经被转换成es5了~~~
虽然我们已经掌握了基本的babel配置,但是es2015+那么多语法,我们总不能一个个plugins去添加吧,哎,这个时候babel大佬们就想到了一个超级牛的想法,那就是预设,也就是一套插件的集合,只需我们配置好预设那么就会自动根据你的代码去转换了,不用有那么沉的心智负担。
我们上面已经安装了@babel/preset-env,所以可以直接添加配置: babel.config.json
perl
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.22"
}
]
]
}
可以看到我们把plugins完全删除了,运行pnpm run build
我们在src/index.js中添加其他的es2015+的特性都可以转换成es5的代码
diff
const app = (appName) => {
console.log(appName)
}
app('hello babel!')
+class Person{
+ constructor(){
+ console.log('Hello Babel Person!!!')
+ }
+}
+const person1 = new Person();
+const asyncFun = async () => {
+ console.log(1)
+ const res = await Promise.resolve(2)
+ console.log(res);
+ console.log(3)
+}
+asyncFun()
运行pnpm run build,可以看到我们只配置了预设,babel就已经为我们加载了目标环境需要支持es6语法的ployfill,非常的方便 上面我们预设中配置了"useBuiltIns": "entry",我们将其改为:"useBuiltIns": "usage",重新打包看一下效果:
发现usage配置将全量的es6+ployfill都加载进来了,这不是我们想要的,这对于lib类型的包非常不友好。
由于useBuiltIns属性将在v7中删除,我们需要通过另一种方式完成按需加载ployfill
useBuiltIns
DANGER
This option was removed in v7.
安装@babel/plugin-transform-runtime
和@babel/runtime
css
pnpm add --save-dev @babel/plugin-transform-runtime
@babel/runtime包运行时也需要用到,所以安装到dependencies
sql
pnpm add @babel/runtime
将我们之前的@babel/plugin-transform-arrow-functions
包删除,并删除node_modules,重新install
diff
{
"name": "babel-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel src --out-dir lib"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.5",
- "@babel/plugin-transform-arrow-functions": "^7.23.3",
+ "@babel/plugin-transform-runtime": "^7.23.4",
"@babel/preset-env": "^7.23.5"
},
"dependencies": {
+ "@babel/runtime": "^7.23.5"
}
}
我们最常用的几个配置属性如下:
1.corejs
可选值: false
, 2
, 3
or { version: 2 | 3, proposals: boolean }
, 默认是false.
eg.['@babel/plugin-transform-runtime', { corejs: 3 }],
这个配置项所依赖的包如下
corejs option |
Install command |
---|---|
false |
pnpm add --save @babel/runtime |
2 |
pnpm add --save @babel/runtime-corejs2 |
3 |
pnpm add --save @babel/runtime-corejs3 |
注意proposals选项需要babel版本高于v7.4.0
2.useESModules
注意:
This option has been deprecated: starting from version
7.13.0
,@babel/runtime
'spackage.json
uses"exports"
option to automatically choose between CJS and ESM helpers.
翻译完就是这个属性将在7.13.0被丢弃,转而自动使用package.json
中的exports属性来决定是commonJS还是ESM模块。
3.version
默认情况下,transform-runtime 假定已安装 @babel/runtime@7.0.0
。如果您安装了更高版本的 @babel/runtime
(或其 corejs 对应版本,例如 @babel/runtime-corejs3
)或将其列为依赖项,则转换运行时可以使用更高级的功能。我们的版本是7.23.5,所以用corejs:3
可以使用以下命令转译您的代码
babel.config.json
perl
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3,
"version": "^7.23.5"
}
]
]
}
运行pnpm run build
可以看到引入的playfill减少了,这对我们来说非常的重要.
前面介绍的都是js文件的转译,那么对于jsx、ts、tsx如何进行转换的呢 我们添加@babel/preset-react
和@babel/preset-typescript
依赖包
scss
pnpm add --save-dev @babel/preset-react @babel/preset-typescript
添加react和react-dom依赖
csharp
pnpm add -S react react-dom
添加测试文件app.jsx、index.html
bash
cd src && touch app.jsx
返回项目根目录下创建index.html
bash
touch index.html
app.jsx
javascript
import React from 'react'
import ReactDOM from 'react-dom'
const app = ({appName}) => {
return (
<div>{appName}</div>
)
}
ReactDOM.render(
<Hello appName="react jsx" />,
document.getElementById('app')
);
index.html
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
package.json添加打包命令
diff
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel src --out-dir lib",
+ "build:jsx": "babel src/app.jsx --out-dir lib/app.js"
},
...
}
babel.config.json添加@babel/preset-react预设
perl
{
"presets": [["@babel/preset-env", {
"modules": "umd"
}], [
"@babel/preset-react",
{
"pragma": "dom", // 默认是 React.createElement(仅在经典的运行时中)
"pragmaFrag": "DomFrag", // 默认是 React.Fragment(仅在经典的运行时中)
"throwIfNamespace": false, // 默认是 true
"runtime": "classic" // 默认是 classic
// "importSource": "custom-jsx-library" // 默认是 react(仅在经典的运行时中)
}
]],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3,
"version": "^7.23.5"
}
]
]
}
执行pnpm run build
可以看到我们利用babel-cli打的包是umd模式,我们从打完的包的代码里可以发现,babel并不能将依赖包一起打包,它主要是转换语法,想要打完的包真正在浏览器运行,就需要借助webpack、vite等打包工具的能力了。
添加对tsx、ts支持,安装依赖
css
pnpm add --save-dev @babel/preset-typescript typescript
src添加main.ts测试代码
bash
cd src && touch main.ts
main.ts
typescript
const main = (name: string, age:number) => {
console.log(name, + '' + age)
}
let num:number = 1;
let nameStr:string = 'coder';
main(nameStr, num)
修改babel.config.json文件
diff
{
"presets": [
["@babel/preset-env",
{
"modules": "umd"
}],
[
"@babel/preset-react",
{
"pragma": "dom", // 默认是 React.createElement(仅在经典的运行时中)
"pragmaFrag": "DomFrag", // 默认是 React.Fragment(仅在经典的运行时中)
"throwIfNamespace": false, // 默认是 true
"runtime": "classic" // 默认是 classic
// "importSource": "custom-jsx-library" // 默认是 react(仅在经典的运行时中)
}
],
+ "@babel/preset-typescript"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3,
"version": "^7.23.5"
}
]
]
}
修改package.json文件
diff
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel src --out-dir lib",
"build:jsx": "babel src/app.jsx --out-dir lib/app.js",
+ "build:ts": "babel src/main.ts --out-dir lib/main --extensions .ts"
},
...
}
这里需要注意,babel-cli不能直接处理ts后缀文件,需要配置--extensions .ts
You will need to specify
--extensions ".ts"
for@babel/cli
&@babel/node
cli's to handle.ts
files.
执行pnpm run build:ts
但是控制台会提示我们Dynamic import can only be transformed when transforming ES modules to AMD, CommonJS or SystemJS.嘛意思啊
查阅官方文档,发现这意思就是动态引入只可以被编译成AMD、CMD和System模式,不支持umd格式,如果umd中用了动态引入,暂时babel是没法转译的,可以通过webpack来解决这个问题dynamic-imports
我们这里demo暂时将@babel/preset-env的modules配置删除,也就是auto
babel.config.json
perl
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"pragma": "dom", // 默认是 React.createElement(仅在经典的运行时中)
"pragmaFrag": "DomFrag", // 默认是 React.Fragment(仅在经典的运行时中)
"throwIfNamespace": false, // 默认是 true
"runtime": "classic" // 默认是 classic
// "importSource": "custom-jsx-library" // 默认是 react(仅在经典的运行时中)
}
],
"@babel/preset-typescript"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": 3,
"version": "^7.23.5"
}
]
]
}
重新运行pnpm run build:ts
添加tsx支持
bash
cd src && touch Hello.tsx touch script.tsx
Hello.tsx
typescript
import * as React from "react";
export interface HelloProps { compiler: string; framework: string; }
export const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!</h1>;
script.tsx
javascript
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Hello } from "./Hello";
ReactDOM.render(
<Hello compiler="TypeScript" framework="React" />,
document.getElementById("app") as HTMLElement
);
安装react、react-dom的类型声明包 @types/react 、@types/react-dom
sql
pnpm add -D @types/react @types/react-dom
package.json添加打包命令
diff
{
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "babel src --out-dir lib",
"build:jsx": "babel src/app.jsx --out-dir lib/app.js",
+ "build:tsx": "babel src/script.tsx --out-dir lib/script --extensions .tsx"
},
...
}
执行pnpm run build:ts
总结
经过5w多字的唠叨,配置讲解的非常详细了(自我感觉),我们已经将webpack、pnpm、babel的基本配置和使用都实战练习了一遍,还有很多配置没有演示到,大家要多查官方文档,期待和大家相遇!fighting~~~