当我们在项目开发中,我们使用了React, Vue ,Angular 等或者其他相关架构的时候,我们大多数人或者中小的公司的项目负责人,对于项目工程化建设方面,往往并不重视或者说也没有时间精力来自定义去打造符合当前个人或者公司的项目发展的工程化体系建设。而我们作为开发者,本身业务需求繁忙,往往项目一个接一个,对于这些工程化的建设都是缺失的,而在工作中,当我们项目开发很久之后,会突然发现现在我的项目竟然还在使用webpack 2.x 3.x或者更低的 webpack 1.x,现在都已经都webpack 5.x 时代了,而我们想去升级这些版本的时候,发现package.json的部分依赖又有版本依赖,而我们不知道这些依赖我们升级了会不会导致项目无法跑起来。无法升级版本,往往意味着打包时间慢,无法使用新的功能,也无法去进一步学习新的知识等等,这些都给我们带来了很多苦恼。
面对 Vue-cli , create-react-app,angular-cli这些 Vue ,React, Angular 这些脚手架,以及Vue 3.X 的 create-vite-app 的新型打包方式的脚手架,还有rollup,parcel,以及无需打包工具(Webpack,Parcel)的 snowpack,还有FIS3这些脚手架工具等。
我们手动去搭建一个脚手架,可以考验了你自身的代码水平,阅读源码的能力,以及我们前端自身的主流技术 nodejs 水平、工程化能力、以及工具服务的设计能力,是前端进阶不可或缺的过程。
我们在开发自己的my-cli脚手架的过程中,并调研流行的 上述这些脚手架工具并形成符合自身项目实际情况下的最佳实践,并学习这些脚手架的核心原理,以及学习探讨。
字面意思就是:
建筑上的脚手架就是先搭个架子,然后工人们慢慢往里面砌砖头,直到建筑成型。
而在我们项目开发之初的场景而言,脚手架就是程序中类似,比如我需要写个h5 页面,需要使用 vue 来实现,那么我需要在做这项工作之前,我需要一个搭建好的平台,而平台里面的一系列配置与工具的合集就是脚手架。
vue-cli 里面包含了一系列文件配置,环境变量与打包工具
这个初始的骨架在其他的项目JAVA也有类似东西
在我们前端工程化体系中,脚手架的强大与否,决定了你们工程化体系是否完善健全,关乎这我们以后的发展是否顺利,脚手架是一个不断升级改造的过程,根据我们自身当前的需求或者未来可能的需求,需要去不断完善升级迭代。
脚手架迭代的过程中,有时候也会融入其他好的脚手架的核心部分,去不断强化自身,有时候也面临着重构的代价。估前端工程化体系中脚手架是很重要的。
脚手架可以帮助我们初始化配置、生成项目结构、自动安装依赖,最后我们一行指令即可运行项目开始开发,或者进行项目构建(build)。
脚手架提供的就是项目开发搭建过程中的最佳实践,但是在开发中,随着业务的不断发展,必然会出现需要针对业务开发的实际情况来进行调整。例如:
总而言之,随着业务发展,我们往往会沉淀出一套更“个性化”,“定制化”,“高效化”的工程体系最佳方案的脚手架,以便今后能复用这些最佳实践与方案。
功能丰富程度不同的脚手架,复杂程度自然也不太一样。但是总体来说,脚手架的工作大体都会包含几个步骤:
此外,你还需要指定一些命令、交互操作行为以及功能实现:
脚手架所具备的全部功能,都是为了我们在项目开发中,可以不用关心内部的具体流程是什么,就好比:
webpack index.js
vue-cli-service serve
vue create hello-world
这样的命令去执行就可以了,我们不需要知道起内部做了哪些操作。只需要一个输出结果 dist 文件。而这一些列的内部操作到最后输出一个结果,这些就是脚手架需要去做的。而我们也可以把这些流程操作进行剥离成类似 @vue/cli,@vue/cli-service … 这些一系列的插件形式输出调用。
脚手架的作用:
设想的思路就是:
初始化package.json文件
npm init
填写一些基本的数据,还有需要取个名称,用于命令行输入。我这边取名为 sca-cli
"name": "sca-cli",
"version": "1.0.0",
"description": "脚手架工具",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "nathan",
"license": "ISC"
}
安装一些基本的依赖,我们打算使用TS来编写我们的脚手架,然后使用babel进行编译,那么我们需要安装一些babel的依赖包,然后配置一下babel.config.js文件。
npm install @babel/core @babel/cli @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env @babel/preset-typescript @babel/plugin-transform-runtime @babel/runtime @babel/runtime-corejs3 --save-dev
我们再新建文件夹 src ,然后新建一个test.ts文件:
const x: number = 0;
我们测试一下,安装我们的思路,我们需要把src下面的.ts文件转换成js文件,然后将转换的文件拷贝到lib目录下面,拷贝之前需求清空 lib 文件目录。综合的命令语句如下:
rm -rf lib && npx babel src --extensions .ts --out-dir lib --copy-files
最终我们得到的test.js:
"use strict";
var x = 0;
console.log(x);
说明我们.ts 转换成 .js 这个功能是没有任何问题的。这个就是我们后面把我们写的代码打包出来需要的命令操作代码。我们需要将这个命令语句,写到package.json 里面:
"build":"rm -rf lib && npx babel src --extensions .ts --out-dir lib --copy-files",
"dev":"npm run build --watch",
从本质上讲 dev 与 build 是一样的!但是从开发的角度上看还是需要区分一下~
需要安装界面化插件 commander
npm install commander --save-dev
修改package.json 文件,删除main路径,新增bin路径,改动如下:
"bin": {
"sca-cli": "./bin/sca-cli.js"
},
再src目录下新建 sca-cli.ts 文件。
重要的事情,将 npm 模块链接到对应的运⾏行行项⽬目中去:
npm link
有些的电脑上,这个命令行会提示需要root权限,那么就执行:
sudo npm link
得到如下的结果:
/Users/nathan/npm/bin/sca-cli -> /Users/nathan/npm/lib/node_modules/sca-cli/bin/sca-cli.js
/Users/nathan/npm/lib/node_modules/sca-cli -> /Users/nathan/learngit/sca-cli
这个时候代码了,你就可以再本地使用 sca-cli 命令了
我们目前就先设定测试环境 dev 与线上环境 build , 新增 development.ts,build.ts 文件。
对应的代码如下:
program
.command('dev')
.description('运行开发/测试环境')
.option('-h,--host ' , '站点主机地址', 'localhost')
.option('-p,--port ' , '站点端口号', 3000)
.action(development);
program
.command('build')
.description('打包编译操作')
.option('-m, --mode ' , '选择打包模式')
.option('-p,--path ' , '源文件目录')
.option('-o,--out-file ' , '输出文件')
.option('-d,--out-dir ' , '输出目录')
.action(build);
编写development.ts,build.ts 如下:
// development.ts
export interface developmentType {
host: string;
port: number;
}
export default async ({ host, port }: developmentType) => {
console.log("host",host)
console.log("port",port)
}
//build.ts
export default async (options) => {
const { mode, path, outFile, outDir } = options
console.log("mode",mode)
console.log("path",path)
console.log("outFile",outFile)
console.log("outDir",outDir)
}
测试一下:
命令行输入如下:
sca-cli dev -p 4000
sca-cli dev -p 4000 -h 192.2.3.4
sca-cli build -m es -p src/ts -o xxx -d dist
分别输出:
host 127.0.0.0
port 4000
/// =====
host 192.2.3.4
port 4000
// =====
mode es
path src/ts
outFile xxx
outDir dist
简单的测试反馈,看了结果页符合我们的预期。接下来我们需要编辑逻辑处理结果的一系列代码了。
src 下面新增 sca-cli.ts 文件,文件代码如下:
import program from 'commander';
import { version } from '../package.json';
import development from './development';
import build from './build';
program.version(version)
program
.command('dev')
.description('运行开发/测试环境')
.option('-h,--host ' , '站点主机地址', 'localhost')
.option('-p,--port ' , '站点端口号', 3000)
.action(development);
program
.command('build')
.description('打包编译操作')
// .option('-m, --mode ', '选择打包模式')
// .option('-p,--path ', '源文件目录')
// .option('-o,--out-file ', '输出文件')
.option('-d,--out-dir ' , '输出目录')
.action(build);
program.parse(process.argv);
if (!program.args[0]) {
program.help();
}
代码说明:
命令行操作,新增 sca-cli dev ,sca-cli build 操作,具体
commander的使用规则与文档,请参考 commander 文档
src 下面新增 development.ts 文件,
主要的核心代码如下:
//webpack 配置
//部分代码
output: {
filename: 'js/[name].[chunkhash:8].js',
chunkFilename: 'js/[name].[chunkhash:8].js',
publicPath: '/',
},
module: {
...rules
}
module 里面的rules 简单枚举几个配置:
(1)JS rules 配置:
{
test: /\.(js|jsx|ts|tsx)$/,
// exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: babelLoaderConfig,
//options: {
// presets: ['@babel/preset-env'],
//plugins: ['@babel/plugin-transform-runtime']
}
},
],
},
(2)css rules 配置:
{
test: /\.(css|scss)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false,
},
},
// {
// loader: require.resolve('style-loader'),
// },
{
loader: require.resolve('css-loader'),
options: {
modules: false
},
},
{
loader: require.resolve('sass-loader'),
options: {
sourceMap: true,
implementation: require('sass'),
},
},
{
loader: require.resolve('postcss-loader'),
options: {
sourceMap: true,
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
autoprefixer: { grid: true },
features: {
'nesting-rules': true
},
browsers: 'last 2 versions',
stage: 3,
},
],
],
}
},
},
],
},
css loader 的注意事项:style-loader 不能与MiniCssExtractPlugin.loader同时使用,我们可以在本地开发的时候,使用style-loader;测试/线上环境部署的时候使用MiniCssExtractPlugin.loader来使用,只需要加个环境变量来控制就可以了。
(3)文件类的 rules 配置:
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: {
loader: "file-loader",
options: {
outputPath:"font"
}
}
},
{
test: /\.(png|svg|jpg|gif)$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath:"img"
}
}
},
(4)babel-loader 配置文件单独出来配置
特殊调整:js文件的配置我这边单独出来了一个文件去配置
babelLoaderConfig.ts 代码如下:
export default {
presets: [
[require.resolve('@babel/preset-env'), {
modules: false,
}],
require.resolve('@babel/preset-react'),
require.resolve('@babel/preset-typescript'),
],
plugins: [
require.resolve('@babel/plugin-transform-runtime'),
require.resolve('@babel/plugin-transform-modules-commonjs'),
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
[require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }],
require.resolve('@babel/plugin-proposal-optional-chaining'),
],
};
这里面的未成完善过,只是一些经验之谈而已,有些可能并不需要的。这块后续会一一去完善。
上述代码 module 里面就是一些js,css,img等一系列配置,详细的这些规则配置可以参考webpack 官网
webpack 使用 webpack-dev-server 启动服务,代码如下:
export default async ({ host, port }: developmentType) => {
// webpack 配置
const webpackConfig = getWebpackConfig(developmentWebpackConfig)
const compiler = webpack(webpackConfig);
//配置 webpack-dev-server
const devServerConfig = {
publicPath: '/',
compress: true,
noInfo: true,
inline: true,
hot: true,
open:true,
}
const devServer = new WebpackDevServer(compiler, devServerConfig);
devServer.listen(port, host, (err) => {
if (err) {
return console.error(err);
}
console.warn(`http://${host}:${port}\n`);
});
['SIGINT', 'SIGTERM'].forEach((sig: any) => {
process.on(sig, () => {
devServer.close();
process.exit();
});
});
}
代码说明:
sca-cli build 与 sac-cli dev 代码的实现逻辑的差异化主要如下:
src的build.ts 主要代码如下:
export default async (options) => {
const { outDir } = options
console.log("outDir", outDir)
// webpack 配置
const webpackConfig = getWebpackConfig(buildConfig)
const path = getProjectPath(outDir || 'dist')
webpackConfig.output.path = path
rimraf(path, () => {
const spinner = ora('building for production...')
spinner.start()
webpack(webpackConfig).run((err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n')
});
})
}
再对代码做了一些合并与调整,详细查看源码
src 新增一个demo 目录,里面新增一个vue 的级别项目配置,然后我们cd demo,运行:
sca-cli dev
运行的结果如下:
运行中:
运行结果:
这个时候浏览器打开页面看看:
我们在运行:
sca-cli build
得到如下结果:
我们测试一下build的出来的页面是否ok,我们只需要cd dist,然后 http-server -c-1,这个时候浏览器打开就可以看到页面了。
demo vue 项目的sca.config.js 源码如下:
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
entries: {
index: {
entry: './main.js',
template: './public/index.html',
favicon: './public/favicon.ico',
assets:''
},
},
resolve: {
alias: {
},
},
setRules: (rules) => {
rules.unshift(
{
test: /\.vue$/,
use: require.resolve("vue-loader")
}
)
rules.unshift({
test: /\.css|scss$/,
use: require.resolve("vue-style-loader")
})
},
setPlugins: (plugins) => {
plugins.push(
new VueLoaderPlugin()
)
}
};
sca.config.js 代码说明:
通过我们当前的尝试与代码开发实现,我们初步掌握并了解了 vue-cli,vue-create-app 等脚手架的基本实现原理与代码基本实现,也清楚掌握了部分nodejs的技术知识。
当前脚手架的实现,我们只尝试做了简单的项目本地开发与项目打包的操作,而里面运用到的技术知识就是webpack 与 babel的组合应用而已。而这两项技术知识对于我们前端开发工程师来说,是最熟悉不过的。现在我们把这两个技术单独从项目中单独抽离出来,这样后续的 webpack,babel的技术升级之后,我们只需对我们当前脚手架做相应的升级改造就可以了,对我们之前的项目就不需要去做任何的改动,而这个就是我们把有些公共基础工具,单独抽离做成脚手架工具的本质。这个对于我们在前端工程化体系的建立是很有帮助的,这样随着我们项目开发的不断迭代,这样我们就可以在当前的基础(简陋的)工程化脚手架的上,创建出符合我们企业级别的脚手架工具。
后面完整之后,相关的文章会做持续的更新。
参考资料:
项目仓库地址
webpack官网
babeljs官网
sca-cli npm