主頁 > 企業開發 > Vue CLI 是如何實作的 -- 終端命令列工具篇

Vue CLI 是如何實作的 -- 終端命令列工具篇

2021-04-22 06:27:48 企業開發

Vue CLI 是一個基于 Vue.js 進行快速開發的完整系統,提供了終端命令列工具、零配置腳手架、插件體系、圖形化管理界面等,本文暫且只分析專案初始化部分,也就是終端命令列工具的實作,

0. 用法

用法很簡單,每個 CLI 都大同小異:

npm install -g @vue/cli
vue create vue-cli-test

目前 Vue CLI 同時支持 Vue 2 和 Vue 3 專案的創建(默認配置),

上面是 Vue CLI 提供的默認配置,可以快速地創建一個專案,除此之外,也可以根據自己的專案需求(是否使用 Babel、是否使用 TS 等)來自定義專案工程配置,這樣會更加的靈活,

選擇完成之后,敲下回車,就開始執行安裝依賴、拷貝模板等命令...

看到 Successfully 就是專案初始化成功了,

vue create  命令支持一些引數配置,可以通過 vue create --help  獲取詳細的檔案:

用法:create [options] <app-name>

選項:
  -p, --preset <presetName>       忽略提示符并使用已保存的或遠程的預設選項
  -d, --default                   忽略提示符并使用默認預設選項
  -i, --inlinePreset <json>       忽略提示符并使用行內的 JSON 字串預設選項
  -m, --packageManager <command>  在安裝依賴時使用指定的 npm 客戶端
  -r, --registry <url>            在安裝依賴時使用指定的 npm registry
  -g, --git [message]             強制 / 跳過 git 初始化,并可選的指定初始化提交資訊
  -n, --no-git                    跳過 git 初始化
  -f, --force                     覆寫目標目錄可能存在的配置
  -c, --clone                     使用 git clone 獲取遠程預設選項
  -x, --proxy                     使用指定的代理創建專案
  -b, --bare                      創建專案時省略默認組件中的新手指導資訊
  -h, --help                      輸出使用幫助資訊

具體的用法大家感興趣的可以嘗試一下,這里就不展開了,后續在原始碼分析中會有相應的部分提到,

1. 入口檔案

本文中的 vue cli 版本為 4.5.9,若閱讀本文時存在 break change,可能就需要自己理解一下啦

按照正常邏輯,我們在 package.json 里找到了入口檔案:

{
  "bin": {
    "vue": "bin/vue.js"
  }
}

bin/vue.js 里的代碼不少,無非就是在 vue  上注冊了 create / add / ui  等命令,本文只分析 create  部分,找到這部分代碼(洗掉主流程無關的代碼后):

// 檢查 node 版本
checkNodeVersion(requiredVersion, '@vue/cli');

// 掛載 create 命令
program.command('create <app-name>').action((name, cmd) => {
  // 獲取額外引數
  const options = cleanArgs(cmd);
  // 執行 create 方法
  require('../lib/create')(name, options);
});

cleanArgs  是獲取 vue create  后面通過 -  傳入的引數,通過 vue create --help 可以獲取執行的引數串列,

獲取引數之后就是執行真正的 create  方法了,等等仔細展開,

不得不說,Vue CLI 對于代碼模塊的管理非常細,每個模塊基本上都是單一功能模塊,可以任意地拼裝和使用,每個檔案的代碼行數也都不會很多,閱讀起來非常舒服,

2. 輸入命令有誤,猜測用戶意圖

Vue CLI 中比較有意思的一個地方,如果用戶在終端中輸入 vue creat xxx  而不是 vue create xxx,會怎么樣呢?理論上應該是報錯了,

如果只是報錯,那我就不提了,看看結果:

終端上輸出了一行很關鍵的資訊 Did you mean create,Vue CLI 似乎知道用戶是想使用 create  但是手速太快打錯單詞了,

這是如何做到的呢?我們在源代碼中尋找答案:

const leven = require('leven');

// 如果不是當前已掛載的命令,會猜測用戶意圖
program.arguments('<command>').action(cmd => {
  suggestCommands(cmd);
});

// 猜測用戶意圖
function suggestCommands(unknownCommand) {
  const availableCommands = program.commands.map(cmd => cmd._name);

  let suggestion;

  availableCommands.forEach(cmd => {
    const isBestMatch =
      leven(cmd, unknownCommand) < leven(suggestion || '', unknownCommand);
    if (leven(cmd, unknownCommand) < 3 && isBestMatch) {
      suggestion = cmd;
    }
  });

  if (suggestion) {
    console.log(`  ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`));
  }
}

代碼中使用了 leven 了這個包,這是用于計算字串編輯距離演算法的 JS 實作,Vue CLI 這里使用了這個包,來分別計算輸入的命令和當前已掛載的所有命令的編輯舉例,從而猜測用戶實際想輸入的命令是哪個,

小而美的一個功能,用戶體驗極大提升,

3. Node 版本相關檢查

3.1 Node 期望版本

create-react-app  類似,Vue CLI 也是先檢查了一下當前 Node 版本是否符合要求:

  • 當前 Node 版本: process.version
  • 期望的 Node 版本: require("../package.json").engines.node

比如我目前在用的是 Node v10.20.1 而 @vue/cli 4.5.9  要求的 Node 版本是 >=8.9,所以是符合要求的,

3.2 推薦 Node LTS 版本

bin/vue.js  中有這樣一段代碼,看上去也是在檢查 Node 版本:

const EOL_NODE_MAJORS = ['8.x', '9.x', '11.x', '13.x'];
for (const major of EOL_NODE_MAJORS) {
  if (semver.satisfies(process.version, major)) {
    console.log(
      chalk.red(
        `You are using Node ${process.version}.\n` +
          `Node.js ${major} has already reached end-of-life and will not be supported in future major releases.\n` +
          `It's strongly recommended to use an active LTS version instead.`
      )
    );
  }
}

