从零开始搭建前端项目开发环境
1 写在最前
为何大多数人觉得搭建一个前端项目的开发环境很困难。首先,一个完整的开发环境需要依赖多个工具,每个工具又有不计其数的配置项,想要全部理解需要花费大量的时间。其次,一些官方文档中文翻译滞后,而且结构组织的也不是很好,没有从一个具体的项目出发讲解如何配置,只看 API 没有什么针对性。最后,好多国内相关博客要么没有标注工具的版本,导致按照步骤进行配置时会发现自己下载的和讲述的某些地方会不一致;要么只讲如何配置,不讲配置的原因,导致大家仍是云里雾里。不过所幸,大部分 CLI 实现了脚手架的功能,帮助快速生成项目,而不用了解工具的具体配置。
但是,作文里总会有个转折不是。当我们需要自己独立去创建一个项目的时候…该怎么办哖,Don’t be afraid,I’m here.
接下来会先介绍一下几个常用工具(Babel 7.13.0,Browserslist 4.16.6, ESLint 7.26.0, EditorConfig)的核心概念、安装和配置,最后会结合 TypeScript + SASS + webpack5 的项目来说如何将它们整合起来形成一个完整的前端项目开发环境。 Here we go.
2 Babel (7.13.0)
Babel 是一个 Javascript 编译器。主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容(backwards compatible)的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其它环境(如:node)中。
2.1 安装
1 | npm i -D @babel/core @babel/cli @babel/preset-env |
运行此命令之前要先初始化项目,新建项目文件夹 xxx , 在该文件夹下 运行:
1 | npm init |
尽量不要一路回车,建议按实际内容来。
2.1.1 @babel/core
主要用于将源代码(JS\TS)解析为 AST(抽象语法树)。
2.1.2 @babel/cli
用于从命令行编译文件。
2.1.3 @babel/preset-env
预设环境。源代码解析成 AST 后,还需要进行转换和生成,这是由插件来做的。@babel/preset-env(预设环境)是常用预设和插件的集合(babel-preset-es2015,babel-preset-es2016,babel-preset-es2017,babel-preset-latest,babel-preset-node5, babel-preset-es2015-node 等,@babel/preset-env 不支持 stage-x 插件)。
最初每年 EcmaScript 标准更新,都需要使用者手动添加最新年份的预设才能进行新语法的转换:
1 | "presets": [ |
后来改成了 babel-preset-latest,意思为最新的预设(包含了以往所有年份),不用每年都需要手动添加一把。最终 latest 也被废弃,变成了目前的 preset-env。开发人员可以在代码中直接书写已经正式发布的特性。不过,当 ES 更新时,肯定还需要更新一下 @babel/preset-env。
2.1.4 core-js
Babel 默认只转换新的 JavaScript 语法,如: 类、箭头函数、扩展运算(spread),而不转换新的 API ,如:Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(如:Object.assign)。不转码的 API 详细清单可以查看 definitions.js 文件。
core-js 提供了 es5、es6+的 polyfills(填充物,用于实现浏览器并不支持的原生 API 的代码,它将一个新的 API 引入到一个旧的环境中,仅靠旧环境中已有的技术手段实现),包括promises, symbols, collections, iterators, typed arrays等等。
这里不得不提一下该库和它的俄罗斯作者。该库一周的下载量比大家熟知的 Angular、 React、Vue 加一起的下载量还要多的多。就这样的一个库谁能想到它的作者竟然需要在命令行里 looking for a good job,后来该作者又因为骑摩托车撞死一人,伤一人,被判入狱 18 个月, 现在应该已经处于服刑期间了…
@babel/polyfill 已废弃。
2.2 配置
可以在三类文件中对 Babel 进行配置,在项目的根目录中创建 babel.config.*、.babelrc.* (* 可为 空,json, js, cjs 或 mjs) ,或在 package.json 中添加。
如果扩展名为 json,那一般的内容是:
1 | { |
如果扩展名为 js,则内容为:
1 | const presets = [ ... ]; |
在 package.json 中:
1 | { |
我们一般使用 babel.config.js 作为配置文件,因为在 JS 中可以写注释,方便理解(JSON 不方便)。其它工具的配置文件能采用 js 做扩展名的也尽量使用 .js。
在项目文件夹下创建 src 目录,新建 index.js。编写内容:
1 | const userInfo = new Map(); |
在 package.json 中添加:
1 | "scripts": { |
2.2.1 @babel/preset-env
最简设置如下(不建议,转换后会多出很多冗余代码,至少要配置一个 targets):
1 | module.exports = { |
在命令行中运行:
1 | npm run build |
就能在新生成的 dist 文件夹中看到 编译后的 index.js :
可以看到之前的 const 被转译成了 var。
下面对两个常用参数进行说明:
2.2.1.1 targets
设置编译代码的目标平台,可以是浏览器也可以是 node 环境。如不设置,会将所有 ES6+的代码编译为 ES5-。建议设置,这样可以按目标平台来决定是否进行转换,以避免增加不必要的补丁,减少打包后的代码体积。
常见设置如下:
1 | module.exports = { |
以上配置的意思是要兼容全球范围内使用量大于 1%的浏览器和它们最新的两个版本,加上 IE 9-11。这里既可以指定浏览器的版本,也可以通过查询的方式来确定要支持的浏览器。详情见Browserslist这一章节。
若设置 targets: ‘> 10%’ , 再运行一次 npm run build ,因为大于 10% 的浏览器只有谷歌,它是支持 const 的, 因此编译结果几乎原封未动:
建议使用 .browserslistrc 文件来定义 targets,这样其它工具(Autoprefixer、PostCSS、Stylelint 等)也能通过该配置获取到目标浏览器,从而做有针对性的处理。
2.2.1.2 useBuiltIns
是否内置兼容。如果设置,就可以不用在 HTML 中引入 polyfill 的 JS 文件来处理浏览器的兼容性问题了。常见设置如下:
1 | module.exports = { |
可选值包括:”usage” | “entry” | false。
false:默认值,即不引入 polyfills,不做浏览器兼容。
entry:根据配置的浏览器,引入这些浏览器不兼容的 polyfill。这个是在文件中已经明确写了 import “core-js” 或其中具体某个模块(如:import “core-js/es/array”)的情况下,babel 会根据 browserslist 自动添加指定浏览器不兼容的该模块下的所有 polyfill,无论当前代码是否需要。
usage:会根据配置的浏览器以及代码中用到的 API 自动添加 polyfill,实现了按需引入。一般使用这个配置。
只有当 useBuiltIns 的值为 entry 或 usage 时,”corejs” 这个选项才会起作用。这里如无特殊情况尽量指定 core-js 的版本为 3(默认为 2), 因为它有很多 2 没有的新特性: “corejs”: 3。
这里运行 npm run build, 运行结果为:
可以看出它在全局作用域下添加了 Map 变量,同时在 Object 原型中添加了 entries 方法。
babel 默认转出的是模块规范为 commonJS,只能在 Node 环境中使用,如果想在浏览器环境中使用,一般有两种方式:
1、 结合 rollup
2、 结合 webpack
我们一般使用 第二种方式,参看最后 webpack 实例。
2.2.2 @babel/plugin-transform-runtime
对于一般应用开发来说,直接使用上述的 polyfill 方案是比较方便的,但如果是开发工具、库的话,这种方案未必适合(由上图可以看出,polyfill 是添加自定义全局对象 或向对象的 prototype 上添加方法实现的)。使用 @babel/plugin-transform-runtime 这个插件就可以解决这个问题。
安装:
1 | npm i -D @babel/plugin-transform-runtime @babel/runtime-corejs3 |
配置:
1 | module.exports = { |
该插件无法读取 preset-env 的 targets 或者是 browserlist 中的目标平台参数,因此不会根据目标平台来决定是否进行转换和注入 polyfills,而是全转换和注入所有。以后可能会在 useBuiltIns 中增加一个参数 “runtime” 来代替该插件。可参见:https://github.com/babel/babel/issues/10133
再运行 npm run build,得到:
凡是 require 进来的模块都直接赋值给普通变量,不会对 Map 、 Object 等全局变量造成影响。
注:这里的和上边所有提到的“编译”一词更准确的说法应该是“转译(Transpile)”。
3 Browserslist (4.11.1)
Browserslist 是一个能够在不同的前端工具间共享目标浏览器的配置。看配置就知道当前项目支持的浏览器有哪些。它使用 Can I Use 的数据做查询。
在项目中添加 Browserslist,常用有两种方式(不能同时在 .browserslistrc 和 package.json 中配置,否则使用 Babel 转译的时候会报错):
- 在项目的根目录下添加 .browserslistrc 文件
1 | > 1% |
- 在 package.json 文件中增加 browserslist 节点
1 | { |
查询结果可通过 https://browserl.ist/ 来查看。
运行命令行:
1 | npx browserslist |
可查看当前项目目标浏览器列表。如果只是在 babel.config.* 文件中配置了 target 是检测不出来的(会优先使用),因此建议使用 单独的 .browserslistrc 来设置。
4 ESLint (7.26.0)
是一个可以查找并且修复 JavaScript(TypeScript)中错误的工具,目的是为了保证代码风格统一,避免出错。
4.1 概念
4.1.1 extends(扩展)
扩展里填的内容是包含了一系列规则的配置文件。这个一般不需要自己定义,因为有很多现成的:如 ESLint 自身的 eslint:recommended、eslint:all 和社区的 google、airbnb。
配置的模块名(npm 的包名)要为 eslint-config-xxx,在配置中可缩写为 xxx。
例:
4.1.2 plugins(插件)
extends 中是对 eslint 现有规则的一系列预设(开启或关闭),而 plugins 不仅可以定义预设,也可以自定义规则(比如自己创建一个 no-console2,区别于 ESLint 的 no-console),甚至可对不是 JavaScript 类型的文件(如 *ts,*.md,*.vue)扩展自己的规则。
插件的模块名一般为 eslint-plugin-xxx,在配置中可缩写为 xxx。
例:
4.1.3 rules(规则)
直接配置已有规则的开启、关闭。比如强制在 JavaScript 中使用单引号(”quotes”: [2, “single”])。规则定义中参数的设置说明:
“off” 或 0:关闭规则
“warn” 或 1:警告,不会影响程序执行
“error” 或 2:错误,程序不能执行
4.2 安装
4.2.1 针对 JavaScript
1 | npm i -D eslint eslint-config-airbnb-base eslint-plugin-import |
4.2.1.1 eslint-config-airbnb-base
包含了 airbnb 最基础(不包含 React 相关)的 JS 编码风格规则。
4.2.1.2 eslint-plugin-import
上边的插件依赖这个。😛
4.2.2 针对 TypeScript
1 | npm i -D eslint eslint-config-airbnb-typescript eslint-plugin-import @typescript-eslint/eslint-plugin |
4.2.2.1eslint-config-airbnb-typescript
Airbnb 风格的 TypeScript 支持。它将一些常见配置都加了进去,省下了好多工作量。
ps. 这是一个匈牙利布达佩斯技术和经济大学的学生做的,想想自己的大学生活都在做啥…
该插件包含了@typescript-eslint/parser(TypeScript 解析器),它调用@typescript-eslint/typescript-estree(通过在给定的源代码上调用 TypeScript 编译器,就是 npm i typescript -D 安装的那个,以产生 TypeScript AST,然后将该 AST 转换为 ESLint 期望的格式)。ESlint 默认的解析器叫 espree。
4.2.2.2 @typescript-eslint/eslint-plugin
与 @typescript-eslint/parser 结合使用时,运行 TypeScript 的分析规则。
4.3 配置
可使用 .eslintrc.( 可为 空,js, yaml, yml, json)或在 package.json 中配置,这里使用 .eslintrc.js 来进行配置。 一个项目中在不同的文件夹下可以有多个 .eslintrc. 配置文件,这样可以约束不同文件夹下的文件使用不同的风格,这一点和 editorConfig 一样。
4.3.1 针对 JavaScript
一般配置如下:
1 | module.exports = { |
4.3.2 针对 TypeScript
一般设置如下:
1 | module.exports = { |
最后要在 VS Code 中安装 ESLint 插件,配置相关参数,使之能够在文件保存时自动修复格式错误;WebStrom 中则需设置允许 ESLint(最新的 2020.1.1 版本中也能够在文件保存时自动修复错误)。
5 stylelint
5 EditorConfig
帮助在不同的编辑器或 IDE 上从事同一项目的多个开发人员保持一致的编码样式。
一个项目里可以有多个.editorconfig 分别放置在不同的文件夹中,当 VS Code 这类编辑器打开一个文件时,它会检查这个文件所在目录和它的父级文件夹(直到项目根目录或者是是某个文件夹中的 .editorconfig 里标识了 root = true 才会停止)中是否存在 .editorconfig。被打开的文件格式会以距当前文件最近的 .editorconfig 中的内容为准。
一般配置如下:
1 | # 告诉编辑器这是最顶层的(不要再向上找了) EditorConfig 文件 |
6 webpack (5.37.0)
webpack 是用于现代 JavaScript 应用程序的静态模块打包器。
当 webpack 处理应用程序时,它会在内部构建一个依赖关系图,该图映射项目所需的每个模块最终会生成一个或多个包。
6.1 概念
6.1.1 modules
webpack 中,无论是 JS 、CSS 还是图片等,总之一切皆模块。 有点像 RxJS,一切皆数据流。
模块间依赖的表述有很多种方式,如:import,require,define,@import,url(…), <img src=…> 等等。
6.1.2 Entry & Output
入口指示 webpack 应该使用哪个模块开始构建其内部依赖关系图。默认为: ./src/index.js。
出口告诉 webpack 在何处发出它创建的包文件以及如何命名这些文件。默认为: ./dist/main.js
6.1.3 Loaders
webpack 默认只能解析 JavaScript 和 JSON,可以通过添加 Loaders 来处理其他类型的文件。
6.1.4 Plugins
可以利用插件来执行更广泛的任务,例如打包优化,资产管理和环境变量的注入。
6.1.5 Mode
分 development、production、none 三种,每种都会对应一系列默认配置。
6.2 实战
接下来以初始化一个 TypeScript + SCSS 项目为例,介绍下 webpack5 的配置流程。
在开始之前先说下,为何没用 ts-loader 和 TSLint:
由于 TSLint 的性能不如 ESLint,再加上有很多热门的社区(React Hooks、Vue),都是通过 ESLint 来构建规则,因此,TypeScript 团队决定专注支持 ESLint。
Babel7 虽然不支持 TS 类型检查,但已经支持转译。 构建需要安装的插件、工具太多,能少一个就少一个,一个编译器既能支持 JS,又能支持 TS,为何不用。因此,感觉 ts-loader 的生命也不会太长了…
Node 环境要求至少为:10.13.0。
6.2.1 初始化项目
1 | npm init |
在命令行中填入项目相关信息,不建议一路回车…
6.2.2 安装 webpack
webpack 是最新的 webpack5。
1 | npm i -D webpack webpack-cli |
如果需要一个 web 服务器做调试和热更新,则需安装:
1 | npm i -D webpack-dev-server |
6.2.3 创建 .editorconfig
统一编码样式。
1 | # 告诉编辑器这是最顶层的(不要再向上找了) EditorConfig 文件 |
6.2.4 安装 TypeScript 编译器并配置
安装编译器的目的是为了配合 ESLint 做代码检查和自动修复。
1 | npm i -D typescript |
在项目根目录下创建 tsconfig.json,其中可定义入口文件以及编译的参数,用于将 TypeScript 转译为 JavaScript。
一定要设置,否则在 IDE 做语法校验的时候,新 JavaScript API(如: Object.fromEntries )会报错。
1 | { |
ESNext 指的是 TypeScript 支持的最新版本的 ES。它会随着 ES 版本的更新而自动更新,一劳永逸。
DOM 类型定义,允许在 TS 中直接写 window,document。
6.2.5 安装 ESLint 并配置
1 | npm i -D eslint eslint-config-airbnb-typescript eslint-plugin-import @typescript-eslint/eslint-plugin |
.eslintrc.js
1 | module.exports = { |
6.2.6 创建 .gitignore
提交 Git 服务器时,忽略的文件列表。使用 SVN 的就不需要这个了。
1 | .DS_Store |
6.2.7 创建 .browserslistrc
这里设置兼容 IE11。当前项目如果是作为库使用的话,该设置不会起作用。详情见:babel-plugin-transform-runtime
1 | > 1% |
6.2.8 安装 webpack loaders
所有 Loaders 都在 webpack.config.js 文件中的 module 节点中进行添加。
6.2.8.1 转译
6.2.8.1.1 babel-loader 及相关
webpack 本身只能打包(模块合并)而没有转译的能力,TS 转译成 JS 用的是 Babel ,并没有使用 TypeScript 编译器。
1 | npm i -D babel-loader |
babel.config.js:
1 | module.exports = { |
webpack.config.js 片段:
1 | module: { |
更多配置可见下方完整的配置。
当前项目将要作为库的话,还得安装:
1 | npm i -D @babel/plugin-transform-runtime @babel/runtime-corejs3 |
同时 babel.config.js 中的配置改成下面这个:
1 | module.exports = { |
6.2.8.2 样式
6.2.8.2.1 sass-loader
加载 SASS/SCSS 文件 并将其编译为 CSS。
1 | npm i -D sass-loader node-sass |
sass-loader 要求要安装 Dart Sass 或者是 Node Sass。弄过 NPM 下载下来的这两者只是个编译器。据说前者在 node 环境中性能比后者要差,因此一般都会使用 node-sass。
webpack.config.js 一般配置如下:
1 | // 执行顺序:从右到左 |
6.2.8.2.2 postcss-loader
PostCSS是 CSS 语法转换的工具。它提供 API 来对 CSS 文件进行分析和修改它的规则。
利用其插件 autoprefixer ,可以给 CSS 添加目标浏览器(Browserslist 中定义的)前缀。
1 | -moz- /* 火狐等使用Mozilla浏览器引擎的浏览器 */ |
1 | npm i -D postcss-loader autoprefixer |
创建 postcss.config.js, 定义 PostCSS 的插件为 autoprefixer:
1 | module.exports = { |
6.2.8.2.3 css-loader
加载 CSS 文件,并以 JS 模块(CSS 样式以字符串的形式封装在其中)的形式返回。
- 如果只使用 css-loader,解析出的 CSS 内容都在打包后的 js 代码中,没有任何作用。
- 只有配合 style-loader 或 mini-css-extract-plugin 等,引用的样式才会起作用。
1 | npm i -D css-loader |
6.2.8.2.4 style-loader
将 CSS 样式注入到 DOM 中。默认是在<header>最后添加<style>,这是通过生成 JS 方法动态添加的。
1 | npm i -D style-loader |
一般这样配置:
1 | { |
6.2.8.3 文件
6.2.8.3.1 url-loader
将小于一个限制大小的文件转换为base64 URIs。超过限制的,会默认使用 file-loader 来做处理。所以这里一定要把 file-loader 也安装上。
1 | npm i -D url-loader file-loader |
6.2.8.3.2 file-loader
生成文件到输出的文件夹中,并返回相对路径 URL 。
1 | npm i -D file-loader |
6.2.9 安装 webpack plugins
6.2.9.1 HTMLWebpackPlugin
会生成一个 HTML5 文件,其中 body 中会加入所有 webpack 打包出来的内容。
1 | npm i -D html-webpack-plugin |
6.2.9.2 clean-webpack-plugin
移除或清空构建出的文件夹。
1 | npm i -D clean-webpack-plugin |
6.2.9.3 mini-css-extract-plugin
将 CSS 提取到单独的 CSS 文件中。
1 | npm i -D mini-css-extract-plugin |
一般的配置如下:
1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); |
6.2.9.4 webpack-bundle-analyzer
可视化 webpack 输出文件的大小。
1 | npm i -D webpack-bundle-analyzer |
1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); |
6.2.10 创建 webpack.config.js
配置 webPack 来处理 TypeScript。
1 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); |
某些开源项目会在项目的根目录下创建一个 build 文件夹,将 webpack 的配置拆分为 base(存放公用配置)、dev(开发环境特有的配置)、prod(生产环境特有的配置),利用合并插件将 dev 或 prod 和 base 合并。个人认为其拆分的思想没有问题,但如果配置项没达到一定规模(500 行以上?)可以不用这么麻烦。如上方配置所示:只使用 webpack.config.js ,先添加公用配置(一个对象),然后通过判断当前是开发还是生产模式,补充相应配置。
6.2.11 添加脚本
package.json
1 | "scripts": { |
6.2.12 形成项目结构
创建 libs、public、src 文件夹
1 | ├── libs // 第三方库 |
其中 index.html
1 |
|
6.2.13 源码地址
https://github.com/THS-FE/typescript-webpack-starter
7 备注
7.1 为什么好多配置文件的后缀是 rc
[Unix: from runcom files on the CTSS system 1962-63, via the startup script /etc/rc] Script file containing startup instructions for an application program (or an entire operating system), usually a text file containing commands of the sort that might have been invoked manually once the system was running but are to be executed automatically each time the system starts up.
rc 代表短语 runcom (运行命令),unix 的爷爷 CTSS 系统中的脚本文件,里边包含了应用或者整个系统启动时要执行的命令。
现在更通用的含义可能是 runtime configration,即应用运行时的配置。
参考:https://stackoverflow.com/questions/11030552/what-does-rc-mean-in-dot-files
https://unix.stackexchange.com/questions/3467/what-does-rc-in-bashrc-stand-for