前端工程化:前端工程化就是通过各种工具和技术,提升前端开发效率的过程
- 前端工程化的内容:各种工具和技术
- 前端工程化的作用:通过使用工具,提升开发效率
前端工程化的内容
- 压缩工具:项目上线前,压缩代码。例如将文件内容中的注释、空格等删除,见换行变成一行,例如 minify
- 转换工具:例如将ES6、CSS3新特性进行转换,解决兼容性问题
- Less、Sass等 CSS 的预编译语言进行编译处理
- Vue、React等 JS 库进行转换
- 格式化格局:多人开发、统一开发规范,例如ESLint、Prettier等
- 自动化工具:自动化构建(npm script & script hooks等)、自动化部署(Git Hook、CI/CD等)、自动化测试(Jest、mocha等)
- 脚手架工具:专用脚手架、通用脚手架
- 通用脚手架:Yeoman、Plop
- 专用脚手架:vue-cli、create-react-app、angular-cli
- 模块化打包:Webpack、Rollup、Parcel
- 前端使用到大部分工具都是用 Node.js 进行开发的
- 工程化 !== 某个工具(例如webpack)
- 脚手架
脚手架
前端工程化的发起者
作用:创建项目基础结构、提供项目规范和约定
通用脚手架:Yeoman
-
Yeoman 是一款脚手架工具:可以帮助开发人员创建项目的基础结构代码
-
yo 是 Yeoman 的命令行管理工具:可以在命令行运行yeoman的命令
-
生成器:Yeoman中具体的脚手架:针对不同项目有不同的脚手架(例如:网站、APP、小程序等)
-
Yeoman使用说明
// 全局安装
yo npm install --global yo
// 安装 generator(根据项目需求安装 generator-webapp、generator-node)
npm install --global generator-webapp
npm install --global generator-node
// 通过 yo 运行 generator
mkdir project-name
cd project-nam
yo webapp
// 启动运行
npm run start
- 构建自己的脚手架工具(自定义 Generator)
// 全局安装 yo(前提)
npm install--global yo
// 首先创建一个 generator-文件名 的文件(下面以 generator-oyzxvues 为例)
mkdir generator - oyzxvues
cd generator - oyzxvues
// 在 generator-oyzxvues 中 npm init 初始化项目,创建一个 package.json 文件
// 在 generator-oyzxvues 文件下创建 /generators/app/index.js 文件
// 在 generator-oyzxvues 项目中使用 yarn add yeoman-generator 安装 yeoman-generator,或者使用npm install yeoman-generator --save
yarn add yeoman - generator
// 在 /generators/app/ 文件夹中创建一个 templates 文件夹,把要安装的文件放在 templates 文件夹下,以 vue 项目为例,这里就是后面要安装的模板文件,
// 在 /generators/app/index.js 中写入
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
prompting() {
return this.prompt([
{
type: 'input',
name: 'title',
message: '你的项目名称是:',
default: this.appname
}
]).then(answers => {
this.answers = answers
})
}
writing() {
// 创建一个类似 vue 的项目路径
const templates = [
'public/favicon.ico',
'public/index.html',
'src/assets/logo.png',
'src/components/HelloWorld.vue',
'src/App.vue',
'src/main.js',
'.gitignore',
'babel.config.js',
'package.json',
'README.md',
'yarn.lock'
]
templates.forEach(item => {
this.fs.copyTpl(
this.templatePath(item),
this.destinationPath(item),
this.answers
)
})
// 单个模板文件的生成
// const tmpl = this.templatePath('index.html')
// const output = this.destinationPath('index.html')
// const context = this.answers
// this.fs.copyTpl(tmpl, output, context)
}
}
// 在 generator-oyzxvues 命令行中输入 npm link 将当前项目放入到本地 npm 模块中,使用 npm unlink 卸载(手动删除也可)
// 创建一个新文件夹,使用 yo oyzxvues 导入 generator-oyzxvues 脚手架,导入后会根据脚手架中的 package.json 下载依赖文件(其实就是 npm install)
- 专用脚手架 vue-cli、create-react-app、angular-cli
- 脚手架实现的原理(使用纯node进行实现,不引入其它脚手架模块)
// 步骤
// 1、创建一个文件夹 myclis,并 npm init -y 初始化项目(-y 就是后面执行中不停选择 yes 回车)
// 2、安装 ejs(模板引擎)、inquirer(用户与命令行交互的工具),npm install ejs inquirer --save
// 3、在当前文件夹 myclis 下创建 cli.js,并且在 package.json 中添加一行 bin 属性且添加需要的可执行js文件名(bin 是指定脚本的入口文件)
// package.json
{
"name": "myclis",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": "cli.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ejs": "^3.1.6",
"inquirer": "^8.2.0"
}
}
// 4、在当前文件夹 myclis 下创建 templates文件夹,并且添加一些文件
// 5、在 cli.js 文件输入要执行的命令
#!/usr/bin / env node
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs')
// 用户与命令行的交互,获取输入信息,当前是一个 promise
inquirer.prompt([
{
type: 'input',
name: 'name',
message: '您的项目名称:',
default: 'oyzx-project'
}
]).then(answers => {
// 在回调中去获取 templates 里面的文件
// 获取模板导入的路径
const tmpDir = path.join(__dirname, 'templates')
// 写入的路径,process.cwd() 方法返回 Node.js 进程的当前工作目录
const destDir = process.cwd()
fs.readdir(tmpDir, (err, files) => {
if (err) throw err
files.forEach(file => {
ejs.renderFile(path.join(tmpDir, file), answers, (err, result) => {
if (err) throw err
fs.writeFileSync(path.join(destDir, file), result)
})
})
})
})
// 最后使用 npm link 将当前项目放入到本地 npm 模块中,使用 npm unlink 卸载(手动删除也可)
// 创建一个新文件夹,使用 myclis(项目创建的名称)导入模板
// 循环多层文件
#!/usr/bin / env node
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs')
// 递归遍历所有文件,并且进行读写
const RecursionFile = (pImportDir, pExportDir) => {
fs.readdir(pImportDir, (err, files) => {
if (err) throw err
files.forEach(file => {
const vPath = path.join(pImportDir, file)
const vState = fs.lstatSync(vPath);
if (vState.isDirectory()) {
// 如果需要对path进行操作 需要转字符串toString()
const vSubPath = pImportDir.slice(path.join(__dirname, 'templates').length, pImportDir.length)
fs.mkdirSync('./' + path.join(vSubPath, file))
RecursionFile(path.join(pImportDir, file), pExportDir)
} else {
fs.readFile(path.join(pImportDir, file), (err, result) => {
if (err) throw err
const vSubPath = pImportDir.slice(path.join(__dirname, 'templates').length, pImportDir.length)
fs.writeFileSync(path.join(pExportDir, vSubPath, file), result)
})
}
})
})
}
// 用户与命令行的交互,获取输入信息,当前是一个 promise
inquirer.prompt([
{
type: 'input',
name: 'name',
message: '您的项目名称:',
default: 'oyzx-project'
}
]).then(answers => {
// 在回调中去获取 templates 里面的文件
// 获取模板导入的路径
const tmpDir = path.join(__dirname, 'templates')
// 写入的路径,process.cwd() 方法返回 Node.js 进程的当前工作目录
const destDir = process.cwd()
RecursionFile(tmpDir, destDir)
})
- 脚手架的工作流程
- 通过命令行交互询问用户问题
- 根据用户回答的结果生成文件
- 自动化构建
- 自动化构建是指手动构建任务,通过命令自动执行的过程
- npm scripts 实现自动化构建的最简方式,npm 允许在 package.json 文件中,使用 scripts 字段定义脚本命令,例如:我们在项目中常用的 npm run [-name] 命令就是 npm scripts 构建
- 把多行执行命令用一行代替
- 执行过程中有两种方式:并行、串行
- 并行:在 scripts 字段命令行中用 空格 隔开命令
- 串行:在 scripts 字段命令中使用 && 命令隔开
AST
- 前言:前端工程化基本上都离不开 AST,底层都是用到 AST 的思想逻辑。首先要理解什么是 AST 抽象语法树 概念,因为 webpack、babel、Vue中的template、React中的JSX等都要用到它。
- 什么是 AST 抽象语法树:
- AST 抽象语法树 是源代码的抽象语法结构树状表现形式,Webpack、ESLint、JSX、TypeScript 的编译和模块化规则之间的转化都是通过 AST 来实现对代码的检查、分析以及编译等操作,AST 将 JS 语法编译成 抽象语法树需要先对其代码块进行遍历、修改并重新编译,遍历树结构是 深度遍历。
- 代码的转义可以通过下面这个网站进行:esprima.org/demo/parse.…
// js代码转义前
const a = 20;
// 使用转义工具转换后
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 20,
"raw": "20"
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
- 实现转换需要用到的工具
- esprima、estraverse 和 escodegen 模块是操作 AST 的三个重要模块,也是实现 babel 的核心依赖,下面是分别介绍三个模块的作用。
- esprima 模块的用法:可以看到通过 esprima 模块中的 parseScript 方法将 JS 代码块转换成 AST 抽象语法树,代码需要将原代码块转换成字符串(esprima 中也提供了一个转换方法 parseModule),然后再将其进行转换。
// 需要安装 esprima 依赖
const esprima = require("esprima");
const code = 'function fn(){}'
// 生成语法树
const tree = esprima.parseScript(code);
console.log(tree);
// tree 的输出
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "fn"
},
"params": [],
"body": {
"type": "BlockStatement",
"body": []
},
"generator": false,
"expression": false,
"async": false
}
],
"sourceType": "script"
}
estraverse 遍历和修改AST
- 下面代码通过 extraverse 模块中 traverse 方法,将 code 转换后的 AST(使用 esprima.parseScript(code)) 进行了遍历,并且打印出 node 上面的 type 属性,type 属性就是 AST抽象语法树 中的一个树节点,修改是获取对应的类型,并对此类型对应的节点上的属性即可。 - 在 AST 的深度遍历中,其实遍历的就是每一层的 type 属性,我们根据 type 属性对当前代码进行解读,且遍历会有两个阶段,进入阶段(enter)和离开阶段(leave),在 estraverse 模块中的 traverse 里面分别用参数指定了 enter 和 leave 两个函数监听,一般我们的侧重点是 enter。
// 安装 esprima、estraverse 依赖
const esprima = require('esprima');
const estraverse = require('estraverse');
// 生产代码字符串
const code = 'function fn(){}';
const ast = esprima.parseScript(code);
// 遍历语法树
estraverse.traverse(ast, {
enter(node) {
console.log('enter', node.type);
},
leave() {
console.log('leave', node.type);
}
})
// enter Program
// enter FunctionDeclaration
// enter Identifier
// leave Identifier
// enter BlockStatement
// leave BlockStatement
// leave FunctionDeclaration
// leave Program
escodegen 将 AST 转换成 JS
- 下面代码就是将 JS 代码块转成 AST,并将遍历、修改后的 AST 重新转成 JS 的全过程
// 安装 esprima、estraverse、escodegen 依赖
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
// 将代码块转成字符串
const code = 'function fn(){}';
// 将代码块转成字符串后的生成 ast抽象语法树
const ast = esprima.parseScript(code);
// 遍历语法树,并且对指定 type 的节点进行修改
estraverse.traverse(ast, {
// 修改函数名
if(node.type === "FunctionDeclaration") {
node.id.name = "ast";
}
})
// 编译语法树,重新生成code模块
const result = escodegen.generate(tree);
console.log(result);
// function ast() {
// }