可能并不是所有人都了解它的作用,在這里稍微科普一下,

簡單來說,Node 的主版本分為奇數版本偶數版本,每個版本發布之后會持續六個月的時間,六個月之后,奇數版本將變為 EOL 狀態,而偶數版本變為 **Active LTS **狀態并且長期支持,所以我們在生產環境使用 Node 的時候,應該盡量使用它的 LTS 版本,而不是 EOL 的版本,

EOL 版本:A End-Of-Life version of Node
LTS 版本: A long-term supported version of Node

這是目前常見的 Node 版本的一個情況:

解釋一下圖中幾個狀態:

  • CURRENT:會修復 bug,增加新特性,不斷改善
  • ACTIVE:長期穩定版本
  • MAINTENANCE:只會修復 bug,不會再有新的特性增加
  • EOL:當進度條走完,這個版本也就不再維護和支持了

通過上面那張圖,我們可以看到,Node 8.x 在 2020 年已經 EOL,Node 12.x 在 2021 年的時候也會進入 **MAINTENANCE **狀態,而 Node 10.x 在 2021 年 4、5 月的時候就會變成 EOL

Vue CLI 中對當前的 Node 版本進行判斷,如果你用的是 EOL 版本,會推薦你使用 LTS 版本,也就是說,在不久之后,這里的應該判斷會多出一個 10.x,還不快去給 Vue CLI 提個 PR(手動狗頭),

4. 判斷是否在當前路徑

在執行 vue create  的時候,是必須指定一個 app-name ,否則會報錯: Missing required argument <app-name> ,

那如果用戶已經自己創建了一個目錄,想在當前這個空目錄下創建一個專案呢?當然,Vue CLI 也是支持的,執行 vue create .  就 OK 了,

lib/create.js  中就有相關代碼是在處理這個邏輯的,

async function create(projectName, options) {
  // 判斷傳入的 projectName 是否是 .
  const inCurrent = projectName === '.';
  // path.relative 會回傳第一個引數到第二個引數的相對路徑
  // 這里就是用來獲取當前目錄的目錄名
  const name = inCurrent ? path.relative('../', cwd) : projectName;
  // 最終初始化專案的路徑
  const targetDir = path.resolve(cwd, projectName || '.');
}

如果你需要實作一個 CLI,這個邏輯是可以拿來即用的,

5. 檢查應用名

Vue CLI 會通過 validate-npm-package-name  這個包來檢查輸入的 projectName 是否符合規范,

const result = validateProjectName(name);
if (!result.validForNewPackages) {
  console.error(chalk.red(`Invalid project name: "${name}"`));
  exit(1);
}

對應的 npm 命名規范可以見:Naming Rules

6. 若目標檔案夾已存在,是否覆寫

這段代碼比較簡單,就是判斷 target  目錄是否存在,然后通過互動詢問用戶是否覆寫(對應的是操作是洗掉原目錄):

// 是否 vue create -m
if (fs.existsSync(targetDir) && !options.merge) {
  // 是否 vue create -f
  if (options.force) {
    await fs.remove(targetDir);
  } else {
    await clearConsole();
    // 如果是初始化在當前路徑,就只是確認一下是否在當前目錄創建
    if (inCurrent) {
      const { ok } = await inquirer.prompt([
        {
          name: 'ok',
          type: 'confirm',
          message: `Generate project in current directory?`,
        },
      ]);
      if (!ok) {
        return;
      }
    } else {
      // 如果有目標目錄,則詢問如何處理:Overwrite / Merge / Cancel
      const { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: `Target directory ${chalk.cyan(
            targetDir
          )} already exists. Pick an action:`,
          choices: [
            { name: 'Overwrite', value: 'overwrite' },
            { name: 'Merge', value: 'merge' },
            { name: 'Cancel', value: false },
          ],
        },
      ]);
      // 如果選擇 Cancel,則直接中止
      // 如果選擇 Overwrite,則先洗掉原目錄
      // 如果選擇 Merge,不用預處理啥
      if (!action) {
        return;
      } else if (action === 'overwrite') {
        console.log(`\nRemoving ${chalk.cyan(targetDir)}...`);
        await fs.remove(targetDir);
      }
    }
  }
}

7. 整體錯誤捕獲

create  方法的最外層,放了一個 catch  方法,捕獲內部所有拋出的錯誤,將當前的 spinner  狀態停止,退出行程,

module.exports = (...args) => {
  return create(...args).catch(err => {
    stopSpinner(false); // do not persist
    error(err);
    if (!process.env.VUE_CLI_TEST) {
      process.exit(1);
    }
  });
};

8. Creator 類

lib/create.js  方法的最后,執行了這樣兩行代碼:

const creator = new Creator(name, targetDir, getPromptModules());
await creator.create(options);

看來最重要的代碼還是在 Creator  這個類中,

打開 Creator.js  檔案,好家伙,500+ 行代碼,并且引入了 12 個模塊,當然,這篇文章不會把這 500 行代碼和 12 個模塊都理一遍,沒必要,感興趣的自己去看看好了,

本文還是梳理主流程和一些有意思的功能,

8.1 constructor 建構式

先看一下 Creator  類的的建構式:

module.exports = class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    super();

    this.name = name;
    this.context = process.env.VUE_CLI_CONTEXT = context;
    // 獲取了 preset 和 feature 的 互動選擇串列,在 vue create 的時候提供選擇
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();
    this.presetPrompt = presetPrompt;
    this.featurePrompt = featurePrompt;

    // 互動選擇串列:是否輸出一些檔案
    this.outroPrompts = this.resolveOutroPrompts();

    this.injectedPrompts = [];
    this.promptCompleteCbs = [];
    this.afterInvokeCbs = [];
    this.afterAnyInvokeCbs = [];

    this.run = this.run.bind(this);

    const promptAPI = new PromptModuleAPI(this);
    // 將默認的一些配置注入到互動串列中
    promptModules.forEach(m => m(promptAPI));
  }
};

