前言
市场上基于 react 的脚手架很多,用起来也很方便,比如:create-react-app、ant design pro、next.js等,每个脚手架都有自己突出的地方,但大体用到的技术都差不多,要想真正理解脚手架的运行原理,就是亲自打造一个。本项目git地址
初始化
这里要用到 node.js ,没有的同学可以自行去下载安装。
创建目录,并初始化node工程。
$ mkdir empty-scaffold
$ cd empty-scaffold
$ npm init
然后一路enter,最后输入yes即可。新建的工程里只有一个 package.json 文件。
webpack 安装配置
第一步、安装webpack的配置:
$ npm i --save-dev webpack webpack-cli html-webpack-plugin terser-webpack-plugin webpack-dev-server webpack-merge
简单解释各个模块的作用:
- webpack-cli: webpack 的命令行接口
- html-webpack-plugin: 生成 HTML 模板,简易模板配置步骤
- webpack-dev-server: webpack 本地服务
- webpack-merge: 合并多个 wenpack 配置文件,由于要配置多个环境,所以需要合并多个配置文件 安装好 webpack 模块后,下面就是配置环境,我们先配置一个开发环境。
第二步、创建 webpack 存放配置文件的目录
在跟目录新建 webpack 文件夹,用来存放其配置文件,然后新建 webpack.base.config.js、webpack.dev.config.js、webpack.prod.config.js 文件,此时目录结构是:
|-- empty-scaffold
|-- package-lock.json
|-- package.json
|-- webpack
|-- webpack.base.config.js
|-- webpack.dev.config.js
|-- webpack.prod.config.js
第三步、简单配置 webpack.base.config.js
先简单配置 webpack.base.config.js 文件,使其可以运行即可:
const path = require('path');
module.exports = {
entry: path.join(__dirname, '../src/index.js'), // 入口文件,/src/index.js
}
第四步、配置 webpack.dev.config.js
const WebpackMerge = require("webpack-merge");
const baseWebpackConfig = require("./webpack.base.config");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = WebpackMerge.merge(baseWebpackConfig, {
// 指定构建环境
mode: "development",
plugins: [
// 配置输出的HTML
new HtmlWebpackPlugin({
filename: "index.html",
template: "public/index.html",
inject: true
}),
],
// 开发环境本地启动的服务配置
devtool: 'eval-source-map',
devServer: {
host: 'localhost',
port: 8089,
// 要求每次都返回HTML,不配置会出现can not GET/
historyApiFallback: true,
hot: true
}
});
上面的配置为本地开发环境的配置,主要功能是:
- 开启本地服务,指定端口为8089
- 配置输出的HTML
第五步、创建src文件夹,作为存放业务代码的地方
在跟目录创建src文件夹,然后创建 index.js 文件,作为打包的入口文件。
此时的目录结构:
|-- empty-scaffold
|-- package-lock.json
|-- package.json
|-- public
| |-- index.html
|-- src
| |-- index.js
|-- webpack
|-- webpack.base.config.js
|-- webpack.dev.config.js
|-- webpack.prod.config.js
第六步、创建启动命令
在 package.json 的 scripts 中,创建命令:
start为本地开发启动的命令build是打包项目的命令
{
// ...
"scripts": {
"start": "webpack-dev-server --config webpack/webpack.dev.config.js --open",
"build": "webpack --config webpack/webpack.prod.config.js",
"test": "echo "Error: no test specified" && exit 1"
}
// ...
}
第七步、启动本地开发环境
运行:
$ npm start
在 index.js 中,输入 console.log('hello),在浏览器中可以看到答应的字符串。
安装配置 React
安装 React
运行:
$ npm install --save react react-dom
此时还无法运行,react需要经过 babel 编译才能执行。下面安装 babel。
安装配置 babel
运行:
$ npm install --save-dev @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/plugin-transform-runtime @babel/runtime-corejs3
模块简单说明:
- babel-loader: 告诉 webpack 使用 babel 解析 jsx 或 tsx 文件
- @babel/core: 将 js 代码分析成 ast,方便各个插件分析语法进行相应处理,官方文档
- @babel/preset-env: preset 本身是预设的意思,它是一系列预设的合集,作用很多,简单的作用是允许我们使用很多js语法,比如es6的const、let、箭头函数等,详细用法可以看官网
- @babel/preset-react: 对 react 的 JSX 语法进行转化
- @babel/plugin-transform-runtime: 由于 babel 是以 polyfill(补丁)方式来处理一些特定的代码,不使用此包时通常直接引入全部补丁,导致代码体积变大;使用
@babel/plugin-transform-runtime可以使将补丁改为统一引入,缩减代码体积 下面将配置 webpack:
- 在跟目录创建 babel.config.js 文件,用来配置 babel
module.exports = {
presets: [ // 预设
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [ // 插件
[
'@babel/plugin-transform-runtime',
{
'corejs': 3
}
],
]
}
- 修改 webpack 配置,使其使用 babel-loader 处理 JSX 语法
修改 webpack.base.config.js :
const path = require('path');
const resolve = (link) => path.resolve(__dirname, link);
module.exports = {
entry: path.join(__dirname, '../src/index.jsx'),
module: { // 模块
rules: [
{
test: /.jsx$/,
use: [
{
loader: 'babel-loader', // 使用 babel-loader
}
],
include: [resolve('../src')],
exclude: /node_modules/
}
]
}
}
修改 index.js 为 index.jsx,修改里面代码:
import React, { useEffect } from 'react';
import ReactDom from 'react-dom';
const Index = () => {
useEffect(() => {
console.log('hello')
}, [])
return (
<div>HELLO</div>
)
}
ReactDom.render(
<Index />,
document.getElementById('root')
)
运行代码:
$ npm start
可以看到正常运行:
到此,简单的 react 脚手架就搭好了,但是里面还缺失很多常用功能,比如:css文件配置、less文件配置、图片等静态资源、lint、typescript等,下面将逐个安装配置。
loader 配置
安装加载各种 loader
运行:
$ npm install --save-dev css-loader style-loader url-loader post-loader less less-loader autoprefixer
重新配置 webpack.base.config.js
const path = require('path');
const lessRegex = /.less$/;
const lessModuleRegex = /.module.less$/;
const resolve = (link) => path.resolve(__dirname, link);
module.exports = {
entry: path.join(__dirname, '../src/index.jsx'),
module: {
rules: [
{
test: /.jsx$/,
use: [
{
loader: 'babel-loader',
}
],
include: [resolve('../src')],
exclude: /node_modules/
},
{
test: /.css$/,
exclude: /node_modules/,
use: [
// 注意loader生效是从下往上的
'style-loader',
'css-loader'
]
},
{
test: lessRegex,
exclude: lessModuleRegex,
use: [
'style-loader',
'css-loader',
'postcss-loader',
{
loader: 'less-loader',
},
]
},
{
test: lessModuleRegex,
use: [
'style-loader',
'css-loader',
'postcss-loader',
{
loader: 'less-loader',
},
]
},
{
test: /.(png|jpe?g|gif|svg)(?.*)?$/,
use: [{
loader: 'url-loader',
options: {
//1024 == 1kb
//小于10kb时打包成base64编码的图片否则单独打包成图片
limit: 10240,
name: path.join('img/[name].[hash:7].[ext]')
}
}]
},
{
test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
use: [{
loader: 'url-loader',
options: {
limit: 10240,
name: path.join('font/[name].[hash:7].[ext]')
}
}]
}
]
}
}
注意:有安装 postcss-loader,其作用是自动补全 css 前缀,比如:ranslate,谷歌中会补全为 -webkit-transition,但需要配置 postcss-loader 使其生效,方法类似babel,在跟目录新建 postcss.config.js,将下面代码复制进去即可
module.exports = {
plugins: [
require('autoprefixer')
]
};
在 src 下新建 global.less 进行测试随便写入 css 并在 index.jsx 中引入
目录结构
|-- empty-scaffold
|-- babel.config.js
|-- package-lock.json
|-- package.json
|-- postcss.config.js
|-- public
| |-- index.html
|-- src
| |-- global.less
| |-- index.jsx
|-- webpack
|-- webpack.base.config.js
|-- webpack.dev.config.js
|-- webpack.prod.config.js
测试现有代码
运行:
$ npm start
可以看到正常加载 .less 文件,并且 css 自动添加前缀。
安装配置 typescript
安装 typescript
$ npm install --save typescript
$ npm install --save-dev @babel/preset-typescript @types/react
配置 ts
- 修改 babel.config.js
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
'corejs': 3
}
],
]
}
- 修改 webpack.base.config.js,两处地方
// ...
module.exports = {
// ...
module: {
rules: [
{
test: /.(j|t)sx?$/, // 1. 修改此处
use: [
{
loader: 'babel-loader',
}
],
include: [resolve('../src')],
exclude: /node_modules/
}
// ...
]
},
resolve: { // 2. 添加这里代码
extensions: ['.ts', '.tsx', '.js', '.jsx'],
}
}
- 添加 typescript 配置文件 新建 tsconfig.json
{
"compilerOptions": {
/* Basic Options */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [
"dom",
"dom.iterable",
"esnext"
], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"sourceMap": true, /* Generates corresponding '.map' file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"isolatedModules": false, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"strict": true, /* Enable all strict type-checking options. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"paths": {
}
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}
OK,到此 typescript 配置完成,可修改 src 下的文件为 tsx 进行测试
此时目录结构
|-- empty-scaffold',
|-- babel.config.js',
|-- directoryList.md',
|-- package-lock.json',
|-- package.json',
|-- postcss.config.js',
|-- tsconfig.json',
|-- public',
| |-- index.html',
|-- src',
| |-- global.less',
| |-- index.jsx',
| |-- pages',
| |-- Home',
| |-- index.tsx',
|-- webpack',
|-- webpack.base.config.js',
|-- webpack.dev.config.js',
|-- webpack.prod.config.js',
配置生产环境
修改 webpack/webpack.pro.config.js
const path = require('path');
const WebpackMerge = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const baseWebpackConfig = require("./webpack.base.config");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = WebpackMerge.merge(baseWebpackConfig, {
// 指定构建环境
mode: "production",
// 输出位置
output: {
filename: "[name].bundle.js",
path: path.join(__dirname, './../dist'),
chunkFilename: "[chunkhash:8].js"
},
// 插件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: "index.html",
template: "public/index.html",
inject: true, // true:默认值,script标签位于html文件的 body 底部
hash: true, // 在打包的资源插入html会加上hash
minify: {
removeComments: true, //去注释
collapseWhitespace: true, //压缩空格
removeAttributeQuotes: true //去除属性引用
}
})
],
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false, // 不将注释提取到单独文件中
})
]
}
})
运行:
$ npm run build
会在跟目录下生产 dist 文件夹,即打包后的文件,可运行 index.html
最终目录结构
|-- empty-scaffold
|-- babel.config.js
|-- directoryList.md
|-- package-lock.json
|-- package.json
|-- postcss.config.js
|-- tsconfig.json
|-- dist
| |-- index.html
| |-- main.bundle.js
|-- public
| |-- index.html
|-- src
| |-- global.less
| |-- index.jsx
| |-- pages
| |-- Home
| |-- index.tsx
|-- webpack
|-- webpack.base.config.js
|-- webpack.dev.config.js
|-- webpack.prod.config.js
下节将引入React路由、ant design