vue-cil 3.0 配置說明

1 安裝vue-cli3.0

0 淘寶鏡像

npm install -g cnpm --registry=https://registry.npm.taobao.org
複製代碼

1 安裝vue-cli

npm install -g @vue/cli
# OR
yarn global add @vue/cli
複製代碼

2 查看版本

vue --version
複製代碼

3 vue-cli構建項目

vue create vue-demo
複製代碼

image.png

選擇默認便可、可起服務javascript

4 目錄結構

├── README.md  					# 說明
|-- dist                       	# 打包後文件夾
├── babel.config.js 			# babel語法編譯
├── package-lock.json 
├── package.json
├── public						# 靜態文件夾
│   ├── favicon.ico
│   └── index.html				#入口頁面
└── src						    # 源碼目錄
 ├── App.vue - 頁面
 ├── assets - 靜態目錄
 │   └── logo.png
 ├── components 組件
 │   └── HelloWorld.vue
 └── main.js # 入口文件,加載公共組件
|-- vue.config.js                # 配置文件 
|-- .eslintrc.js    		  	# ES-lint校驗                   
|-- .gitignore          		# git忽略上傳的文件格式   
|-- babel.config.js   			# babel語法編譯                        
|-- package.json       	     # 項目基本信息 
複製代碼

標準的vue目錄結構css

2 環境變量和模式

0 介紹

在npm下實際上vue-cli  啓動的時候,已經肯定了環境變量與模式,這樣方便代碼的構建html

打開package.json後vue

image.png

咱們這邊描述的環境就在scripts中java

1 模式

是 Vue CLI 項目中一個重要的概念。默認狀況下,一個 Vue CLI 項目有三個模式:node

  • development 模式用於 vue-cli-service serve
  • production 模式用於 vue-cli-service build 和 vue-cli-service test:e2e
  • test 模式用於 vue-cli-service test:unit

你能夠經過傳遞 --mode 選項參數爲命令行覆寫默認的模式。例如,若是你想要在構建命令中使用開發環境變量,請在你的 package.json 腳本中加入webpack


``` "dev-build": "vue-cli-service build --mode development", ```

2 調用

那麼咱們在代碼裏面怎麼調用呢、和他的應用場景呢ios

process.env.NODE_ENV
複製代碼

這樣咱們就能夠獲取他的環境變量nginx

3 場景

咱們在src目錄下新建一個config
構建一個env.js的目錄git

let baseUrl = '';

const env = process.env
if (env.NODE_ENV == 'development') {
    baseUrl = `http://192.168.1.1`; // 開發環境地址
} else if (env.NODE_ENV == 'production') {
    baseUrl = `http://192.168.1.2`; //生產環境地址
} else if (env.NODE_ENV == 'test') {
    baseUrl = `http://192.168.1.3`; //測試環境地址
}

export {
    baseUrl,
    env
}
複製代碼

那麼這樣咱們就能夠直接使用不一樣環境下的

3 IE兼容處理、移除console

npm install @babel/polyfill -s

npm install babel-plugin-transform-remove-console -s
複製代碼

在babel.config.js中配置以下

const plugins = []
if (process.env.NODE_ENV === 'production') {
  // 移除console.log
  plugins.push('transform-remove-console')
}

module.exports = {
  presets: [
    ['@vue/app', {
      polyfills: [
        'es6.array.iterator',
        'es6.promise',
        'es7.promise.finally',
        'es6.symbol',
        'es6.array.find-index',
        'es7.array.includes',
        'es6.string.includes',
        'es6.array.find',
        'es6.object.assign'
      ]
    }]
  ],
  plugins
}
複製代碼

4 vue-cli 基本配置

新建一個vue.config.js的文件夾,開始可配置vue-cli

module.exports = {
    //部署應用包時的基本 URL
    publicPath: process.env.NODE_ENV === 'production' ? '/online/' : './',
    //當運行 vue-cli-service build 時生成的生產環境構建文件的目錄
    outputDir: 'dist',
    //放置生成的靜態資源 (js、css、img、fonts) 的 (相對於 outputDir 的) 目錄
    assetsDir: 'assets',
    // eslint-loader 是否在保存的時候檢查 安裝@vue/cli-plugin-eslint有效
    lintOnSave: true,
    //是否使用包含運行時編譯器的 Vue 構建版本。設置true後你就能夠在使用template
    runtimeCompiler: true,
    // 生產環境是否生成 sourceMap 文件 sourceMap的詳解請看末尾 
    productionSourceMap: false,

}
複製代碼