建構式嘛,主要就是初始化一些變數,這里主要將邏輯都封裝在 resolveIntroPrompts / resolveOutroPrompts  和 PromptModuleAPI  這幾個方法中,

主要看一下 PromptModuleAPI 這個類是干什么的,

module.exports = class PromptModuleAPI {
  constructor(creator) {
    this.creator = creator;
  }
  // 在 promptModules 里用
  injectFeature(feature) {
    this.creator.featurePrompt.choices.push(feature);
  }
  // 在 promptModules 里用
  injectPrompt(prompt) {
    this.creator.injectedPrompts.push(prompt);
  }
  // 在 promptModules 里用
  injectOptionForPrompt(name, option) {
    this.creator.injectedPrompts
      .find(f => {
        return f.name === name;
      })
      .choices.push(option);
  }
  // 在 promptModules 里用
  onPromptComplete(cb) {
    this.creator.promptCompleteCbs.push(cb);
  }
};

這里我們也簡單說一下,promptModules  回傳的是所有用于終端互動的模塊,其中會呼叫 injectFeatureinjectPrompt 來將互動配置插入進去,并且會通過 onPromptComplete  注冊一個回呼,

onPromptComplete 注冊回呼的形式是往 promptCompleteCbs 這個陣列中 push 了傳入的方法,可以猜測在所有互動完成之后應該會通過以下形式來呼叫回呼:

this.promptCompleteCbs.forEach(cb => cb(answers, preset));

回過來看這段代碼:

module.exports = class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    const promptAPI = new PromptModuleAPI(this);
    promptModules.forEach(m => m(promptAPI));
  }
};

Creator  的建構式中,實體化了一個 promptAPI  物件,并遍歷 prmptModules  把這個物件傳入了 promptModules  中,說明在實體化 Creator  的時候時候就會把所有用于互動的配置注冊好了,

這里我們注意到,在建構式中出現了四種 promptpresetPromptfeaturePromptinjectedPromptsoutroPrompts,具體有什么區別呢?下文有有詳細展開,

8.2 EventEmitter 事件模塊

首先, Creator  類是繼承于 Node.js 的 EventEmitter 類,眾所周知, events  是 Node.js 中最重要的一個模塊,而 EventEmitter 類就是其基礎,是 Node.js 中事件觸發與事件監聽等功能的封裝,

在這里, Creator  繼承自 EventEmitter , 應該就是為了方便在 create  程序中 emit  一些事件,整理了一下,主要就是以下 8 個事件:

this.emit('creation', { event: 'creating' }); // 創建
this.emit('creation', { event: 'git-init' }); // 初始化 git
this.emit('creation', { event: 'plugins-install' }); // 安裝插件
this.emit('creation', { event: 'invoking-generators' }); // 呼叫 generator
this.emit('creation', { event: 'deps-install' }); // 安裝額外的依賴
this.emit('creation', { event: 'completion-hooks' }); // 完成之后的回呼
this.emit('creation', { event: 'done' }); // create 流程結束
this.emit('creation', { event: 'fetch-remote-preset' }); // 拉取遠程 preset

我們知道事件 emit  一定會有 on  的地方,是哪呢?搜了一下原始碼,是在 @vue/cli-ui 這個包里,也就是說在終端命令列工具的場景下,不會觸發到這些事件,這里簡單了解一下即可:

const creator = new Creator('', cwd.get(), getPromptModules());
onCreationEvent = ({ event }) => {
  progress.set({ id: PROGRESS_ID, status: event, info: null }, context);
};
creator.on('creation', onCreationEvent);

簡單來說,就是通過 vue ui  啟動一個圖形化界面來初始化專案時,會啟動一個 server 端,和終端之間是存在通信的, server 端掛載了一些事件,在 create 的每個階段,會從 cli 中的方法觸發這些事件,

9. Preset(預設)

Creator  類的實體方法 create  接受兩個引數:

  • cliOptions:終端命令列傳入的引數
  • preset:Vue CLI 的預設

9.1 什么是 Preset(預設)

Preset 是什么呢?官方解釋是一個包含創建新專案所需預定義選項和插件的 JSON 物件,讓用戶無需在命令提示中選擇它們,比如:

{
  "useConfigFiles": true,
  "cssPreprocessor": "sass",
  "plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-eslint": {
      "config": "airbnb",
      "lintOn": ["save", "commit"]
    }
  },
  "configs": {
    "vue": {...},
    "postcss": {...},
    "eslintConfig": {...},
    "jest": {...}
  }
}

在 CLI 中允許使用本地的 preset 和遠程的 preset,

9.2 prompt

用過 inquirer 的朋友的對 prompt 這個單詞一定不陌生,它有 input / checkbox 等型別,是用戶和終端的互動,

我們回過頭來看一下在 Creator 中的一個方法 getPromptModules, 按照字面意思,這個方法是獲取了一些用于互動的模塊,具體來看一下:

exports.getPromptModules = () => {
  return [
    'vueVersion',
    'babel',
    'typescript',
    'pwa',
    'router',
    'vuex',
    'cssPreprocessors',
    'linter',
    'unit',
    'e2e',
  ].map(file => require(`../promptModules/${file}`));
};

看樣子是獲取了一系列的模塊,回傳了一個陣列,我看了一下這里列的幾個模塊,代碼格式基本都是統一的::

