最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端工程化-制作一个公司的脚手架

    正文概述 掘金(forestxieCode)   2021-05-30   580

    成效

    前端工程化-制作一个公司的脚手架

    引言

    随着前端工程化的理念不断深入,越来越多的人选择使用脚手架从零到一搭建自己的项目。其中大家最熟悉的就是create-react-appvue-cli,它们可以帮助我们初始化配置生成项目结构、自动安装依赖,最后我们一行指令可运行项目开始开发,或者进行项目构建(build)。

    这些脚手架提供的都是普遍意义上的最佳实践,但是实际开发中发现,随着业务的不断发展,必然会出现需要针对业务开发的实际情况来进行调整。例如:

    • 通过调整插件与配置实现Webpack打包性能优化后

    • 项目架构调整

    • 编码风格

    • 用户权限控制

    • 融合公司的基建

    • ...

    总而言之,随着业务发展,我们往往会沉淀出一套更“个性化”的业务方案。这时候我们最直接的做法就是开发出一个该方案的脚手架来,以便今后能复用这些最佳实践与方案。

    1. 脚手架怎么工作?

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

    • 初始化,一般在这个时候会进行环境的初始化,做一些前置的检查,如版本更新。
    • 用户输入,进行交互,例如用vue-cli的时候,它会“问”你很多的配置选项
    • 生成模板
    • 生成配置文件
    • 安装依赖
    • 清理,校验等收尾工作

    在企业中一般我们只是想轻量级,快速的创建一个特定场景的脚手架(可能并不需要像vue-cli那么完备)。所有下面将演示制作的基本流程,从制作简单的demo调试,到复杂的功能开发。

    2.脚手架整体架构设计与基本流程

    前端工程化-制作一个公司的脚手架

    基本流程

    前端工程化-制作一个公司的脚手架

    3. 开发脚手架我们需要用到的三方库

    库名描述
    commander处理控制台命令chalk五彩斑斓的控制台latest-version活动最新的npm包inquirer控制台询问download-git-repogit远程仓库拉取figlet粉笔字glob匹配指定路径文件ora命令行环境的loading效果clear清除控制台输出的信息log-symbols各种日记级别的彩色符号metalsmith处理模板

    4.项目实战

    • 目录结构
        |-- .gitignore
        |-- .npmignore
        |-- package-lock.json
        |-- package.json
        |-- README.md
        |-- bin
        |   |-- cunw
        |   |-- cunw-init
        |   |-- cunw-list
        |-- conf
        |   |-- const.js
        |   |-- index.js
        |   |-- template.json
        |-- lib
        |   |-- download.js
        |   |-- generator.js
        |   |-- projectConstruction.js
        |-- utils
            |-- log.js
    
    
    1. 项目初始化

    新建一个 cunw文件夹,初始化项目npm init --y,并安装所需依赖

    {
      "name": "cunw-cli",
      "version": "0.0.1",
      "description": "快速生成项目的脚手架",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "forestxiecode",
      "license": "ISC",
      "dependencies": {
        "chalk": "^4.1.1",
        "clear": "^0.1.0",
        "commander": "^6.1.0",
        "download-git-repo": "^3.0.2",
        "ejs": "^2.7.4",
        "figlet": "^1.5.0",
        "git-clone": "^0.1.0",
        "glob": "^7.1.6",
        "handlebars": "^4.7.6",
        "inquirer": "^6.5.2",
        "latest-version": "^5.1.0",
        "log-symbols": "^4.0.0",
        "metalsmith": "^2.3.0",
        "minimatch": "^3.0.4",
        "ora": "^5.1.0",
        "rimraf": "^3.0.2",
        "wrap-ansi": "^7.0.0"
      },
      "devDependencies": {},
    }
    
    2. 测试环境

    在根目录下lib文件夹下,新建cunw-init.js文件,编写测试代码。

    + #! /usr/bin/env node
    + console.log('测试')
    

    并且修改pack.json文件,全局link指令,测试环境,调试代码,如。

    +  "bin": {
    +      "cunw-init": "bin/cunw-init"
    +   }
    
    • 在根目录 全局link下 npm link
    • 指令测试

    前端工程化-制作一个公司的脚手架

    3.编写程序代码

    根据输入,获取项目名称

    // 根据输入,获取项目名称
    let projectName = program.args[0];
    if (!projectName) {
      // 相当于执行命令的--help选项,显示help信息,这是commander内置的一个命令选项
      program.help();
      return;
    }
    // 返回 Node.js 进程的当前工作目录
    let rootName = path.basename(process.cwd());
    

    执行主函数,在这使用figlet工具打印大写的粉笔字体

        const data = await figlet('WELCOM   CUNW   CLI')
        console.log(chalk.green(data))
    

    进入版本检查,获取本地package.json文件下的版本号,用latest-version模块获取最后一次版本号进行对比

    function checkVersion() {
      return new Promise(async (resolve, reject) => {
        const spinner = ora(`检测版本....`);
        spinner.start();
        let webVersion = await latestVersion(`${CONST.CLI_NAME}`);
        let localVersion = require("../package.json").version;
        spinner.succeed();
        console.log(`本地版本${localVersion}, 最新版本${webVersion}\n`);
        let webVersionArr = webVersion.split(".");
        let localVersionArr = localVersion.split(".");
        let isNew = webVersionArr.some((item, index) => {
          return Number(item) > Number(localVersionArr[index]);
        });
        if (isNew) {
          log.warn(`检查已存在更新版本,请执行指令 npm install @cunw/cunw-cli -g 更新版本\n`)
          setTimeout(() => {
            resolve(isNew);
          }, 2000)
        } else {
          setTimeout(() => {
            resolve(isNew);
          }, 1000)
        }
      });
    }
    

    路径检查,判断当前是否已经存在该文件夹,否则创建该文件

    // 路径检查
    function checkDir() {
      return new Promise(async (resolve, reject) => {
        const list = glob.sync("*"); // 遍历当前目录
        if (list.length) {
          if (
            list.filter(name => {
              const fileName = path.resolve(process.cwd(), path.join(".", name));
              const isDir = fs.statSync(fileName).isDirectory();
              return name.indexOf(projectName) !== -1 && isDir;
            }).length !== 0
          ) {
            log.error(`项目${projectName}已经存在`)
            reject(`项目${projectName}已经存在`);
          }
          resolve(projectName);
        } else if (rootName === projectName) {
          let answer = await inquirer.prompt([
            {
              name: "buildInCurrent",
              message:
                "当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目",
              type: "confirm",
              default: true
            }
          ]);
          resolve(answer.buildInCurrent ? "." : projectName);
        } else {
          resolve(projectName);
        }
      });
    }
    // 创建该文件
    function makeDir(projectRoot) {
      if (projectRoot !== ".") {
        fs.mkdirSync(projectName);
      }
    }
    

    使用inquirer.js处理命令行交互,让用户现在自己需要的模板进行渲染。

    function selectTemplate() {
      return new Promise((resolve, reject) => {
        let choices = []
        Object.values(templateConfig).forEach(item => {
          if (item.enable) {
            choices.push({
              name: item.name,
              value: item.value
            });
          }
        });
        let config = {
          // type: 'checkbox',
          type: "list",
          message: "请选择创建项目类型",
          name: "select",
          choices: [new inquirer.Separator("模板类型"), ...choices]
        };
        inquirer.prompt(config).then(data => {
          let { select } = data;
          let { name, git, value } = templateConfig[select];
          resolve({
            git,
            name,
            value
          });
        });
      });
    }
    

    得到用户的模板地址,使用download-git-repo模块下载模板。

    module.exports = function (target, url) {
      const spinner = ora(`正在下载项目模板,源地址:${url}`)
      target = path.join(CONST.TEMPLATE_NAME)
      spinner.start()
      return new Promise((resolve, reject) => {
        download(`direct:${url}`,
          target, { clone: true }, (err) => {
            if (err) {
              spinner.fail()
              console.log(logSymbols.fail, chalk.red("模板下载失败"));
              reject(err)
            } else {
              spinner.succeed()
              console.log(logSymbols.success, chalk.green("模板下载完毕"));
              resolve(target)
            }
          })
      })
    }
    

    使用metalsmith处理模板

    引用官网的介绍:

    它就是一个静态网站生成器,可以用在批量处理模板的场景,类似的工具包还有WintersmithAssembleHexo。它最大的一个特点就是EVERYTHING IS PLUGIN,所以,metalsmith本质上就是一个胶水框架,通过黏合各种插件来完成生产工作。

    给项目模板添加变量占位符。

    module.exports = function (config) {
      let { metadata, src, dest } = config;
      if (!src) {
        return Promise.reject(new Error(`无效的source:${src}`));
      }
      // 官方模板
      return new Promise((resolve, reject) => {
        const metalsmith = Metalsmith(process.cwd())
          .metadata(metadata)
          .clean(false)
          .source(src)
          .destination(dest);
        const ignoreFile = path.resolve(process.cwd(), src, CONST.FILE_IGNORE);
        if (fs.existsSync(ignoreFile)) {
          // 定义一个用于移除模板中被忽略文件的metalsmith插件
          metalsmith.use((files, metalsmith, done) => {
            const meta = metalsmith.metadata();
            // 先对ignore文件进行渲染,然后按行切割ignore文件的内容,拿到被忽略清单
            const ignores = ejs
              .render(fs.readFileSync(ignoreFile).toString(), meta)
              .split("\n")
              .filter(item => !!item.length);
            Object.keys(files).forEach(fileName => {
              // 移除被忽略的文件
              ignores.forEach(ignorePattern => {
                if (minimatch(fileName, ignorePattern)) {
                  delete files[fileName];
                }
              });
            });
            done();
          });
        }
    
        metalsmith
          .use((files, metalsmith, done) => {
            const meta = metalsmith.metadata();
            // 编译模板
            Object.keys(files).forEach(fileName => {
              try {
                const t = files[fileName].contents.toString();
                if (/(<%.*%>)/g.test(t)) {
                  files[fileName].contents = new Buffer.from(ejs.render(t, meta));
                }
              } catch (err) {
                // console.log("fileName------------", fileName);
                // console.log("er -------------", err);
              }
            });
            done();
          })
          .build(err => {
            rm(src);
            err ? reject(err) : resolve();
          });
      });
    };
    

    package.jsonnameversiondescription字段的内容被替换成了handlebar语法的占位符,模板中其他地方也做类似的替换,完成后重新提交模板的更新。

    调用该函数删除一些无用的文件,做一些清理工作。

    function deleteCusomizePrompt(target) {
      // 自定义选项模板路径
      const cusomizePrompt = path.join(process.cwd(), target, CONST.CUSTOMIZE_PROMPT)
      if (fs.existsSync(cusomizePrompt)) {
        rm(cusomizePrompt)
      }
      // 忽略文档路径
      const fileIgnore = path.join(process.cwd(), target, CONST.FILE_IGNORE)
      if (fs.existsSync(fileIgnore)) {
        rm(fileIgnore)
      }
    }
    

    最后执行结束的回调,初始化项目。

     clear()
      log.succes("创建成功")
      // 初始化项目
      await initProject(projectRoot)
      // 运行项目
      console.log(chalk.green(`====================================\n
              运行项目 ...\n
              cd ./demmo\n
             npm run serve\n
    ===================================
    `))
    

    5. 如何发布一个npm包

    1. 注册一个npm账号
    2. 项目根目录下npm login 登入npm账号,最后执行npm publish 发布

    总结

    去模仿,去参照,其实实现一个脚手架也不是特别复杂。

    • 通过node可以很好的解决一些工程化上的问题
    • 其实npm存在很好的node模块库如:
      • 通过download-git-repo处理下载
      • 通过inquirer.js处理终端交互
      • 通过metalsmith和模板引擎将交互输入项插入到项目模板中

    通过这次开发自己的脚手架中。还想自己存在很多不足,也在模仿,和学习。


    起源地下载网 » 前端工程化-制作一个公司的脚手架

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元