7、自制脚手架
最后更新于:2022-04-02 06:09:13
在学习了Webpack基础后,查看别人写好的脚手架总是会一头雾水,后面就上网查各种资料,一边参考一边修改,整出了一套简易的[脚手架](https://github.com/pwstrick/pwu)(已上传至GiuHub和[npm](https://www.npmjs.com/package/pwu)上),借鉴了[Create React App](https://www.html.cn/create-react-app/docs/getting-started/)(CRA)的目录结构(如下所示),并做成了[命令行工具](https://github.com/pwstrick/pwu-cli)(已上传至GiuHub和[npm](https://www.npmjs.com/package/pwu-cli)上)。
~~~css
├── pwu --------------------------------------- 脚手架示例
│ ├── config -------------------------------- webpack配置目录
│ ├── ├── jest ------------------------------ Jest测试的配置目录
│ ├── ├── webpack.base.config.js ------------ 通用配置
│ ├── ├── webpack.dev.config.js ------------- 开发环境配置
│ ├── ├── webpack.prod.config.js ------------ 生产环境配置
│ ├── bin ----------------------------------- 命令行工具
│ ├── ├── pwu.js ---------------------------- 命令文件
│ ├── dist ---------------------------------- 输出目录
│ ├── ├── css ------------------------------- 样式
│ ├── ├── img ------------------------------- 图像
│ ├── ├── js -------------------------------- 脚本
│ ├── public -------------------------------- 模板目录
│ ├── ├── index.html ------------------------ 模板页面
│ ├── src ----------------------------------- 源文件目录
│ ├── ├── __tests__ ------------------------- 测试目录
│ ├── ├── component ------------------------- 组件目录
│ ├── ├── font ------------------------------ 字体目录
│ ├── ├── img ------------------------------- 图像目录
│ ├── ├── index.js -------------------------- 入口文件
│ ├── ├── index.scss ------------------------ 全局样式
│ ├── package.json -------------------------- 管理依赖的包
│ ├── package-lock.json --------------------- 管理包的版本号和来源
│ ├── postcss.config.js --------------------- 后处理器配置文件
│ ├── tsconfig.json ------------------------- TypeScript配置文件
│ ├── .eslintrc ----------------------------- ESLint配置文件
│ ├── .eslintignore ------------------------- ESLint忽略的文件和目录
│ ├── .gitignore ---------------------------- Git忽略的文件和目录
~~~
## 一、通用配置
**1)入口和出口**
在通用配置中包含两个环境都需要的参数,例如入口和出口,如下所示。[path](http://nodejs.cn/api/path.html)是Node.js中的路径模块[path.resolve()](http://nodejs.cn/api/path.html#path_path_resolve_paths)用于解析绝对路径,[\_\_dirname](http://nodejs.cn/api/globals.html#globals_dirname)可读取当前模块的目录名。
~~~
const path = require("path");
module.exports = {
entry: {
index: "./src/index.js"
},
output: {
path: path.resolve(__dirname, "../dist"),
publicPath: "/"
}
};
~~~
[publicPath](https://webpack.docschina.org/configuration/output/#output-publicpath)指定静态资源的基础路径,公式如下。
~~~
静态资源最终路径 = output.publicPath + 加载器或插件的配置路径
~~~
假设html元素的背景是一条相对路径,那么最后生成的路径将会是“/img/lake.png”,其中配置的输出目录是“img”。
~~~css
html {
background: url("../../../public/img/lake.png") no-repeat;
}
/* 生成的背景路径 */
html {
background: url("/img/lake.png") no-repeat;
}
~~~
在CRA的webpack.config.js配置文件中,也有对publicPath的配置,如下所示,生产和开发环境会有对应的值。
~~~
const publicPath = isEnvProduction ? paths.servedPath : isEnvDevelopment && '/';
~~~
**2)加载器**
在加载器中,会添加脚本([babel-loader](https://webpack.docschina.org/loaders/babel-loader/))、样式([css-loader](https://webpack.docschina.org/loaders/css-loader/)、[postcss-loader](https://webpack.docschina.org/loaders/postcss-loader/)和[sass-loader](https://webpack.docschina.org/loaders/sass-loader/))、图像([url-loader](https://webpack.docschina.org/loaders/url-loader/))以及字体([file-loader](https://webpack.docschina.org/loaders/file-loader/))。
~~~
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/
},
{
test: /\.(sass|scss)$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader"
]
},
{
test: /\.(jpg|png|gif)$/,
use: {
loader: "url-loader",
options: {
name: "[name].[ext]",
outputPath: "img/",
limit: 8192
}
}
},
{
test: /\.(eot|ttf|svg|woff|woff2)$/,
use: {
loader: "file-loader",
options: {
name: "[name]_[hash].[ext]",
outputPath: "font/"
}
}
}
]
}
};
~~~
在解析样式的配置中,使用了四个加载器,后声明的先执行。[Babel](https://www.babeljs.cn/repl)的配置信息写到了package.json文件中,新建一个babel字段,useBuiltIns的值为usage,表示自动加载源码所需的Polyfill。
~~~
"babel": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"ie": 11,
"chrome": 49
},
"corejs": "2",
"useBuiltIns": "usage"
}
],
"@babel/preset-react"
]
}
~~~
postcss-loader又称为CSS后处理器,常用来提升浏览器兼容性,它有许多配套插件(例如[autofix](https://github.com/browserslist/browserslist#readme)),这些插件的配置被放在单独的postcss.config.js文件中,如下所示。
~~~
module.exports = {
plugins: [require("autoprefixer")()]
};
~~~
在执行时,postcss-loader会建议将浏览器的信息放在package.json中,新建一个browserslist字段,如下所示。
~~~
"browserslist": [
"last 5 version",
">1%",
"ie >=8"
]
~~~
MiniCssExtractPlugin.loader引用的是[mini-css-extract-plugin](https://webpack.docschina.org/plugins/mini-css-extract-plugin/)插件的加载器,该插件能从JS文件中提取CSS样式,保存到单独的CSS文件中。
url-loader和file-loader中的outputPath属性用于配置输出目录。图像中的limit属性的值是8192,以字节为单位,相当于8kb,如果图像尺寸小于该值,那就将其转换成Base64格式,嵌入到文件中,减少HTTP请求。字体文件的名称还会加上唯一标识的hash值,生成的名称如下所示。
~~~
iconfont_7346d960c4ad96f1ea8d5a8834fab00f.ttf
~~~
**3)插件**
MiniCssExtractPlugin插件的作用前面已提过,其中chunkFilename参数会在动态导入时用到。
~~~
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[hash].css",
chunkFilename: "css/[id].[hash].css"
})
]
~~~
## 二、开发环境配置
在开发环境中,需要引入通用配置,再利用[webpack-merge](https://webpack.docschina.org/guides/production/)合并,如下所示。[mode](https://www.webpackjs.com/concepts/mode/)字段用于告知webpack使用相应模式的优化。输出的文件名称也包含hash,但只会提取前8个字符。
~~~
const base = require('./webpack.base.config.js');
const merge = require('webpack-merge');
module.exports = merge(base, {
mode: "development",
output: {
filename: "js/[name].[hash:8].bundle.js"
}
});
~~~
**1)webpack-dev-server**
开启基于Node.js的本地服务器:[webpack-dev-server](https://webpack.docschina.org/configuration/dev-server/)。
~~~
devServer: {
contentBase: path.resolve(__dirname, "../dist"),
open: true, //自动打开浏览器
port: 4000, //端口号
compress: true, //启用gzip压缩:
useLocalIp: true, //使用本机IP
hot: true //开启热更新
}
~~~
**2)Source Map**
通过Source Map追踪错误或警告在源文件中的原始位置,以便调试,可配置[devtool](https://webpack.docschina.org/configuration/devtool/)实现,如下所示。
~~~
devtool: "source-map"
~~~
再添加webpack的[HotModuleReplacementPlugin](https://webpack.docschina.org/plugins/hot-module-replacement-plugin/)插件,如下所示。
~~~
const webpack = require('webpack');
module.exports = merge(base, {
plugins: [
new webpack.HotModuleReplacementPlugin()
]
});
~~~
**3)HtmlWebpackPlugin**
[HtmlWebpackPlugin](https://webpack.docschina.org/plugins/html-webpack-plugin/)插件能根据模板生成一个HTML文件,还能自动引入所需的bundle文件。模板文件被放置在public目录中,如下所示。
~~~html
脚手架示例
~~~
具体配置如下,[inject参数](https://github.com/jantimon/html-webpack-plugin#options)用于指定脚本注入位置,例如body元素的底部。
~~~
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = merge(base, {
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
inject: "body"
})
]
});
~~~
**4)脚本命令**
在package.js文件的scripts字段中,声明了start命令,开启本地服务器,并实时重载脚本。
~~~
{
"scripts": {
"start": "webpack-dev-server --config ./config/webpack.dev.config.js"
}
}
~~~
## 三、生产环境配置
生产环境比较注重性能,因此需要做很多优化配置,例如压缩、代码分离等,mode采用production优化模式,如下所示。
~~~
module.exports = merge(base, {
mode: "production",
output: {
filename: 'js/[name].[chunkhash:8].bundle.js'
}
}
~~~
**1)optimization**
首先优化的是代码分离,也就是将稳定不变的模块(例如react、react-dom等)抽取成一个单独的文件,splitChunks参数的配置可参考[SplitChunksPlugin](https://webpack.docschina.org/plugins/split-chunks-plugin/)插件。
~~~
module.exports = merge(base, {
optimization: {
splitChunks: {
chunks: "all",
minSize: 30000,
maxSize: 0,
minChunks: 1,
cacheGroups: {
vendors: {
test: /node_modules/,
name: "vendor",
enforce: true
}
}
}
}
});
~~~
cacheGroups是优化的关键,它是一个缓存组(属性如下所示),vendors会筛选从node\_modules目录下引入的模块。
(1)test:一个字符串、正则或函数,模块的匹配条件。
(2)name:拆分出的chunk(块)的名字。
(3)enforce:当为true时,可忽略minSize、minChunks、maxAsyncRequests和maxInitialRequests选项。
(4)priority:打包的优先级。
接下来优化的是压缩,配置到minimizer选项中。[UglifyjsWebpackPlugin](https://webpack.docschina.org/plugins/uglifyjs-webpack-plugin/)插件会使用使用UglifyJS去压缩JavaScript代码。[OptimizeCssAssetsPlugin](https://github.com/NMFR/optimize-css-assets-webpack-plugin)插件用于压缩CSS文件。
~~~
const UglifyjsWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = merge(base, {
optimization: {
minimizer: [
new UglifyjsWebpackPlugin(),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require("cssnano"),
cssProcessorPluginOptions: {
preset: ["default", { discardComments: { removeAll: true } }]
},
canPrint: true
})
]
}
});
~~~
**2)插件**
生产环境也需要模板插件,只不过要配置minify选项,如下所示,去除注释和空格。
~~~
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
inject: "body",
minify: {
removeComments: true,
collapseWhitespace: true
}
})
~~~
[CleanWebpackPlugin](https://github.com/johnagan/clean-webpack-plugin)插件可清除输出目录中的文件。
~~~
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = merge(base, {
plugins: [
new CleanWebpackPlugin()
]
});
~~~
偶尔会出现图45中的错误,目前还没找出原因。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2c/5b/2c5ba7d7b16727bb652e9058d908b372_1922x613.png =800x)
图 45
**3)脚本命令**
在package.js文件的scripts字段中,新增build命令,可在本地构建项目。
~~~
{
"scripts": {
"start": "webpack-dev-server --config ./config/webpack.dev.config.js",
"build": "webpack --config ./config/webpack.prod.config.js"
}
}
~~~
## 四、TypeScript
若要支持[TypeScript](https://webpack.docschina.org/guides/typescript/),那么必须安装相应的模块以及加载器,命令如下。
~~~
npm install --save-dev typescript ts-loader
~~~
在webpack的通用配置中,添加如下字段,[resolve](https://webpack.docschina.org/configuration/resolve/)的extensions属性能够在引入模块时不带扩展。
~~~
module.exports = {
resolve: {
extensions: [".tsx", ".ts", ".js"]
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [ 'ts-loader' ],
exclude: /node_modules/
}
]
}
};
~~~
还得要添加tsconfig.json配置文件,如下所示,具体的字段说明可以[参考官方文档](https://www.tslang.cn/docs/handbook/compiler-options.html)。
~~~
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}
~~~
由于要使用react和react-dom,因此还需要安装它们的声明文件:[@types/react](https://www.npmjs.com/package/@types/react)和[@types/react-dom](https://www.npmjs.com/package/@types/react-dom)。并且使用了html-webpack-plugin插件,它的声明文件([@types/html-webpack-plugin](https://www.npmjs.com/package/@types/html-webpack-plugin))也得安装。
都安装好后,就能在tsx文件中使用JSX语法了,如下所示。
~~~
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './component/app/app';
function init() {
ReactDOM.render(React.createElement(App, null), document.getElementById('root'));
}
init();
~~~
在webpack中的通用配置中,可添加新的入口文件,如下所示。
~~~
module.exports = {
entry: {
index: "./src/index.ts",
index2: "./src/index.tsx"
}
}
~~~
## 五、ESLint
[ESLint](https://cn.eslint.org/)是目前流行的静态代码检测工具,它能建立一套代码规范,保证代码的一致性,并且还能避免不必要的错误。
**1)基础配置**
首先需要安装[ESLint](https://www.npmjs.com/package/eslint)和ESLint的[加载器](https://www.npmjs.com/package/eslint-loader),命令如下所示。
~~~
npm install --save-dev eslint eslint-loader
~~~
然后在通用配置中添加eslint-loader,如下所示。
~~~
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: [ 'babel-loader', 'eslint-loader'] ,
exclude: /node_modules/
},
{
test: /\.tsx?$/,
use: [ 'ts-loader', 'eslint-loader' ],
exclude: /node_modules/
}
]
}
};
~~~
接着在根目录中创建.eslintrc配置文件,如下所示,rules字段中可记录各种规则。
~~~
{
"rules": {
}
}
~~~
**2)规则**
现在运行脚手架会报错(如下所示),因为ESLint不能识别ES6语法。
~~~
1:1 error Parsing error: The keyword 'import' is reserved
~~~
为了避免该错误,需要安装[babel-eslint](https://www.npmjs.com/package/babel-eslint),并且修改.eslintrc文件。
~~~
{
"parser": "babel-eslint",
"rules": {
}
}
~~~
下面添加一条简单的[max-len](https://cn.eslint.org/docs/rules/max-len)规则(其它规则可参考[官方文档](https://cn.eslint.org/docs/rules/)),一行最长200,4个Tab字符的宽度,忽略尾部注释和行内注释。
~~~
{
"rules": {
"max-len": ["warn", 200, 4, { "ignoreComments": true }]
}
}
~~~
当超过该限制时,会显示下面的警告。
~~~
7:1 warning This line has a length of 292. Maximum allowed is 200 max-len
~~~
由于使用了React,因此还可以添加React的规则,安装[eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react),并修改.eslintrc文件。
~~~
{
"plugins": [
"react"
]
}
~~~
如果不想自己定义规则,那么可以直接使用网上开源的规则,例如[Airbnb](https://www.npmjs.com/package/eslint-config-airbnb)的[JavaScript编码规范](https://juejin.im/entry/56e8c0c1816dfa0051376758)。注意,Airbnb的标准包会依赖[eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import)、eslint-plugin-react和[eslint-plugin-jsx-a11y](https://www.npmjs.com/package/eslint-plugin-jsx-a11y)等插件。安装成功后,再次修改.eslintrc文件。
~~~
{
"extends": "airbnb"
}
~~~
重新运行脚手架,马上就会出现一大堆错误和警告,修改加载器(如下所示),使用--fix参数可以将它们减少很多。
~~~
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: [
'babel-loader',
{loader: 'eslint-loader', options: {fix: true}}
],
exclude: /node_modules/
},
{
test: /\.tsx?$/,
use: [
'ts-loader',
{loader: 'eslint-loader', options: {fix: true}}
],
exclude: /node_modules/
}
]
}
};
~~~
**3)pre-commit**
如果使用的版本控制系统是Git,那么可以在每次提交前检测ESLint的规则。当检测失败时,就能阻止提交。
[husky](https://www.npmjs.com/package/husky)是一个Git钩子工具,可以防止不良的git commit、git push等操作。[lint-staged](https://www.npmjs.com/package/lint-staged)可对暂存的Git文件执行指定的任务。注意,husky对Node和Git的版本有要求,前者要大于10,后者要大于2.13。
接下来修改package.json文件,添加husky和lint-staged字段,在lint-staged中配置ESLint检测以及需要检测的文件后缀。当检测失败时,会得到图46中的提示。
~~~
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint"
]
}
~~~
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/be/b9/beb9c9b3d38e95a44322aacf8b1fbedd_1136x803.png =600x)
图 46
## 六、Jest
Jest是Facebook开源的一个测试框架,曾经写过一篇[入门的教程](https://www.kancloud.cn/pwstrick/fe-questions/1414223)。要将Jest集成到[Webpack](https://doc.ebichu.cc/jest/docs/zh-Hans/webpack.html)中,首先得安装[Jest](https://www.npmjs.com/package/jest),安装完成后在package.json文件中添加一条脚本命令(如下所示),执行Jest并打印测试覆盖率。注意,生成的测试覆盖率信息默认会保存到coverage目录中。
~~~
"scripts": {
"test": "jest --coverage"
}
~~~
现在执行“npm test”,不会有任何结果,因为还没写测试脚本。Jest默认会测试\_\_tests\_\_目录和名称中包含spec或test的脚本文件(包括TypeScript文件),并且默认还会忽略node\_modules目录中的文件,配置项如下所示。
~~~
testMatch: [ '**/__tests__/**/*.js?(x)', '**/?(*.)(spec|test).js?(x)' ]
testPathIgnorePatterns: ["node_modules"]
~~~
在src目录中新增\_\_tests\_\_目录,并新建app.js,其代码如下所示,添加了一个用于演示的测试用例。
~~~
describe("my test case", () => {
test("one plus one is two", () => {
expect(1 + 1).toBe(2);
});
});
~~~
当在测试用例中使用ES6语法时(例如像下面这样引入组件),会提示错误,此时需要引入[babel-jest](https://www.npmjs.com/package/babel-jest)。而babel-jest在安装Jest时已经自动下载,因此不必再单独安装。
~~~
import { App } from '../component/app/app';
~~~
在package.json文件定义jest字段,并声明[transform](https://doc.ebichu.cc/jest/docs/zh-Hans/configuration.html#transform-object-string-string)选项,添加下面这条规则,就能避免报错。
~~~
"jest": {
"transform": {
"^.+\\.js$": "babel-jest"
}
}
~~~
Jest还有一些其它配置,在测试时能发挥重大作用。例如在使用样式对象时,将所有的className原样返回(例如styles.container === 'container'),这会便于快照测试。要实现这个功能,得安装[identity-obj-proxy](https://www.npmjs.com/package/identity-obj-proxy),并修改moduleNameMapper选项,如下所示。
~~~
"jest": {
"moduleNameMapper": {
"\\.(css|scss)$": "identity-obj-proxy"
}
}
~~~
当moduleNameMapper不能满足需求时,可以使用transform选项设定转换规则,如下所示。
~~~
"jest": {
"transform": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$": "/config/jest/fileTransformer.js"
}
}
~~~
fileTransformer.js文件位于配置目录的jest目录中,其作用就是返回文件的名称(如下代码所示),例如require('avatar.png')返回“avatar.png”。
~~~
const path = require('path');
module.exports = {
process(src, filename, config, options) {
return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
}
};
~~~
注意,之前使用了ESLint检测代码,因此测试用例也会被检测。如果不想执行ESLint,那么可以添加.eslintignore文件,内容如下所示,其中配置目录也被忽略了。
~~~
src/__tests__
config
~~~
## 七、命令行工具
之前曾写过一篇命令行工具的[简易教程](https://www.kancloud.cn/pwstrick/fe-questions/1627451)。目前的设想是将命令行工具从脚手架中分离出来,通过命令下载脚手架。
首先安装[ora](https://github.com/sindresorhus/ora)、[chalk](https://github.com/chalk/chalk)、[commander](https://github.com/tj/commander.js)和[download-git-repo](https://gitlab.com/flippidippi/download-git-repo)四个包,安装命令如下所示。
~~~
npm install --save ora chalk commander download-git-repo
~~~
ora是一个优雅的终端旋转器,chalk可为终端中的文字添加颜色,commander是一个编辑命令的工具,download-git-repo可下载GitHub上的仓库代码。下面是具体的命令,命令([pwu-cli](https://www.npmjs.com/package/pwu-cli))已上传到npm中,安装成功后,可以执行“pwu create demo”创建demo目录(如图47所示),并自动下载[pwu](https://github.com/pwstrick/pwu)仓库中的脚手架代码。
~~~
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const ora = require('ora');
const chalk = require('chalk');
const program = require('commander');
const download = require('download-git-repo');
program
.version('1.0.0', '-v, --version', '版本');
program
.command('create ')
.description('create a repository')
.action(name => {
const spinner = ora('开始下载脚手架');
spinner.start();
const destination = path.join(process.cwd(), name);
if(fs.existsSync(destination)) {
console.log(chalk.red('脚手架已存在'));
return;
}
download('github:pwstrick/pwu', destination, (err) => {
spinner.stop();
console.log(chalk.green('脚手架下载成功'));
});
});
program.parse(process.argv)
~~~
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/79/12/79120ce399c677d63c378eed86a587ba_717x268.gif =400x)
图 47
在发布到npm时,npm可根据.gitignore文件中的内容进行过滤,这样就能避免上传依赖的模块。
*****
> 原文出处:
[博客园-前端利器躬行记](https://www.cnblogs.com/strick/category/1472499.html)
[知乎专栏-前端利器躬行记](https://zhuanlan.zhihu.com/pwtool)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎浏览。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
';