module.exports = cli => {
  cli.injectFeature({
    name: '',
    value: '',
    short: '',
    description: '',
    link: '',
    checked: true,
  });

  cli.injectPrompt({
    name: '',
    when: answers => answers.features.includes(''),
    message: '',
    type: 'list',
    choices: [],
    default: '2',
  });

  cli.onPromptComplete((answers, options) => {});
};

單獨看 injectFeatureinjectPrompt 的物件是不是和 inquirer 有那么一點神似?是的,他們就是用戶互動的一些配置選項,那 Feature  和 Prompt  有什么區別呢?

Feature:Vue CLI 在選擇自定義配置時的頂層選項:

Prompt:選擇具體 Feature 對應的二級選項,比如選擇了 Choose Vue version 這個 Feature,會要求用戶選擇是 2.x 還是 3.x:

onPromptComplete 注冊了一個回呼方法,在完成互動之后執行,

看來我們的猜測是對的, getPromptModules 方法就是獲取一些用于和用戶互動的模塊,比如:

  • babel:選擇是否使用 Babel
  • cssPreprocessors:選擇 CSS 的前處理器(Sass、Less、Stylus)
  • ...

先說到這里,后面在自定義配置加載的章節里會展開介紹 Vue CLI 用到的所有 prompt ,

9.3 獲取預設

我們具體來看一下獲取預設相關的邏輯,這部分代碼在 create  實體方法中:

// Creator.js
module.exports = class Creator extends EventEmitter {
  async create(cliOptions = {}, preset = null) {
    const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG;
    const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this;

    if (!preset) {
      if (cliOptions.preset) {
        // vue create foo --preset bar
        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone);
      } else if (cliOptions.default) {
        // vue create foo --default
        preset = defaults.presets.default;
      } else if (cliOptions.inlinePreset) {
        // vue create foo --inlinePreset {...}
        try {
          preset = JSON.parse(cliOptions.inlinePreset);
        } catch (e) {
          error(
            `CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`
          );
          exit(1);
        }
      } else {
        preset = await this.promptAndResolvePreset();
      }
    }
  }
};

可以看到,代碼中分別針對幾種情況作了處理:

  • cli 引數配了 --preset
  • cli 引數配了 --default
  • cli 引數配了 --inlinePreset
  • cli 沒配相關引數,默認獲取 Preset 的行為

前三種情況就不展開說了,我們來看一下第四種情況,也就是默認通過互動 prompt  來獲取 Preset 的邏輯,也就是 promptAndResolvePreset  方法,

先看一下實際用的時候是什么樣的:

我們可以猜測這里就是一段 const answers = await inquirer.prompt([])  代碼,

 async promptAndResolvePreset(answers = null) {
    // prompt
    if (!answers) {
      await clearConsole(true);
      answers = await inquirer.prompt(this.resolveFinalPrompts());
    }
    debug("vue-cli:answers")(answers);
 }

 resolveFinalPrompts() {
    this.injectedPrompts.forEach((prompt) => {
      const originalWhen = prompt.when || (() => true);
      prompt.when = (answers) => {
        return isManualMode(answers) && originalWhen(answers);
      };
    });

    const prompts = [
      this.presetPrompt,
      this.featurePrompt,
      ...this.injectedPrompts,
      ...this.outroPrompts,
    ];
    debug("vue-cli:prompts")(prompts);
    return prompts;
 }

是的,我們猜的沒錯,將 this.resolveFinalPrompts  里的配置進行互動,而 this.resolveFinalPrompts  方法其實就是將在 Creator  的建構式里初始化的那些 prompts  合到一起了,上文也提到了有這四種 prompt,在下一節展開介紹,
**

9.4 保存預設

在 Vue CLI 的最后,會讓用戶選擇 save this as a preset for future?,如果用戶選擇了 Yes,就會執行相關邏輯將這次的互動結果保存下來,這部分邏輯也是在 promptAndResolvePreset 中,

async promptAndResolvePreset(answers = null)  {
  if (
    answers.save &&
    answers.saveName &&
    savePreset(answers.saveName, preset)
  ) {
    log();
    log(
      `??  Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(
        rcPath
      )}`
    );
  }
}

在呼叫 savePreset 之前還會對預設進行決議、校驗等,就不展開了,直接來看一下 savePreset 方法:

exports.saveOptions = toSave => {
  const options = Object.assign(cloneDeep(exports.loadOptions()), toSave);
  for (const key in options) {
    if (!(key in exports.defaults)) {
      delete options[key];
    }
  }
  cachedOptions = options;
  try {
    fs.writeFileSync(rcPath, JSON.stringify(options, null, 2));
    return true;
  } catch (e) {
    error(
      `Error saving preferences: ` +
        `make sure you have write access to ${rcPath}.\n` +
        `(${e.message})`
    );
  }
};

exports.savePreset = (name, preset) => {
  const presets = cloneDeep(exports.loadOptions().presets || {});
  presets[name] = preset;
  return exports.saveOptions({ presets });
};

代碼很簡單,先深拷貝一份 Preset(這里直接用的 lodash 的 clonedeep),然后進過一些 merge 的操作之后就 writeFileSync 到上文有提到的 .vuerc 檔案了,

10. 自定義配置加載

這四種 prompt  分別對應的是預設選項、自定義 feature 選擇、具體 feature 選項和其它選項,它們之間存在互相關聯、層層遞進的關系,結合這四種 prompt,就是 Vue CLI 展現開用戶面前的所有互動了,其中也包含自定義配置的加載,

10.1 presetPrompt: 預設選項

也就是最初截圖里看到的哪三個選項,選擇 Vue2 還是 Vue3 還是自定義 feature

如果選擇了 Vue2  或者 Vue3 ,則后續關于 preset  所有的 prompt  都會終止,

10.2 featurePrompt: 自定義 feature 選項

**
如果在 presetPrompt  中選擇了 Manually,則會繼續選擇 feature

