如何手动去搭建企业级脚手架 sca-cli (一)

如何手动去搭建企业级脚手架 sca-cli

  • sca-cli 项目构建与打包流程
  • 前言
    • 面对问题
    • 验证自身
  • 脚手架 CLI
    • 脚手架是什么?
    • 脚手架怎么工作?
  • 功能实现
    • 实现思路
    • 初始化配置
    • 基础安装与配置
    • 界面化交互配置
    • 操作文件配置
    • 命令逻辑配置
      • 入口文件 sca-cli.ts
      • webpack module rules 代码
      • sca-cli dev 代码
      • sca-cli build 代码
    • 测试sca-cli dev 与 sca-cli build
    • sca.config.js 配置文件
  • 总结
    • 后续优化内容

sca-cli 项目构建与打包流程

前言

面对问题

当我们在项目开发中,我们使用了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脚手架的过程中,并调研流行的 上述这些脚手架工具并形成符合自身项目实际情况下的最佳实践,并学习这些脚手架的核心原理,以及学习探讨。

脚手架 CLI

脚手架是什么?

字面意思就是:

建筑上的脚手架就是先搭个架子,然后工人们慢慢往里面砌砖头,直到建筑成型。

而在我们项目开发之初的场景而言,脚手架就是程序中类似,比如我需要写个h5 页面,需要使用 vue 来实现,那么我需要在做这项工作之前,我需要一个搭建好的平台,而平台里面的一系列配置与工具的合集就是脚手架。

vue-cli 里面包含了一系列文件配置,环境变量与打包工具
这个初始的骨架在其他的项目JAVA也有类似东西

在我们前端工程化体系中,脚手架的强大与否,决定了你们工程化体系是否完善健全,关乎这我们以后的发展是否顺利,脚手架是一个不断升级改造的过程,根据我们自身当前的需求或者未来可能的需求,需要去不断完善升级迭代。

脚手架迭代的过程中,有时候也会融入其他好的脚手架的核心部分,去不断强化自身,有时候也面临着重构的代价。估前端工程化体系中脚手架是很重要的。

脚手架可以帮助我们初始化配置、生成项目结构、自动安装依赖,最后我们一行指令即可运行项目开始开发,或者进行项目构建(build)。

脚手架提供的就是项目开发搭建过程中的最佳实践,但是在开发中,随着业务的不断发展,必然会出现需要针对业务开发的实际情况来进行调整。例如:

  • 项目架构调整
  • 删除替换升级优化插件
  • 优化打包速度
  • 实现 Webpack 打包性能优化后
  • 融入微服务架构体系

总而言之,随着业务发展,我们往往会沉淀出一套更“个性化”,“定制化”,“高效化”的工程体系最佳方案的脚手架,以便今后能复用这些最佳实践与方案。

脚手架怎么工作?

功能丰富程度不同的脚手架,复杂程度自然也不太一样。但是总体来说,脚手架的工作大体都会包含几个步骤:

  1. 初始化,环境变量或者初始值的初始化,做一些前置的版本依赖检查
  2. 用户输入,例如用 vue-cli 的时候,界面化交互,提供一系列配置选择
  3. 根据用户输入与选择项,生成配置文件
  4. 根据配置文件,生成项目结构
  5. 安装配置文件中的相关依赖
  6. 生成项目初始文件

此外,你还需要指定一些命令、交互操作行为以及功能实现:

  1. command 命令行定义
  2. 快速生成应用模板,如vue-cli等根据与开发者的一些交互式问答生成应用框架
  3. 根据配置对代码进行拉取、创建module模板文件过程
  4. 模板相关约定配置
  5. eslint,代码校验
  6. 自动化测试工具与命令操作
  7. 编译es umd ,cjs等相关类型 build
  8. 编译分析,利用webpack插件进行分析
  9. npm库 publish 发布功能

脚手架所具备的全部功能,都是为了我们在项目开发中,可以不用关心内部的具体流程是什么,就好比:

webpack index.js
vue-cli-service serve
vue create hello-world

这样的命令去执行就可以了,我们不需要知道起内部做了哪些操作。只需要一个输出结果 dist 文件。而这一些列的内部操作到最后输出一个结果,这些就是脚手架需要去做的。而我们也可以把这些流程操作进行剥离成类似 @vue/cli,@vue/cli-service … 这些一系列的插件形式输出调用。

脚手架的作用:

  • 减少重复性的工作,不需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。
  • 可以根据交互动态生成项目结构和配置文件。
  • 多人协同,协作更为方便。
  • 项目开发,维护也方便。

功能实现

实现思路