5 添加別名

新建一個vue.config.js的文件夾,開始可配置vue-cli

1 別名配置

const path =  require('path'); //引入path模塊(node)
const resolve = (dir) => path.join(__dirname, dir); //將文件組成絕對路徑
 
module.exports = {
    chainWebpack: config => {
        // 添加別名
        config.resolve.alias
          .set('@', resolve('src'))
          .set('assets', resolve('src/assets'))
          .set('components', resolve('src/components'))
    }
}
複製代碼

2 運用場景

目錄的結構可能層層疊疊,後在達到目錄

例如: 剛剛的env.js目錄

import {baseUrl} from '../config/env'
複製代碼

可是咱們不必定保證,目錄就那麼靠近,那麼怎麼處理

// @表明src目錄下,便可這樣調用

import {baseUrl} from '@/config/env'
複製代碼

7 優化-配置externals

防止將某些 import 的包(package)打包到 bundle 中,而是在運行時(runtime)再去從外部獲取這些擴展依賴


### 項目中的使用 > 通常性vue項目,咱們都會把一些框架包,給抽離出來。例如:
  • Vue
  • ELEMENT
  • VueRouter
  • Vuex
  • axios

1 引入框架

咱們把一些外包引用的包,提取出來,放在public中

2 編輯 externals

咱們在vue.config.