featurePrompt  就是存盤的這個串列,對應的代碼是這樣的:

const isManualMode = answers => answers.preset === '__manual__';

const featurePrompt = {
  name: 'features',
  when: isManualMode,
  type: 'checkbox',
  message: 'Check the features needed for your project:',
  choices: [],
  pageSize: 10,
};

在代碼中可以看到,在 isManualMode  的時候才會彈出這個互動,

10.3 injectedPrompts: 具體 feature 選項

featurePrompt  只是提供了一個一級串列,當用戶選擇了 Vue Version / Babel / TypeScript  等選項之后,會彈出新的互動,比如 Choose Vue version

injectedPrompts  就是存盤的這些具體選項的串列,也就是上文有提到通過 getPromptModules 方法在 promptModules  目錄獲取到的那些 prompt  模塊:

對應的代碼可以再回顧一下:

cli.injectPrompt({
  name: 'vueVersion',
  when: answers => answers.features.includes('vueVersion'),
  message: 'Choose a version of Vue.js that you want to start the project with',
  type: 'list',
  choices: [
    {
      name: '2.x',
      value: '2',
    },
    {
      name: '3.x (Preview)',
      value: '3',
    },
  ],
  default: '2',
});

可以看到,在 answers => answers.features.includes('vueVersion'),也就是 featurePrompt 的互動結果中如果包含 vueVersion  就會彈出具體選擇 Vue Version  的互動,

10.4 outroPrompts: 其它選項

**
這里存盤的就是一些除了上述三類選項之外的選專案前包含三個:

**Where do you prefer placing config for Babel, ESLint, etc.? **Babel,ESLint 等組態檔如何存盤?

  • In dedicated config files,單獨保存在各自的組態檔中,
  • In package.json,統一存盤在 package.json 中,

**Save this as a preset for future projects? **是否保存這次 Preset 以便之后直接使用,

如果你選擇了 Yes,則會再出來一個互動:Save preset as 輸入 Preset 的名稱

10.5 總結:Vue CLI 互動流程

這里總結一下 Vue CLI 的整體互動,也就是 prompt  的實作,

也就是文章最開始的時候提到,Vue CLI 支持默認配置之外,也支持自定義配置(Babel、TS 等),這樣一個互動流程是如何實作的,

Vue CLI 將所有互動分為四大類:

從預設選項到具體 feature 選項,它們是一個層層遞進的關系,不同的時機和選擇會觸發不同的互動,

Vue CLI 這里在代碼架構上的設計值得學習,將各個互動維護在不同的模塊中,通過統一的一個 prmoptAPI  實體在 Creator  實體初始化的時候,插入到不同的 prompt  中,并且注冊各自的回呼函式,這樣設計對于 prompt  而言是完全解耦的,洗掉某一項 prompt  對于背景關系的影響可以忽略不計,

好了,關于預設(Preset)和互動(Prompt)到這里基本分析完了,剩下的一些細節問題就不再展開了,

這里涉及到的相關原始碼檔案有,大家可以自行看一下:

  • Creator.js
  • PromptModuleAPI.js
  • utils/createTools.js
  • promptModules
  • ...

11. 初始化專案基礎檔案

當用戶選完所有互動之后,CLI 的下一步職責就是根據用戶的選項去生成對應的代碼了,這也是 CLI 的核心功能之一,

11.1 初始化 package.json 檔案

根據用戶的選項會掛載相關的 vue-cli-plugin,然后用于生成 package.json  的依賴 devDependencies,比如 @vue/cli-service / @vue/cli-plugin-babel / @vue/cli-plugin-eslint  等,

Vue CLI 會現在創建目錄下寫入一個基礎的 package.json :

{
  "name": "a",
  "version": "0.1.0",
  "private": true,
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0"
  }
}

11.2 初始化 Git

根據傳入的引數和一系列的判斷,會在目標目錄下初始化 Git 環境,簡單來說就是執行一下 git init

await run('git init');

具體是否初始化 Git 環境是這樣判斷的:

shouldInitGit(cliOptions) {
  // 如果全域沒安裝 Git,則不初始化
  if (!hasGit()) {
    return false;
  }
  // 如果 CLI 有傳入 --git 引數,則初始化
  if (cliOptions.forceGit) {
    return true;
  }
  // 如果 CLI 有傳入 --no-git,則不初始化
  if (cliOptions.git === false || cliOptions.git === "false") {
    return false;
  }
  // 如果當前目錄下已經有 Git 環境,就不初始化
  return !hasProjectGit(this.context);
}

11.3 初始化 README.md

專案的 README.md  會根據背景關系動態生成,而不是寫死的一個檔案:

function generateReadme(pkg, packageManager) {
  return [
    `# ${pkg.name}\n`,
    '## Project setup',
    '```',
    `${packageManager} install`,
    '```',
    printScripts(pkg, packageManager),
    '### Customize configuration',
    'See [Configuration Reference](https://cli.vuejs.org/config/).',
    '',
  ].join('\n');
}

Vue CLI 創建的 README.md  會告知用戶如何使用這個專案,除了 npm install  之外,會根據 package.json  里的 scripts  引數來動態生成使用檔案,比如如何開發、構建和測驗:

const descriptions = {
  build: 'Compiles and minifies for production',
  serve: 'Compiles and hot-reloads for development',
  lint: 'Lints and fixes files',
  'test:e2e': 'Run your end-to-end tests',
  'test:unit': 'Run your unit tests',
};

function printScripts(pkg, packageManager) {
  return Object.keys(pkg.scripts || {})
    .map(key => {
      if (!descriptions[key]) return '';
      return [
        `\n### ${descriptions[key]}`,
        '```',
        `${packageManager} ${packageManager !== 'yarn' ? 'run ' : ''}${key}`,
        '```',
        '',
      ].join('\n');
    })
    .join('');
}