设想的思路就是:

  1. 先实现 dev 项目构建与 build 项目打包功能,初始化 init 项目文件或者按需自定义式项目文件生成这个后续完善。毕竟项目文件我们完全可以去拷贝现有的项目来实现。
  2. 命令行控制语句先实现 sca-cli dev 本地环境测试部署与 sca-cli build 线上环境项目打包。毕竟这样符合我们项目开发主要需要的。
  3. 我们需要一个自定义配置文件 sca.config.js,里面的格式我们参考webpack.config.js的配置,我们新增一些我们自定义的,还有一部分可以当做webpack配置的扩展。本质上这个可以看做是类似webpack.config.js的自定义配置扩展插件。
  4. 目前的主流框架有vue react angular,先实现Vue框架的实现,后续再兼容其他框架。框架实现的差异化,我们可以通过命令行传递mode类型来区别。实现了一个框架功能另外的框架也就很容易多了。
  5. 项目使用的技术架构,基本上就是webpack 5.x, babel 7 等这些最新版本的。Vue 版本暂时不考虑 vue 3.x,使用最新的 [email protected],当前的支持es的npm 包的生态都还没健全。如果大家需要使用Vue 3.x的话,可以参考 Snowpack + Vue 初探与项目搭建demo

初始化配置

初始化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 文件。

  • dev 环境,站点的网址与端口号,估dev有两个参数
  • build 环境,设定有打包方式,指定打包目录,输出文件名称,输出目录

对应的代码如下:

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

简单的测试反馈,看了结果页符合我们的预期。接下来我们需要编辑逻辑处理结果的一系列代码了。

命令逻辑配置

入口文件 sca-cli.ts

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 文档

webpack module rules 代码

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 官网

sca-cli dev 代码

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();
        });
    });
}

代码说明:

  1. webpackConfig 里面就是webpack 的配置文件
  2. WebpackDevServer 就是配置 webpack-dev-server 的方法
  3. getWebpackConfig 方法就是通过读取我们的sca.config.js的文件,转换成 webpack 的配置文件

sca-cli build 代码

sca-cli build 与 sac-cli dev 代码的实现逻辑的差异化主要如下:

  • webpack 配置文件不同
  • dev 配置了 热更新 new webpack.HotModuleReplacementPlugin()
  • build 配置 new MiniCssExtractPlugin()
  • devtool 配置不一样,dev: ‘eval-cheap-source-map’;build:‘nosources-source-map’,

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')
        });
    })
}

再对代码做了一些合并与调整,详细查看源码

测试sca-cli dev 与 sca-cli build

src 新增一个demo 目录,里面新增一个vue 的级别项目配置,然后我们cd demo,运行:

sca-cli dev

运行的结果如下:
运行中:
如何手动去搭建企业级脚手架 sca-cli (一)_第1张图片运行结果:
如何手动去搭建企业级脚手架 sca-cli (一)_第2张图片这个时候浏览器打开页面看看:

如何手动去搭建企业级脚手架 sca-cli (一)_第3张图片看到结果,ok 运行成功;

我们在运行:

sca-cli build

得到如下结果:
如何手动去搭建企业级脚手架 sca-cli (一)_第4张图片如何手动去搭建企业级脚手架 sca-cli (一)_第5张图片我们测试一下build的出来的页面是否ok,我们只需要cd dist,然后 http-server -c-1,这个时候浏览器打开就可以看到页面了。

sca.config.js 配置文件

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 代码说明:

  • entries 里面就是配置打包的入库文件,包括 js 与 html 模板页面,里面的配置规范遵循 html-webpack-plugin 配置规范
  • resolve 里面的配置就是webpack 的resove 配置会merge 到最终的配置文件中
  • setRules 新增 webpack module 的 rules 配置,因为当前项目是vue ,故这里新增了vue-loader 与 vue-style-loader 来解析.vue文件与.vue 里面的css 文件
  • setPlugins 新增webpack plugins 里面的插件,我们项目是vue,因而我们新增了 VueLoaderPlugin 插件的配置。

总结

通过我们当前的尝试与代码开发实现,我们初步掌握并了解了 vue-cli,vue-create-app 等脚手架的基本实现原理与代码基本实现,也清楚掌握了部分nodejs的技术知识。

当前脚手架的实现,我们只尝试做了简单的项目本地开发与项目打包的操作,而里面运用到的技术知识就是webpack 与 babel的组合应用而已。而这两项技术知识对于我们前端开发工程师来说,是最熟悉不过的。现在我们把这两个技术单独从项目中单独抽离出来,这样后续的 webpack,babel的技术升级之后,我们只需对我们当前脚手架做相应的升级改造就可以了,对我们之前的项目就不需要去做任何的改动,而这个就是我们把有些公共基础工具,单独抽离做成脚手架工具的本质。这个对于我们在前端工程化体系的建立是很有帮助的,这样随着我们项目开发的不断迭代,这样我们就可以在当前的基础(简陋的)工程化脚手架的上,创建出符合我们企业级别的脚手架工具。

后续优化内容

  1. sca-cli dev/build 里面新增类型打包, 比如:es|lib|umd|umd-ugly|…
  2. sca-cli dev/build 可以指定目录,指定文件(正则匹配比如:.ts,.tsx ),输出文件,输出目录,…等一系列可配置化
  3. 新增打包分析器,对优化打包性能做出可视化分析
  4. 支持框架 react 的项目开发与打包构建。

后面完整之后,相关的文章会做持续的更新。

参考资料:

项目仓库地址
webpack官网
babeljs官网
sca-cli npm

你可能感兴趣的:(webpack,配置,Vue,javascript,typescript,node.js,webpack,vue-cli3)