module.exports = {

   configureWebpack: config => {

       config.externals = {
         'vue': 'Vue',
         'element-ui': 'ELEMENT',
         'vue-router': 'VueRouter',
         'vuex': 'Vuex',
         'axios': 'axios'
       }

   }
複製代碼

3 引入cdn

那麼這時候咱們須要在  public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no">
    <meta http-equiv="X-UA-Compatible" content="chrome=1" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/element-ui/2.5.4/theme-chalk/index.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/animate/3.5.2/animate.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/iconfont/1.0.0/index.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/iconfont/1.0.0/iconfont.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/iconfont/1.0.0/index.css">
    <title>vue-demo</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but vue-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <script src="<%= BASE_URL %>cdn/vue/2.5.2/vue.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/vuex/2.4.1/vuex.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/vue-router/3.0.1/vue-router.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/axios/1.0.0/axios.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/element-ui/2.5.4/index.js" charset="utf-8"></script>
  </body>

</html>
複製代碼

8 優化-開啓Gzip 壓縮

1 介紹

vue cli 3.0相比2.0有很多的改動,最明顯的就是 build文件夾不見了,改成根目錄的vue.config.js
在此記錄一下Gzip配置的過程

npm i -D compression-webpack-plugin
複製代碼

2 修改vue.config.js

const CompressionPlugin = require("compression-webpack-plugin")

module.exports = {
	configureWebpack:config=>{
        if(process.env.NODE_ENV === 'production'){
            return{
                plugins: [
                    new CompressionPlugin({
                        test:/\.js$|\.html$|.\css/, //匹配文件名
                        threshold: 10240,//對超過10k的數據壓縮
                        deleteOriginalAssets: false //不刪除源文件
                    })
                ]
            }
        }
    },
}
複製代碼

3 nginx配置gzip

gzip  on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 6;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
複製代碼

第1行:開啓Gzip

第2行:不壓縮臨界值,大於1K的才壓縮,通常不用改

第3行:buffer,就是,嗯,算了不解釋了,不用改

第4行:用了反向代理的話,末端通訊是HTTP/1.0,有需求的應該也不用看我這科普文了;有這句的話註釋了就行> 了,默認是HTTP/1.1

第5行:壓縮級別,1-10,數字越大壓縮的越好,時間也越長,看心情隨便改吧

第6行:進行壓縮的文件類型,缺啥補啥就好了,JavaScript有兩種寫法,最好都寫上吧,總有人抱怨js文件沒有壓> 縮,其實多寫一種格式就好了

第7行:跟Squid等緩存服務有關,on的話會在Header裏增長"Vary: Accept-Encoding",我不須要這玩意,本身> 對照狀況看着辦吧

第8行:IE6對Gzip不怎麼友好,不給它Gzip了

9 優化-首屏加載

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no">
    <meta http-equiv="X-UA-Compatible" content="chrome=1" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/element-ui/2.5.4/theme-chalk/index.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/animate/3.5.2/animate.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/iconfont/1.0.0/index.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/iconfont/1.0.0/iconfont.css">
    <link rel="stylesheet" href="<%= BASE_URL %>cdn/iconfont/1.0.0/index.css">
    <title>vue-demo</title>
    
   <style> html, body, #app { height: 100%; margin: 0px; padding: 0px; } .chromeframe { margin: 0.2em 0; background: #ccc; color: #000; padding: 0.2em 0; } #loader-wrapper { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; } #loader { display: block; position: relative; left: 50%; top: 50%; width: 150px; height: 150px; margin: -75px 0 0 -75px; border-radius: 50%; border: 3px solid transparent; /* COLOR 1 */ border-top-color: #FFF; -webkit-animation: spin 2s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -ms-animation: spin 2s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -moz-animation: spin 2s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -o-animation: spin 2s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ animation: spin 2s linear infinite; /* Chrome, Firefox 16+, IE 10+, Opera */ z-index: 1001; } #loader:before { content: ""; position: absolute; top: 5px; left: 5px; right: 5px; bottom: 5px; border-radius: 50%; border: 3px solid transparent; /* COLOR 2 */ border-top-color: #FFF; -webkit-animation: spin 3s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -moz-animation: spin 3s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -o-animation: spin 3s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -ms-animation: spin 3s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ animation: spin 3s linear infinite; /* Chrome, Firefox 16+, IE 10+, Opera */ } #loader:after { content: ""; position: absolute; top: 15px; left: 15px; right: 15px; bottom: 15px; border-radius: 50%; border: 3px solid transparent; border-top-color: #FFF; /* COLOR 3 */ -moz-animation: spin 1.5s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -o-animation: spin 1.5s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -ms-animation: spin 1.5s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ -webkit-animation: spin 1.5s linear infinite; /* Chrome, Opera 15+, Safari 5+ */ animation: spin 1.5s linear infinite; /* Chrome, Firefox 16+, IE 10+, Opera */ } @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: rotate(0deg); /* IE 9 */ transform: rotate(0deg); /* Firefox 16+, IE 10+, Opera */ } 100% { -webkit-transform: rotate(360deg); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: rotate(360deg); /* IE 9 */ transform: rotate(360deg); /* Firefox 16+, IE 10+, Opera */ } } @keyframes spin { 0% { -webkit-transform: rotate(0deg); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: rotate(0deg); /* IE 9 */ transform: rotate(0deg); /* Firefox 16+, IE 10+, Opera */ } 100% { -webkit-transform: rotate(360deg); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: rotate(360deg); /* IE 9 */ transform: rotate(360deg); /* Firefox 16+, IE 10+, Opera */ } } #loader-wrapper .loader-section { position: fixed; top: 0; width: 51%; height: 100%; background: #7171C6; /* Old browsers */ z-index: 1000; -webkit-transform: translateX(0); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: translateX(0); /* IE 9 */ transform: translateX(0); /* Firefox 16+, IE 10+, Opera */ } #loader-wrapper .loader-section.section-left { left: 0; } #loader-wrapper .loader-section.section-right { right: 0; } /* Loaded */ .loaded #loader-wrapper .loader-section.section-left { -webkit-transform: translateX(-100%); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: translateX(-100%); /* IE 9 */ transform: translateX(-100%); /* Firefox 16+, IE 10+, Opera */ -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000); transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000); } .loaded #loader-wrapper .loader-section.section-right { -webkit-transform: translateX(100%); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: translateX(100%); /* IE 9 */ transform: translateX(100%); /* Firefox 16+, IE 10+, Opera */ -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000); transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000); } .loaded #loader { opacity: 0; -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } .loaded #loader-wrapper { visibility: hidden; -webkit-transform: translateY(-100%); /* Chrome, Opera 15+, Safari 3.1+ */ -ms-transform: translateY(-100%); /* IE 9 */ transform: translateY(-100%); /* Firefox 16+, IE 10+, Opera */ -webkit-transition: all 0.3s 1s ease-out; transition: all 0.3s 1s ease-out; } /* JavaScript Turned Off */ .no-js #loader-wrapper { display: none; } .no-js h1 { color: #222222; } #loader-wrapper .load_title { font-family: 'Open Sans'; color: #FFF; font-size: 19px; width: 100%; text-align: center; z-index: 9999999999999; position: absolute; top: 60%; opacity: 1; line-height: 30px; } #loader-wrapper .load_title span { font-weight: normal; font-style: italic; font-size: 13px; color: #FFF; opacity: 0.5; } </style>
    
  </head>
  <body>
    <noscript>
      <strong>We're sorry but vue-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    
    <div id="app">
      <div id="loader-wrapper">
        <div id="loader"></div>
        <div class="loader-section section-left"></div>
        <div class="loader-section section-right"></div>
        <div class="load_title">正在加載 vue,請耐心等待
          <br>
          <span>V1.3</span>
        </div>
      </div>
    </div>

    <!-- built files will be auto injected -->
    <script src="<%= BASE_URL %>cdn/vue/2.5.2/vue.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/vuex/2.4.1/vuex.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/vue-router/3.0.1/vue-router.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/axios/1.0.0/axios.min.js" charset="utf-8"></script>
    <script src="<%= BASE_URL %>cdn/element-ui/2.5.4/index.js" charset="utf-8"></script>
  </body>

</html>

複製代碼

image.png

10 預處理器 (Sass/Less/Stylus)

# Sass
npm install -D sass-loader node-sass

# Less
npm install -D less-loader less

# Stylus
npm install -D stylus-loader stylus
複製代碼
而後你就能夠導入相應的文件類型,或在 *.vue 文件中這樣來使用:

<style scoped lang="stylus">
.personal
  position relative
  .banner-red
    width 100%
    height 100px
</style>
複製代碼

11 node自動化部署

1 安裝scp2

npm install scp2 --save-dev
複製代碼

2 配置一個服務器列表

deploy/products.js

image.png

/* *定義多個服務器帳號 及 根據 SERVER_ID 導出當前環境服務器帳號 */
const SERVER_LIST = [
  {
    id: 0,
    name: 'A-測試環境',
    host: 'xxx.xxx.xxx.xxx', // ip
    port: 22,// 端口
    username: 'root', // 登陸服務器的帳號
    password: 'root', // 登陸服務器的帳號
    path: 'xxx/xxx/xxx', // 發佈至靜態服務器的項目路徑
    del: ['/var/www/jx/admin/js', '/var/www/jx/admin/css'] // 刪除這些沒法替換的
  },
  {
    id: 1,
    name: 'B-生成環境',
    host: 'xxx.xxx.xxx.xxx', // ip
    port: 22,// 端口
    username: 'root', // 登陸服務器的帳號
    password: 'root', // 登陸服務器的帳號
    path: 'xxx/xxx/xxx', // 發佈至靜態服務器的項目路徑
    del: ['/var/www/jx/admin/js', '/var/www/jx/admin/css'] // 刪除這些沒法替換的
  },
];

module.exports = SERVER_LIST;
複製代碼

3 建立scp2自動化部署腳本

const scpClient = require('scp2');
const ora = require('ora');
const chalk = require('chalk');
const servers = require('./products');
let server = servers[process.env.NODE_ENV === 'prod' ? 1 : 0];
const spinner = ora('正在發佈到' + (process.env.NODE_ENV === 'prod' ? '生產' : '測試') + '服務器...');

var Client = require('ssh2').Client;

var conn = new Client();
conn
  .on('ready', function() {
    // rm 刪除dist文件,\n 是換行 換行執行 重啓nginx命令 我這裏是用docker重啓nginx
    let dels = ""
    server.del.forEach(item => {
        dels += `rm -rf ${item}\n`;
    });
    conn.exec(dels, function ( err, stream ) {
      if (err) throw err;
      stream
        .on('close', function(code, signal) {
          // 在執行shell命令後,把開始上傳部署項目代碼放到這裏面
            spinner.start();
            scpClient.scp(
            'dist/',
            {
                host: server.host,
                port: server.port,
                username: server.username,
                password: server.password,
                path: server.path
            },
            function (err) {
                spinner.stop();
                if (err) {
                    console.log(chalk.red('發佈失敗.\n'));
                    throw err;
                } else {
                    console.log(chalk.green('Success! 成功發佈到' + (process.env.NODE_ENV === 'prod' ? '生產' : '測試') + '服務器! \n'));
                }
            }
            );
        conn.end();
    })
    .on('data', function (data) {
            console.log('STDOUT: ' + data);
        })
        .stderr.on('data', function (data) {
            console.log('STDERR: ' + data);
        });
    });
    })
    .connect({
        host: server.host,
        port: server.port,
        username: server.username,
        password: server.password
    });
複製代碼

4 添加指令

在 package.json 中添加指令

"upload-test": "NODE_ENV=test node ./deploy",
"upload-prod": "NODE_ENV=prod node ./deploy"
複製代碼

5 使用指令

發佈測試環境

npm run upload-test
複製代碼

12 node自動化新建頁面

每一次都須要須要寫vue的大體構造,我以爲仍是比較麻煩的一件事。通常性可使用vscode新建,可是我認爲node更加方便

1 配置屬於你的模板

image.png

generateTpl.js

安裝我的習慣能夠寫多個模板這裏,咱們只演示一種

exports.table = function(pageName) {

var tpl = `<template> <basic-container> <h3>${pageName}-page</h3> </basic-container> </template> <script> export default { data() { return { }; }, methods: { } }; </script> <style scoped> </style> `
return tpl;
}
複製代碼

2 安裝 inquirer

npm install inquirer --save-dev

3 建立自動化新建腳本

var path = require('path');
var fs = require('fs');
var generateTpl = require('./generateTpl');
var inquirer = require('inquirer')

const createPage = {
    template: "table",
    init: function() {
        const promptName = [{
            type: 'input',
            message: '模板的名稱',
            name: 'templateName',
            filter: function (val) {
                return val.toLowerCase()
            }
        }]
        const promptList = [{
            type: 'list',
            message: '請選擇一種模版',
            name: 'template',
            choices: ['表格', '表單'],
            filter: function (val) {
                return val.toLowerCase()
            }
        }]

        inquirer.prompt(promptName).then(name => {
            inquirer.prompt(promptList).then(anwsers => {
                this.initParams(name.templateName);
                if (anwsers.template==='表格') {
                    this.template = "table"
                }else{
                    this.template = "form"
                }
                this.getAllPage();
                this.generatePage();
            })
        })
    },
    initParams: function (templateName) {
        
        this.pageName = templateName;

        this.pageDir = path.join(__dirname, '../src/views');

        this.allPages = ""

    },
    getAllPage: function() {

        this.allPages = fs.readdirSync(this.pageDir);

    },
    generatePage: function(){

        if(this.allPages.indexOf(this.pageName) == -1) {
            
            this.toGenerageDir();
        } else {

            console.error('當前頁面已經存在了');
        }
    },
    toGenerageDir: function(){
        
        try{
            
            fs.mkdir(path.join(this.pageDir, this.pageName), function(err){

                if(err){

                    console.error(err);
                    return;
                }

                this.toGenerateFiles();

                console.log('頁面建立完成');

            }.bind(this));
        }catch(e){

            console.error(e);
        }
    },
    toGenerateFiles: function() {
        
        // 選擇模板
        var vueFile = path.join(this.pageDir, this.pageName, `index.vue`);
        var imageDir = path.join(this.pageDir, this.pageName, 'images')
		
        // 建立文件
        fs.writeFileSync(vueFile, generateTpl[this.template](this.pageName));
        fs.mkdirSync(imageDir)
    }
}

createPage.init();
複製代碼

3 添加指令

在 package.json 中添加指令

"create-page": "node create-page/create-page.js",
複製代碼

4 使用指令

npm run create-page
複製代碼

image.png
相關文章
相關標籤/搜索