這里可能會有讀者問,為什么不直接拷貝一個 README.md  檔案過去呢?

  • 第一,Vue CLI 支持不同的包管理,對應安裝、啟動和構建腳本都是不一樣的,這個是需要動態生成的;
  • 第二,動態生成自由性更強,可以根據用戶的選項去生成對應的檔案,而不是大家都一樣,

11.4 安裝依賴

呼叫 ProjectManageinstall 方法安裝依賴,代碼不復雜:

 async install () {
   if (this.needsNpmInstallFix) {
     // 讀取 package.json
     const pkg = resolvePkg(this.context)
     // 安裝 dependencies
     if (pkg.dependencies) {
       const deps = Object.entries(pkg.dependencies).map(([dep, range]) => `${dep}@${range}`)
       await this.runCommand('install', deps)
     }
     // 安裝 devDependencies
     if (pkg.devDependencies) {
       const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) => `${dep}@${range}`)
       await this.runCommand('install', [...devDeps, '--save-dev'])
     }
     // 安裝 optionalDependencies
     if (pkg.optionalDependencies) {
       const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) => `${dep}@${range}`)
       await this.runCommand('install', [...devDeps, '--save-optional'])
     }
     return
   }
   return await this.runCommand('install', this.needsPeerDepsFix ? ['--legacy-peer-deps'] : [])
 }

簡單來說就是讀取 package.json 然后分別安裝 npm 的不同依賴,

這里的邏輯深入進去感徑訓是挺復雜的,我也沒仔細深入看,就不展開說了,,,

11.4.1 自動判斷 NPM 源

這里有一個有意思的點,關于安裝依賴時使用的 npm 倉庫源,如果用戶沒有指定安裝源,Vue CLI 會自動判斷是否使用淘寶的 NPM 安裝源,猜猜是如何實作的?

function shouldUseTaobao() {
  let faster
  try {
    faster = await Promise.race([
      ping(defaultRegistry),
      ping(registries.taobao)
    ])
  } catch (e) {
    return save(false)
  }

  if (faster !== registries.taobao) {
    // default is already faster
    return save(false)
  }

  const { useTaobaoRegistry } = await inquirer.prompt([
    {
      name: 'useTaobaoRegistry',
      type: 'confirm',
      message: chalk.yellow(
        ` Your connection to the default ${command} registry seems to be slow.\n` +
          `   Use ${chalk.cyan(registries.taobao)} for faster installation?`
      )
    }
  ])
  return save(useTaobaoRegistry);
}

Vue CLI 中會通過 Promise.race 去請求默認安裝源淘寶安裝源:
**

  • 如果先回傳的是淘寶安裝源,就會讓用戶確認一次,是否使用淘寶安裝源
  • 如果先回傳的是默認安裝源,就會直接使用默認安裝源

一般來說,肯定都是使用默認安裝源,但是考慮國內用戶,,咳咳,,為這個設計點贊,

15. Generator 生成代碼

除了 Creator  外,整個 Vue CLI 的第二大重要的類是 Generator,負責專案代碼的生成,來具體看看干了啥,

15.1 初始化插件

generate  方法中,最先執行的是一個 initPlugins  方法,代碼如下:

async initPlugins () {
  for (const id of this.allPluginIds) {
    const api = new GeneratorAPI(id, this, {}, rootOptions)
    const pluginGenerator = loadModule(`${id}/generator`, this.context)

    if (pluginGenerator && pluginGenerator.hooks) {
      await pluginGenerator.hooks(api, {}, rootOptions, pluginIds)
    }
  }
}

在這里會給每一個 package.json  里的插件初始化一個 GeneratorAPI  實體,將實體傳入對應插件的 generator  方法并執行,比如 @vue/cli-plugin-babel/generator.js

15.2 GeneratorAPI 類

Vue CLI 使用了一套基于插件的架構,如果你查閱一個新創建專案的 package.json,就會發現依賴都是以 @vue/cli-plugin- 開頭的,插件可以修改 webpack 的內部配置,也可以向 vue-cli-service 注入命令,在專案創建的程序中,絕大部分列出的特性都是通過插件來實作的,

剛剛提到,會往每一個插件的 generator  中傳入 GeneratorAPI  的實體,看看這個類提供了什么,

15.2.1 例子:@vue/cli-plugin-babel

為了不那么抽象,我們先拿 @vue/cli-plugin-babel 來看,這個插件比較簡單:

module.exports = api => {
  delete api.generator.files['babel.config.js'];

  api.extendPackage({
    babel: {
      presets: ['@vue/cli-plugin-babel/preset'],
    },
    dependencies: {
      'core-js': '^3.6.5',
    },
  });
};

這里 api  就是一個 GeneratorAPI 實體,這里用到了一個 extendPackage  方法:

// GeneratorAPI.js
// 刪減部分代碼,只針對 @vue/cli-plugin-babel 分析
extendPackage (fields, options = {}) {
  const pkg = this.generator.pkg
  const toMerge = isFunction(fields) ? fields(pkg) : fields
  // 遍歷傳入的引數,這里是 babel 和 dependencies 兩個物件
  for (const key in toMerge) {
    const value = https://www.cnblogs.com/axuebin/p/toMerge[key]
    const existing = pkg[key]
    // 如果 key 的名稱是 dependencies 和 devDependencies
    // 就通過 mergeDeps 方法往 package.json 合并依賴
    if (isObject(value) && (key ==='dependencies' || key === 'devDependencies')) {
      pkg[key] = mergeDeps(
        this.id,
        existing || {},
        value,
        this.generator.depSources,
        extendOptions
      )
    } else if (!extendOptions.merge || !(key in pkg)) {
      pkg[key] = value
    }
  }
}

這時候,默認的 package.json  就變成:

