diff --git a/.env.production b/.env.production index 57c73d26..d0a2a6b3 100644 --- a/.env.production +++ b/.env.production @@ -9,3 +9,7 @@ VITE_GLOB_API_URL=/api # Interface prefix VITE_GLOB_API_URL_PREFIX= + + +# TODO use Cdn +VITE_USE_CDN = true diff --git a/README.md b/README.md index 19bc2ff9..ba477d48 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,7 @@ yarn clean:lib # 删除node_modules,兼容window系统 - [x] 图表库 - [x] 数字动画 - [x] 首屏加载等待动画 +- [x] 抽取生产环境配置文件 ## 正在开发的功能 @@ -228,7 +229,7 @@ yarn clean:lib # 删除node_modules,兼容window系统 - [ ] 主题配置 - [ ] 黑暗主题 - [ ] 打包 Gzip -- [ ] 抽取生产环境配置文件 +- [ ] 打包 CDN - [ ] 系统性能优化 更多组件/功能/建议/bug/欢迎提交 pr 或者 issue diff --git a/build/config/vite/cdn.ts b/build/config/vite/cdn.ts new file mode 100644 index 00000000..0538e1a4 --- /dev/null +++ b/build/config/vite/cdn.ts @@ -0,0 +1,21 @@ +const css = ['//cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.css']; + +// TODO use esm? +const js = [ + '//cdn.bootcdn.net/ajax/libs/vue/3.0.0/vue.global.prod.js', + '//cdn.bootcdn.net/ajax/libs/vue-router/4.0.0-beta.13/vue-router.global.min.js', + '//cdn.bootcdn.net/ajax/libs/vuex/4.0.0-beta.4/vuex.global.prod.js', + '//cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.min.js', + '//cdn.bootcdn.net/ajax/libs/qs/6.9.4/qs.min.js', + '//cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.js', + // '//cdn.bootcdn.net/ajax/libs/lodash.js/4.17.15/lodash.min.js', + // '//cdn.bootcdn.net/ajax/libs/crypto-js/3.3.0/crypto-js.min.js', + // '//cdn.bootcdn.net/ajax/libs/vue-i18n/8.18.1/vue-i18n.min.js', +]; + +export const externals = ['vue', 'vuex', 'vue-router', 'axios', 'qs', 'nprogress']; + +export const cdnConf = { + css, + js, +}; diff --git a/build/config/vite/env.ts b/build/config/vite/env.ts deleted file mode 100644 index 313f446c..00000000 --- a/build/config/vite/env.ts +++ /dev/null @@ -1,10 +0,0 @@ -import moment from 'moment'; -// @ts-ignore -import pkg from '../../../package.json'; -export function setupBasicEnv() { - // version - process.env.VITE_VERSION = (pkg as any).version; - // build time - process.env.VITE_APP_BUILD_TIME = moment().format('YYYY-MM-DD HH:mm:ss'); - process.env.VITE_BUILD_SHORT_TIME = moment().format('MMDDHHmmss'); -} diff --git a/build/constant.ts b/build/constant.ts new file mode 100644 index 00000000..5e02f35c --- /dev/null +++ b/build/constant.ts @@ -0,0 +1 @@ +export const GLOB_CONFIG_FILE_NAME = '_app.config.js'; diff --git a/build/getShortName.ts b/build/getShortName.ts new file mode 100644 index 00000000..6a52fc34 --- /dev/null +++ b/build/getShortName.ts @@ -0,0 +1,5 @@ +export const getShortName = (env: any) => { + return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__` + .toUpperCase() + .replace(/\s/g, ''); +}; diff --git a/build/jsc.js b/build/jsc.js new file mode 100644 index 00000000..d39ce932 --- /dev/null +++ b/build/jsc.js @@ -0,0 +1,36 @@ +// js调用cli 兼容调用ts + +const { sh } = require('tasksfile'); +const { argv } = require('yargs'); + +let command = ``; + +Object.keys(argv).forEach((key) => { + if (!/^\$/.test(key) && key !== '_') { + // @ts-ignore + if (argv[key]) { + command += `--${key} `; + } + } +}); + +// 执行任务名称 +let taskList = argv._; + +let NODE_ENV = process.env.NODE_ENV || 'development'; + +if (taskList.includes('build') || taskList.includes('report') || taskList.includes('preview')) { + NODE_ENV = 'production'; +} + +if (taskList && Array.isArray(taskList) && taskList.length) { + sh( + `cross-env NODE_ENV=${NODE_ENV} ts-node --project ./build/tsconfig.json ./build/script/cli.ts ${taskList.join( + ' ' + )} ${command}`, + { + async: true, + nopipe: true, + } + ); +} diff --git a/build/script/build.ts b/build/script/build.ts new file mode 100644 index 00000000..a87373a1 --- /dev/null +++ b/build/script/build.ts @@ -0,0 +1,28 @@ +// #!/usr/bin/env node + +import { sh } from 'tasksfile'; +import { argv } from 'yargs'; +import { runBuildConfig } from './buildConf'; +import { runUpdateHtml } from './updateHtml'; +import { errorConsole, successConsole } from '../utils'; + +export const runBuild = async () => { + try { + const argvList = argv._; + let cmd = `cross-env NODE_ENV=production vite build`; + await sh(cmd, { + async: true, + nopipe: true, + }); + + // Generate configuration file + if (!argvList.includes('no-conf')) { + await runBuildConfig(); + } + await runUpdateHtml(); + successConsole('Vite Build successfully!'); + } catch (error) { + errorConsole('Vite Build Error\n' + error); + process.exit(1); + } +}; diff --git a/build/script/buildConf.ts b/build/script/buildConf.ts new file mode 100644 index 00000000..b80cafc0 --- /dev/null +++ b/build/script/buildConf.ts @@ -0,0 +1,44 @@ +import { GLOB_CONFIG_FILE_NAME } from '../constant'; +import fs, { writeFileSync } from 'fs-extra'; + +import viteConfig from '../../vite.config'; +import { errorConsole, successConsole, getCwdPath, getEnvConfig } from '../utils'; + +const getShortName = (env: any) => { + return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__` + .toUpperCase() + .replace(/\s/g, ''); +}; + +function createConfig( + { + configName, + config, + configFileName = GLOB_CONFIG_FILE_NAME, + }: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} } +) { + try { + const windowConf = `window.${configName}`; + const outDir = viteConfig.outDir || 'dist'; + const configStr = `${windowConf}=${JSON.stringify(config)}; + + Object.freeze(${windowConf}); + Object.defineProperty(window, "${configName}", { + configurable: false, + writable: false, + }); + `; + fs.mkdirp(getCwdPath(outDir)); + writeFileSync(getCwdPath(`${outDir}/${configFileName}`), configStr); + + successConsole('The configuration file is build successfully!'); + } catch (error) { + errorConsole('Configuration file configuration file failed to package\n' + error); + } +} + +export function runBuildConfig() { + const config = getEnvConfig(); + const configFileName = getShortName(config); + createConfig({ config, configName: configFileName }); +} diff --git a/build/script/changelog.ts b/build/script/changelog.ts index 19d6d448..65f3b0c1 100644 --- a/build/script/changelog.ts +++ b/build/script/changelog.ts @@ -1,14 +1,11 @@ // #!/usr/bin/env node import { sh } from 'tasksfile'; -import chalk from 'chalk'; +import { errorConsole, successConsole } from '../utils'; -const createChangeLog = async () => { +export const runChangeLog = async () => { try { let cmd = `conventional-changelog -p custom-config -i CHANGELOG.md -s -r 0 `; - // if (shell.which('git')) { - // cmd += '&& git add CHANGELOG.md'; - // } await sh(cmd, { async: true, nopipe: true, @@ -18,21 +15,10 @@ const createChangeLog = async () => { async: true, nopipe: true, }); - console.log( - chalk.blue.bold('**************** ') + - chalk.green.bold('CHANGE_LOG generated successfully!') + - chalk.blue.bold(' ****************') - ); + successConsole('CHANGE_LOG.md generated successfully!'); } catch (error) { - console.log( - chalk.blue.red('**************** ') + - chalk.green.red('CHANGE_LOG generated error\n' + error) + - chalk.blue.red(' ****************') - ); + errorConsole('CHANGE_LOG.md generated error\n' + error); + process.exit(1); } }; -createChangeLog(); -module.exports = { - createChangeLog, -}; diff --git a/build/script/cli.ts b/build/script/cli.ts new file mode 100644 index 00000000..17f5b84b --- /dev/null +++ b/build/script/cli.ts @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import { argv } from 'yargs'; + +import { runChangeLog } from './changelog'; +import { runPostInstall } from './postinstall'; +import { runPreview } from './preview'; +import { runPreserve } from './preserve'; +import { runBuild } from './build'; + +const task = (argv._ || [])[0]; + +console.log('Run Task: ' + chalk.cyan(task)); + +switch (task) { + // change log + case 'log': + runChangeLog(); + break; + + case 'build': + runBuild(); + break; + + case 'preserve': + runPreserve(); + break; + + case 'postinstall': + runPostInstall(); + break; + + case 'preview': + runPreview(); + break; + + // TODO + case 'gzip': + break; + default: + break; +} + +export default {}; diff --git a/build/script/hm.ts b/build/script/hm.ts new file mode 100644 index 00000000..2c3f3b6b --- /dev/null +++ b/build/script/hm.ts @@ -0,0 +1,12 @@ +// 百度统计代码 用于站点部署 +// 只在build:site开启 +export const hmScript = ` +`; diff --git a/build/script/postinstall.ts b/build/script/postinstall.ts index 242489f3..dd02de40 100644 --- a/build/script/postinstall.ts +++ b/build/script/postinstall.ts @@ -2,9 +2,14 @@ import { exec, which } from 'shelljs'; function ignoreCaseGit() { try { - if (which('git')) { + if (which('git').code === 0) { exec('git config core.ignorecase false '); } - } catch (error) {} + } catch (error) { + console.log(error); + } +} + +export function runPostInstall() { + ignoreCaseGit(); } -ignoreCaseGit(); diff --git a/build/script/preserve.ts b/build/script/preserve.ts index 4f19e503..cd4beaef 100644 --- a/build/script/preserve.ts +++ b/build/script/preserve.ts @@ -1,53 +1,57 @@ -// 是否需要更新依赖,防止package.json更新了依赖,其他人获取代码后没有install +// Do you need to update the dependencies to prevent package.json from updating the dependencies, and no install after others get the code import path from 'path'; import fs from 'fs-extra'; import { isEqual } from 'lodash'; -import chalk from 'chalk'; import { sh } from 'tasksfile'; +import { successConsole, errorConsole } from '../utils'; const resolve = (dir: string) => { return path.resolve(process.cwd(), dir); }; +const reg = /[\u4E00-\u9FA5\uF900-\uFA2D]/; + let NEED_INSTALL = false; -fs.mkdirp(resolve('build/.cache')); -function checkPkgUpdate() { - const pkg = require('../../package.json'); - const { dependencies, devDependencies } = pkg; - const depsFile = resolve('build/.cache/deps.json'); - if (!fs.pathExistsSync(depsFile)) { - NEED_INSTALL = true; - return; +export async function runPreserve() { + const cwdPath = process.cwd(); + if (reg.test(cwdPath)) { + errorConsole( + 'Do not include Chinese, Japanese or Korean in the full path of the project directory, please modify the directory name and run again!' + ); + errorConsole('项目目录全路径请勿包含中文、日文、韩文,请修改目录名后再次重新运行!'); + process.exit(1); } - const depsJson = require('../.cache/deps.json'); - if (!isEqual(depsJson, { dependencies, devDependencies })) { - NEED_INSTALL = true; + fs.mkdirp(resolve('build/.cache')); + function checkPkgUpdate() { + const pkg = require('../../package.json'); + const { dependencies, devDependencies } = pkg; + const depsFile = resolve('build/.cache/deps.json'); + if (!fs.pathExistsSync(depsFile)) { + NEED_INSTALL = true; + return; + } + const depsJson = require('../.cache/deps.json'); + + if (!isEqual(depsJson, { dependencies, devDependencies })) { + NEED_INSTALL = true; + } } -} -checkPkgUpdate(); - -(async () => { + checkPkgUpdate(); if (NEED_INSTALL) { - console.log( - chalk.blue.bold('**************** ') + - chalk.red.bold('检测到依赖变化,正在安装依赖(Tip: 项目首次运行也会执行)!') + - chalk.blue.bold(' ****************') + // no error + successConsole( + 'A dependency change is detected, and the dependency is being installed to ensure that the dependency is consistent! (Tip: The project will be executed for the first time)!' ); try { - // 从代码执行貌似不会自动读取.npmrc 所以手动加上源地址 - // await run('yarn install --registry=https://registry.npm.taobao.org ', { - await sh('yarn install ', { + await sh('npm run bootstrap ', { async: true, nopipe: true, }); - console.log( - chalk.blue.bold('**************** ') + - chalk.green.bold('依赖安装成功,正在运行!') + - chalk.blue.bold(' ****************') - ); + + successConsole('Dependency installation is successful, start running the project!'); const pkg = require('../../package.json'); const { dependencies, devDependencies } = pkg; @@ -64,4 +68,4 @@ checkPkgUpdate(); } } catch (error) {} } -})(); +} diff --git a/build/script/preview.ts b/build/script/preview.ts index 468b6fad..2ca2ca53 100644 --- a/build/script/preview.ts +++ b/build/script/preview.ts @@ -11,7 +11,7 @@ import { getIPAddress } from '../utils'; const BUILD = 1; const NO_BUILD = 2; -// 启动服务器 +// start server const startApp = () => { const port = 9680; portfinder.basePort = port; @@ -23,7 +23,6 @@ const startApp = () => { if (err) { throw err; } else { - // const publicPath = process.env.BASE_URL; app.listen(port, function () { const empty = ' '; const common = `The preview program is already running: @@ -36,7 +35,7 @@ const startApp = () => { }); }; -const preview = async () => { +export const runPreview = async () => { const prompt = inquirer.prompt({ type: 'list', message: 'Please select a preview method', @@ -61,7 +60,3 @@ const preview = async () => { } startApp(); }; - -(() => { - preview(); -})(); diff --git a/build/script/updateHtml.ts b/build/script/updateHtml.ts new file mode 100644 index 00000000..38286e29 --- /dev/null +++ b/build/script/updateHtml.ts @@ -0,0 +1,98 @@ +import { readFileSync, writeFileSync, existsSync } from 'fs-extra'; +import viteConfig, { htmlConfig } from '../../vite.config'; +import { getCwdPath, successConsole, errorConsole } from '../utils'; +import { GLOB_CONFIG_FILE_NAME } from '../constant'; +import { hmScript } from './hm'; +const pkg = require('../../package.json'); + +const { title, addHm, cdnConf, useCdn } = htmlConfig; + +function injectTitle(html: string, htmlTitle: string) { + if (/<\/title>/.test(html)) { + return html.replace(/<\/title>/, `${htmlTitle}`); + } + return html; +} + +function injectConfigScript(html: string) { + const tag = `\t\t`; + + if (/<\/head>/.test(html)) { + return html.replace(/<\/head>/, `${tag}\n\t\t`); + } + return html; +} + +function injectHmScript(html: string) { + if (/
/.test(html)) { + return html.replace(//, `\n${hmScript}`); + } + return html; +} + +function injectCdnCss(html: string) { + if (!cdnConf) return html; + const { css } = cdnConf; + if (!css || css.length === 0) return html; + + let cdnCssTag = ''; + for (const cssLink of css) { + cdnCssTag += ``; + } + if (/<\/head>/.test(html)) { + return html.replace(/<\/head>/, `${cdnCssTag}\n\t\t`); + } + return html; +} + +function injectCdnjs(html: string) { + if (!cdnConf) return html; + const { js } = cdnConf; + if (!js || js.length === 0) return html; + + let cdnJsTag = ''; + for (const src of js) { + // TODO + // + cdnJsTag += `\t\n`; + } + if (/<\/body>/.test(html)) { + return html.replace(/<\/body>/, `${cdnJsTag}\n`); + } + return html; +} + +export async function runUpdateHtml() { + const outDir = viteConfig.outDir || 'dist'; + const indexPath = getCwdPath(outDir, 'index.html'); + if (!existsSync(`${indexPath}`)) { + return; + } + try { + let processedHtml = ''; + const rawHtml = readFileSync(indexPath, 'utf-8'); + processedHtml = rawHtml; + processedHtml = injectTitle(processedHtml, title); + processedHtml = injectConfigScript(processedHtml); + if (addHm) { + processedHtml = injectHmScript(processedHtml); + } + if (useCdn) { + processedHtml = injectCdnCss(processedHtml); + processedHtml = injectCdnjs(processedHtml); + } + + writeFileSync(indexPath, processedHtml); + successConsole('Update Html Successfully!'); + } catch (error) { + errorConsole('Update Html Error\n' + error); + } +} diff --git a/build/utils.ts b/build/utils.ts index a210dc04..9c44a0e4 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -1,6 +1,9 @@ import fs from 'fs'; +import path from 'path'; import { networkInterfaces } from 'os'; import dotenv from 'dotenv'; +import chalk from 'chalk'; + export const isFunction = (arg: unknown): arg is (...args: any[]) => any => typeof arg === 'function'; export const isRegExp = (arg: unknown): arg is RegExp => @@ -72,6 +75,8 @@ export interface ViteEnv { VITE_USE_MOCK: boolean; VITE_PUBLIC_PATH: string; VITE_PROXY: [string, string][]; + VITE_GLOB_APP_TITLE: string; + VITE_USE_CDN: boolean; } export function loadEnv(): ViteEnv { @@ -100,3 +105,49 @@ export function loadEnv(): ViteEnv { } return ret; } + +export function getEnvConfig(match = 'VITE_GLOB_', confFiles = ['.env', '.env.production']) { + let envConfig = {}; + confFiles.forEach((item) => { + try { + const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item))); + + envConfig = { ...envConfig, ...env }; + } catch (error) {} + }); + Object.keys(envConfig).forEach((key) => { + const reg = new RegExp(`^(${match})`); + if (!reg.test(key)) { + Reflect.deleteProperty(envConfig, key); + } + }); + return envConfig; +} + +export function successConsole(message: any) { + console.log( + chalk.blue.bold('**************** ') + + chalk.green.bold('✨ ' + message) + + chalk.blue.bold(' ****************') + ); +} + +export function errorConsole(message: any) { + console.log( + chalk.blue.bold('**************** ') + + chalk.red.bold('✨ ' + message) + + chalk.blue.bold(' ****************') + ); +} + +export function warnConsole(message: any) { + console.log( + chalk.blue.bold('**************** ') + + chalk.yellow.bold('✨ ' + message) + + chalk.blue.bold(' ****************') + ); +} + +export function getCwdPath(...dir: string[]) { + return path.resolve(process.cwd(), ...dir); +} diff --git a/getEnvConfig.ts b/getEnvConfig.ts deleted file mode 100644 index 5da9a57d..00000000 --- a/getEnvConfig.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { GlobEnvConfig } from './src/types/config'; - -export const getGlobEnvConfig = (): GlobEnvConfig => { - const env = import.meta.env; - return (env as unknown) as GlobEnvConfig; -}; diff --git a/index.html b/index.html index 84ba684a..6c1daa94 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,14 @@ + + + +