代码地址 github.com/Maricaya/ad… , 有帮助请 star ~
背景
刚接触 react 的时候,大家都是通过 create-react-app 等脚手架来快速创建应用。当基本配置满足不了我们的特殊需求时(比如使用 sass),我们会修改 eject 出来的 webpack 配置文件。
面对各种配置项一脸懵逼,不知道怎么快速修改。其实,学习项目搭建与配置没有什么技巧,都是经验(套路)。
这篇文章会介绍使用 webpack 手动搭建一个 react 项目的套路,来看看我的踩坑经历吧。
工具
- Yarn
- webpack 4
- 环境:webpack-dev-server 3
- 语言:TypeScript 3
- 编辑器:WebStorm / VScode 等
步骤
npm 初始化
npm init -y
这一步创建 package.json
安装 webpack、TypeScript、React
打开 webpack 官网,找到安装命令。
yarn add webpack webpack-cli typescript --dev
yarn add react react-dom
yarn add @types/react @types/react-dom --dev
新建 lib/index.tsx
在这个文件中写入几行 TypeScript 代码。
console.log(123);
index.tsx 在浏览器中是不能运行的,需要 webpack 帮我们打包成浏览器可执行的 index.js。
webpack.config.js
在根目录新建 webpack.config.js。
格式
以下配置都写在 module.exports 对象内部。
module.exports = {}
mode
mode 是 webpack 4 中新增加的选项, 有两个可选值:production(生产环境)和 development(开发环境)。 mode 不可省略。 我们先配置成开发环境,后面有需要再修改。
mode: 'development'
entry
打包的入口文件。
entry: {
<key>: <value>
}
在这里只介绍对象形式,是因为这个是最完整的entry配置,其他形式只是它的简化而已。
对象中的每一对属性对,都代表着一个入口文件。因此多页面配置时,肯定是这种形式的 entry。
key 可以是字符串变量,对应输出文件的名称;也可以是路径,比如 'a/b/c',对应着文件的输出路径。 详细解释可以看这篇文章。
entry: {
index: './lib/index.tsx' // ./ 当前目录下
}
resolve
webpack在启动后会从配置的入口模块触发找出所有依赖的模块, resolve 配置 webpack 如何寻找模块对应的文件。
在导入语句没带文件后缀时,webpack 会自动带上后缀去尝试访问文件是否存在。 resolve.extensions 用于配置在尝试过程中用到的后缀列表。
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
}
module.rules
tsx
接下来配置解释 tsx 的 loader。那么问题来了,怎么知道哪些是最合适的 loader?
答案是没有标准 loader,只能是一个一个试。
把网上能找到的配置方式全部试一遍之后,我找到一个我认为最好的方式 awesome-typescript-loader
。
安装
yarn add awesome-typescript-loader --dev
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader'
}
]
}
scss
再配置翻译 scss 的模块,需要使用三个 loader,style-loader, css-loader,sass-loader
yarn add --dev style-loader css-loader sass-loader
可以看出 webpack 中 loader 的原则,一个loader只做一件事情
- sass-loader 将 icon.scss 翻译为 icon.css
- css-loader icon.css -> 对象
- style-loader 对象 ->
<style>css</style>
{
test: /\.s([ac])ss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}
配置完了输入、中间 loader,接下来我们来配置输出 output。
output
-
path 输出地址 我本来以为这里可以直接写绝对路径
__dirname + './dist'
,后来发现在 windows 上会报错。 因为在不同的操作系统上,路径表示方法不一样。- windows:
__dirname + '\\dist'
, - mac & linux:
__dirname + './dist'
。
- windows:
所以,我们直接使用 node.js 提供的方法 path.resolve(__dirname, 'dist')
,
这个 API 会根据操作系统的类型将两个目录连接起来。
-
library: 'adorable-react'
开发 npm 包的时候会用到这个命令,library 是指定用户使用require时的模块名。 这个模块在使用时,命令是require("adorable-react")
。 -
libraryTarget 表示库的输出格式是什么,输出模块规范。有这么几种选择 umd、amd、commonjs2。
一般都选 'umd',一种在所有模块定义下都可运行的方案。
下面问题来了,umd 到底是什么呢?
要知道 umd 是什么,我们需要先了解下前端打包的历史。
前端最开始是没有包管理系统的,所有的js代码都通过 script 标签一个一个加载。 这个阶段 js 包内重名的全局变量会互相影响。这时就需要打包工具,把变量作用域限制在局部。
前端程序员提出了 AMD 打包方案,Node.js 社区也提出了另一套打包规则 commonJS。
- AMD 比较著名的方案 require.js,我们的变量都定义到 define 函数中。
define (function () {
// 这里是我们的变量
var a = 1 ...
})
这个标准叫做 amd 异步模块定义。
- commonJS 差不多是同一时间,Node.js 社区也提出了另一套打包规则 commonJS。
var a = 1 // 只在当前文件有效
module.exports = { ... }
我们正在写的 webpack.config.js 也遵循这个规范。
commonJS 只在 Node.js 中使用,AMD 只在浏览器中使用。 导致你在使用时,必须区分代码是运行在客户端还是服务端。
这时,有人发明了一套统一的规则 UMD 统一模块定义。 UMD 会判断当前运行环境是什么,根据运行环境选择使用 commonJS 或者 AMD。
所以在这里,我们选择最完整的 UMD。
最后,完整的 output 代码如下:
const path = require('path')
// mo
output: {
path: path.resolve(__dirname, 'dist/lib'),
- library: 'adorable-react',
libraryTarget: 'umd'
}
配置 tsconfig.json
在根目录下创建 tsconfig.json,它是 TypeScript 的配置文件。
主要包含两块内容:
- 指定待编译的文件
- 定义编译选项
我们这篇文章主要讲 webpack 4.0 的配置,ts 的配置文件先略过,可以直接拷贝我的仓库里 tsconfig.json 文件。
尝试第一次打包
配置完这些基本规则之后,我们可以尝试用 npx webpack
打包啦。
打包后的文件是这样的:
根目录/dist/lib/index.js
— 这是 index.tsx 翻译之后的文件
根目录/lib/index.d.ts
— 包含了声明的所有类型
index.d.ts 放置的目录不对,生成的 *.d.ts 类型文件都应该放入dist目录中。
一顿搜索后,我在 tsconfig.json "compilerOptions"
中配置 "outDir": "dist"
。
再次打包,所有文件都放进了 dist 目录,第一次用 webpack 打包就完成啦。
目前 webpack 就做了一件事,把 ts、tsx 翻译成 js。
webpack-dev-server
接下来我们安装 webpack-dev-server,让代码能够自动编译,实现热更新。
yarn add webpack-dev-server --dev
npx webpack-dev-server
此时,访问 http://localhost:8080/index.js 我们可以看到编译后的文件。
随意改动 lib/index.tsx
中的文件,http://localhost:8080/index.js 会自动更新。
那么问题来了,webpack-dev-server 为什么编译这么快?什么是热更新?
速度快
整个的过程我们可以简化一下,webpack-dev-server 会启动一个小型服务器(称为 Bundle Server)。
Bundle Server就是一个服务器,会执行这些编译后的文件,让浏览器可以访问到。
webpack 打包好的文件 index.js 会传输给 Bundle Server,放在内存中,
等用户访问 index.tsx 时,服务器直接从内存中抛出index.js,所以编译很快。
热更新
在应用程序的开发环境,方便开发人员在不刷新页面的情况下,就能修改代码, 并且直观地在页面上看到变化的机制。
html-webpack-plugin
到目前为止,我还没有 html 文件,在根目录新建 html 入口文件 index.html。
下面问题来了,引入 js 的文件路径是什么?
只要 entry 下的文件名称修改,我们就需要手动修改 js 的路径。那为什么不自动生成路径呢?
安装插件 html-webpack-plugin
,根目录创建 index.html 文件,把 webpack 打包后的静态文件自动插入到 html 中。
yarn add --dev html-webpack-plugin
webpack.config.js 配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
title: "adorable-react", // 页面 title
template: "index.html"
})
],
把 index.tsx 的 ReactDom 挂载到 index.html 上。 index.tsx 中:
const div = document.createElement('div');
div.innerText = 'div';
document.body.appendChild(div);
externals
如果我们想引用一个库,但是又不想让webpack打包, 可以通过配置 externals 实现。
这个功能主要用在,创建一个 npm 库的时候。
比如:我们开发了一个 React 的组件库,里面引用了 react 和 react-dom。 但是我们没必要把 react 和 react-dom 打包起来,因为用户会在使用时会自己下载。
那么我们就可以 externals 的方式,将 react/react-dom 从我们的源代码中排出去。
externals: {
// 配置如何引入
react: {
// 运行在 Node.js 环境中,import * as React from 'react' 等价于 const react = require('react')
commonjs: 'react',
commonjs2: 'react',
// 使用require.js等加载,等价于 define(["react"], factory);
amd: 'react',
//在浏览器中使用,需要提供一个全局的变量'React',等价于 var React = (window.React) or (React);
root: 'React'
},
'react-dom': {
commonjs: 'react-dom',
commonjs2: 'react-dom',
amd: 'react-dom',
root: 'ReactDom'
}
}
webpack 配置区分开发、生产模式
看到这里,你已经完成了 webpack 所有基本配置!给自己鼓鼓掌吧!
不过目前 develop 模式和 production 模式都写在一起,难道每次打包都需要手动修改吗?
别急,webpack 为我们提供了自动切换方式。
将 webpack 文件分为 develop 模式和 production 模式,公共部分写入 webpack.config.js。
production 写入 webpack.config.prod.js,develop 写入 webpack.config.dev.js。
完整写法可以点击链接查看,这里这给出核心代码: webpack.config.prod.js
const base = require('./webpack.config')
// 使用 Object.assign
module.exports = Object.assign({}, base,{
mode: 'production'
})
配置 package.json scripts
我们在 package.json scripts 中配置好命令,直接使用 yarn xxx
就可以运行项目。
直接看最后的命令:
{
"script": {
"start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js"
}
}
开发模式命令 webpack-dev-server
,webpack 的配置是 webpack.config.dev.js
。
生产模式命令 webpack
,webpack 的配置是 webpack.config.prod.js
。
cross-env NODE_ENV=xxx
NODE_ENV=development
是我们写入的环境变量。
你可能也有这个疑问,webpack.dev.config.js 中不是有 mode 吗? 为什么要手动写入环境变量?
因为 webpack 处理的这个入口脚本文件及其引用的脚本文件都无法访问 webpack.dev.config.js 的属性, 所以手动写入环境变量,让所有文件可以访问到 process.env.NODE_ENV。
cross-env 插件的作用是:保证环境变量在各个平台都能添加成功。(在 unix、windows 上环境变量写法不一样)
安装方式
yarn add --dev cross-env
引入测试(选看)
引入 react 社区最流行的测试框架 jest,官网
测试配置不是重点,大家可以直接看仓库
这里简单说下配置:
yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
yarn add --dev ts-jest
创建 .babelrc
{ "presets": ["react-app"] }
配置 scripts test
"test": "cross-env NODE_ENV=test jest --config=jest.config.js --runInBand"
配置 Enzyme Enzyme 是用于 React 的 JavaScript 测试实用程序,可以更轻松地测试 React 组件。
yarn add --dev enzyme enzyme-adapter-react-16
创建 test 文件夹,写入配置,这里不赘述了,有兴趣直接看源码吧~
总结每个目录作用
终于完成了项目搭建,最后来看看每个目录的作用吧!
现在每个文件的功能我们都非常清楚,出了问题,我们也知道应该在哪里修改。
.
├── .babelrc // babel 配置
├── README.md
├── index.html // 首页
├── jest.config.js // jest 配置
├── dist // 最终代码
├── lib // 源代码
│ ├── __tests__
│ │ └── index.unit.tsx
│ ├── index.scss
│ └── index.tsx
├── package.json
├── test // test 配置
│ ├── __mocks__
│ │ ├── file-mock.js
│ │ └── object-mock.js
│ └── setupTests.js
├── tsconfig.json // ts 配置
├── tsconfig.test.json // ts 测试配置
├── tslint.json // 代码检测配置
├── webpack.config.dev.js // webpack 配置
├── webpack.config.js
├── webpack.config.prod.js
├── webpack.config.wheel.js
└── yarn.lock
恭喜你完成了项目搭建,接下来我们可以愉快地 coding 了!