{
  "babel": {
    "presets": ["@vue/cli-plugin-babel/preset"]
  },
  "dependencies": {
    "core-js": "^3.6.5"
  },
  "devDependencies": {},
  "name": "test",
  "private": true,
  "version": "0.1.0"
}

看完這個例子,對于 GeneratorAPI  的實體做什么可能有些了解了,我們就來具體看看這個類的實體吧,

15.2.2 重要的幾個實體方法

先介紹幾個 GeneratorAPI  重要的實體方法,這里就只介紹功能,具體代碼就不看了,等等會用到,

  • extendPackage:拓展 package.json 配置
  • render:通過 ejs 渲染模板檔案
  • onCreateComplete: 注冊檔案寫入硬碟之后的回呼
  • genJSConfig: 將 json 檔案輸出成 js 檔案
  • injectImports: 向檔案中加入 import
  • ...

16. @vue/cli-service

上文已經看過一個 @vue/cli-plugin-babel  插件,對于 Vue CLI 的插件架構是不是有點感覺?也了解到一個比較重要的 GeneratorAPI  類,插件中的一些修改配置的功能都是這個類的實體方法,

接下來看一個比較重要的插件 @vue/cli-service,這個插件是 Vue CLI 的核心插件,和 create react app  的 react-scripts  類似,借助這個插件,我們應該能夠更深刻地理解 GeneratorAPI 以及 Vue CLI 的插件架構是如何實作的,

來看一下 @vue/cli-service  這個包下的 generator/index.js  檔案,這里為了分析方便,將原始碼拆解成多段,其實也就是分別呼叫了 GeneratorAPI  實體的不同方法:

16.1 渲染 template

api.render('./template', {
  doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript'),
});

template  目錄下的檔案通過 render  渲染到記憶體中,這里用的是 ejs  作為模板渲染引擎,

16.2 寫 package.json

通過 extendPackagepacakge.json 中寫入 Vue   的相關依賴:

if (options.vueVersion === '3') {
  api.extendPackage({
    dependencies: {
      vue: '^3.0.0',
    },
    devDependencies: {
      '@vue/compiler-sfc': '^3.0.0',
    },
  });
} else {
  api.extendPackage({
    dependencies: {
      vue: '^2.6.11',
    },
    devDependencies: {
      'vue-template-compiler': '^2.6.11',
    },
  });
}

通過 extendPackagepacakge.json 中寫入 scripts

api.extendPackage({
  scripts: {
    serve: 'vue-cli-service serve',
    build: 'vue-cli-service build',
  },
  browserslist: ['> 1%', 'last 2 versions', 'not dead'],
});

通過 extendPackagepacakge.json 中寫入 CSS 預處理引數:

if (options.cssPreprocessor) {
  const deps = {
    sass: {
      sass: '^1.26.5',
      'sass-loader': '^8.0.2',
    },
    'node-sass': {
      'node-sass': '^4.12.0',
      'sass-loader': '^8.0.2',
    },
    'dart-sass': {
      sass: '^1.26.5',
      'sass-loader': '^8.0.2',
    },
    less: {
      less: '^3.0.4',
      'less-loader': '^5.0.0',
    },
    stylus: {
      stylus: '^0.54.7',
      'stylus-loader': '^3.0.2',
    },
  };

  api.extendPackage({
    devDependencies: deps[options.cssPreprocessor],
  });
}

16.3 呼叫 router 插件和 vuex 插件

// for v3 compatibility
if (options.router && !api.hasPlugin('router')) {
  require('./router')(api, options, options);
}

// for v3 compatibility
if (options.vuex && !api.hasPlugin('vuex')) {
  require('./vuex')(api, options, options);
}

是不是很簡單,通過 GeneratorAPI  提供的實體方法,可以在插件中非常方便地對專案進行修改和自定義,

17. 抽取單獨組態檔

上文提到,通過 extendPackage  回往 package.json  中寫入一些配置,但是,上文也提到有一個互動是 Where do you prefer placing config for Babel, ESLint, etc.? 也就是會將配置抽取成單獨的檔案,generate  里的 extractConfigFiles  方法就是執行了這個邏輯,

extractConfigFiles(extractAll, checkExisting) {
  const configTransforms = Object.assign(
    {},
    defaultConfigTransforms,
    this.configTransforms,
    reservedConfigTransforms
  );
  const extract = (key) => {
    if (
      configTransforms[key] &&
      this.pkg[key] &&
      !this.originalPkg[key]
    ) {
      const value = https://www.cnblogs.com/axuebin/p/this.pkg[key];
      const configTransform = configTransforms[key];
      const res = configTransform.transform(
        value,
        checkExisting,
        this.files,
        this.context
      );
      const { content, filename } = res;
      this.files[filename] = ensureEOL(content);
      delete this.pkg[key];
    }
  };
  if (extractAll) {
    for (const key in this.pkg) {
      extract(key);
    }
  } else {
    extract("babel");
  }
}

這里的 configTransforms  就是一些會需要抽取的配置:

如果 extractAll  是 true,也就是在上面的互動中選了 Yes,就會將 package.json  里的所有 key configTransforms 比較,如果都存在,就將配置抽取到獨立的檔案中,

18. 將記憶體中的檔案輸出到硬碟

上文有提到,api.render  會通過 EJS 將模板檔案渲染成字串放在記憶體中,執行了 generate  的所有邏輯之后,記憶體中已經有了需要輸出的各種檔案,放在 this.files  里, generate  的最后一步就是呼叫 writeFileTree  將記憶體中的所有檔案寫入到硬碟,

到這里 generate  的邏輯就基本都講完了,Vue CLI 生成代碼的部分也就講完了,

19. 總結

整體看下來,Vue CLI 的代碼還是比較復雜的,整體架構條理還是比較清楚的,其中有兩點印象最深:

第一,整體的互動流程的掛載,將各個模塊的互動邏輯通過一個類的實體維護起來,執行時機和成功回呼等也是設計的比較好,

第二,插件機制很重要,插件機制將功能和腳手架進行解耦,

看來,無論是 create-react-app 還是 Vue CLI,在設計的時候都會盡量考慮插件機制,將能力開放出去再將功能集成進來,無論是對于 Vue CLI 本身的核心功能,還是對于社區開發者來說,都具備了足夠的開放性和擴展性,

整體代碼看下來,最重要的就是兩個概念:

  • Preset:預設,包括整體的互動流程(Prompt)
  • Plugin:插件,整體的插件系統

圍繞這兩個概念,代碼中的這幾個類:CreatorPromptModuleAPIGeneratorGeneratorAPI 就是核心,

簡單總結一下流程:

  1. 執行 vue create
  2. 初始化 Creator 實體 creator,掛載所有互動配置
  3. 呼叫 creator 的實體方法 create
  4. 詢問用戶自定義配置
  5. 初始化 Generator 實體 generator
  6. 初始化各種插件
  7. 執行插件的 generator 邏輯,寫 package.json、渲染模板等
  8. 將檔案寫入到硬碟

這樣一個 CLI 的生命周期就走完了,專案已經初始化好了,

附:Vue CLI 中可以直接拿來用的工具方法

看完 Vue CLI 的原始碼,除了感嘆這復雜的設計之外,也發現很多工具方法,在我們實作自己的 CLI 時,都是可以拿來即用的,在這里總結一下,

獲取 CLI 引數

決議 CLI 通過 -- 傳入的引數,

const program = require('commander');

function camelize(str) {
  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''));
}

function cleanArgs(cmd) {
  const args = {};
  cmd.options.forEach(o => {
    const key = camelize(o.long.replace(/^--/, ''));
    // if an option is not present and Command has a method with the same name
    // it should not be copied
    if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
      args[key] = cmd[key];
    }
  });
  return args;
}

檢查 Node 版本

通過 semver.satisfies 比較兩個 Node 版本:

  • process.version: 當前運行環境的 Node 版本
  • wanted: package.json 里配置的 Node 版本
const requiredVersion = require('../package.json').engines.node;

function checkNodeVersion(wanted, id) {
  if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
    console.log(
      chalk.red(
        'You are using Node ' +
          process.version +
          ', but this version of ' +
          id +
          ' requires Node ' +
          wanted +
          '.\nPlease upgrade your Node version.'
      )
    );
    process.exit(1);
  }
}

checkNodeVersion(requiredVersion, '@vue/cli');

讀取 package.json

const fs = require('fs');
const path = require('path');

function getPackageJson(cwd) {
  const packagePath = path.join(cwd, 'package.json');

  let packageJson;
  try {
    packageJson = fs.readFileSync(packagePath, 'utf-8');
  } catch (err) {
    throw new Error(`The package.json file at '${packagePath}' does not exist`);
  }

  try {
    packageJson = JSON.parse(packageJson);
  } catch (err) {
    throw new Error('The package.json is malformed');
  }

  return packageJson;
}

物件排序

這里主要是在輸出 package.json 的時候可以對輸出的物件先進行排序,更美觀一些,,

module.exports = function sortObject(obj, keyOrder, dontSortByUnicode) {
  if (!obj) return;
  const res = {};

  if (keyOrder) {
    keyOrder.forEach(key => {
      if (obj.hasOwnProperty(key)) {
        res[key] = obj[key];
        delete obj[key];
      }
    });
  }

  const keys = Object.keys(obj);

  !dontSortByUnicode && keys.sort();
  keys.forEach(key => {
    res[key] = obj[key];
  });

  return res;
};

輸出檔案到硬碟

這個其實沒啥,就是三步:

  • fs.unlink 洗掉檔案
  • fs.ensureDirSync 創建目錄
  • fs.writeFileSync 寫檔案
const fs = require('fs-extra');
const path = require('path');

// 洗掉已經存在的檔案
function deleteRemovedFiles(directory, newFiles, previousFiles) {
  // get all files that are not in the new filesystem and are still existing
  const filesToDelete = Object.keys(previousFiles).filter(
    filename => !newFiles[filename]
  );

  // delete each of these files
  return Promise.all(
    filesToDelete.map(filename => {
      return fs.unlink(path.join(directory, filename));
    })
  );
}

// 輸出檔案到硬碟
module.exports = async function writeFileTree(dir, files, previousFiles) {
  if (previousFiles) {
    await deleteRemovedFiles(dir, files, previousFiles);
  }
  // 主要就是這里
  Object.keys(files).forEach(name => {
    const filePath = path.join(dir, name);
    fs.ensureDirSync(path.dirname(filePath));
    fs.writeFileSync(filePath, files[name]);
  });
};

判斷專案是否初始化 git

其實就是在目錄下執行 git status 看是否報錯,

const hasProjectGit = cwd => {
  let result;
  try {
    execSync('git status', { stdio: 'ignore', cwd });
    result = true;
  } catch (e) {
    result = false;
  }
  return result;
};

物件的 get 方法

可以用 lodash,現在可以直接用 a?.b?.c 就好了

function get(target, path) {
  const fields = path.split('.');
  let obj = target;
  const l = fields.length;
  for (let i = 0; i < l - 1; i++) {
    const key = fields[i];
    if (!obj[key]) {
      return undefined;
    }
    obj = obj[key];
  }
  return obj[fields[l - 1]];
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/278772.html

標籤:JavaScript

上一篇:前端資料結構--線性結構-佇列、堆疊

下一篇:使用Vue-TreeSelect組件的時候,用watch變數方式解決彈出編輯對話框界面無法觸發更新的問題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more