diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1558ce4..0ee30412 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,11 +13,11 @@ jobs: node-version: 16 - name: Install and Build run: | - yarn - yarn build + pnpm install + pnpm run build - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: branch: gh-pages - folder: public + folder: dist single-commit: true diff --git a/.gitignore b/.gitignore index 40b878db..b1768e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -node_modules/ \ No newline at end of file +node_modules/ +docs/.vitepress/dist +docs/.vitepress/cache +docs/.vitepress/.temp \ No newline at end of file diff --git a/README.md b/README.md index a57e21e8..cae53c7b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # 前端網頁課程教材 [![Actions Status](https://github.com/rogeraabbccdd/F2E-book/workflows/Deploy/badge.svg)](https://github.com/rogeraabbccdd/F2E-book/actions)

- +

## 教材內容 本教材為本人於勞動部泰山職業訓練場前端網頁開發技術班的授課教材 -本教材包含以下基礎課程,部分章節的內容不完整,詳細請報名課程 +包含以下基礎課程,部分章節的內容不完整,詳細請報名課程 - Git - JavaScript - jQuery @@ -14,9 +14,4 @@ - Vue.js ## 課程報名 -- [111 年前端網頁開發技術 1 期](https://ttms.etraining.gov.tw/eYVTR/YR008/Detail?BCM_SNO=133844) -- [111 年前端網頁開發技術 2 期](https://ttms.etraining.gov.tw/eYVTR/YR008/Detail?BCM_SNO=133845) - -## 連結 -- [線上閱讀](https://rogeraabbccdd.github.io/F2E-book/) -- [版型](https://github.com/rogeraabbccdd/vuepress-theme-reco) +- [課程介紹](https://wdaweb.github.io/) diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 00000000..1f695893 Binary files /dev/null and b/bun.lockb differ diff --git a/docs/.vitepress/components/DemoBlock.vue b/docs/.vitepress/components/DemoBlock.vue new file mode 100644 index 00000000..157f7a60 --- /dev/null +++ b/docs/.vitepress/components/DemoBlock.vue @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/docs/.vitepress/components/FlowChart.vue b/docs/.vitepress/components/FlowChart.vue new file mode 100644 index 00000000..c93a920b --- /dev/null +++ b/docs/.vitepress/components/FlowChart.vue @@ -0,0 +1,112 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/ImageFigure.vue b/docs/.vitepress/components/ImageFigure.vue new file mode 100644 index 00000000..6d76d5c8 --- /dev/null +++ b/docs/.vitepress/components/ImageFigure.vue @@ -0,0 +1,45 @@ + + + diff --git a/docs/.vitepress/components/Mindmap.vue b/docs/.vitepress/components/Mindmap.vue new file mode 100644 index 00000000..70c1f933 --- /dev/null +++ b/docs/.vitepress/components/Mindmap.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/PDF.vue b/docs/.vitepress/components/PDF.vue new file mode 100644 index 00000000..812e6734 --- /dev/null +++ b/docs/.vitepress/components/PDF.vue @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/components/Tree.vue b/docs/.vitepress/components/Tree.vue new file mode 100644 index 00000000..067cebdb --- /dev/null +++ b/docs/.vitepress/components/Tree.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/docs/.vitepress/components/theme/VPContent.vue b/docs/.vitepress/components/theme/VPContent.vue new file mode 100644 index 00000000..a6cd383e --- /dev/null +++ b/docs/.vitepress/components/theme/VPContent.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs new file mode 100644 index 00000000..a48601ef --- /dev/null +++ b/docs/.vitepress/config.mjs @@ -0,0 +1,205 @@ +import { defineConfig } from 'vitepress' +import { flowchartPlugin } from './plugins/flowchart/index.mjs' +import MarkdownItContainer from 'markdown-it-container' +import { demoBlockPlugin } from './plugins/demo-block/plugin.mjs' +import { fileURLToPath, URL } from 'node:url' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + base: '/F2E-book', + outDir: 'dist', + appearance: 'dark', + title: "前端班課程講義", + description: "進入 JavaScript 的世界", + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: '課程報名', link: 'https://wdaweb.github.io' }, + ], + sidebar: [ + { + text: '前言', + items: [ + { text: '在開始之前', link: '/intro/before-start' }, + ] + }, + { + text: 'JavaScript 基礎', + items: [ + { text: '認識 JavaScript', link: '/basic/intro' }, + { text: '變數', link: '/basic/variable' }, + { text: '運算子', link: '/basic/operator' }, + { text: '邏輯判斷式', link: '/basic/condition' }, + { text: '迴圈', link: '/basic/loop' }, + { text: '陣列與物件', link: '/basic/array-object' }, + { text: 'function', link: '/basic/function' }, + { text: '物件導向', link: '/basic/class' }, + { text: '資料處理 - 文字', link: '/basic/data-string' }, + { text: '資料處理 - 陣列', link: '/basic/data-array' }, + { text: '資料處理 - 數字', link: '/basic/data-number' }, + { text: '計時器', link: '/basic/timer' }, + ] + }, + { + text: 'JavaScript 網頁操作', + items: [ + { text: 'BOM', link: '/interaction/bom' }, + { text: 'DOM', link: '/interaction/dom' }, + { text: '事件', link: '/interaction/events' }, + { text: 'Observer', link: '/interaction/observer' }, + { text: '時鐘', link: '/interaction/clock' }, + ] + }, + { + text: 'jQuery', + items: [ + { text: 'jQuery - DOM', link: '/jquery/dom' }, + { text: 'jQuery - 動畫', link: '/jquery/animation' }, + { text: '打殭屍小遊戲', link: '/jquery/zombie' }, + { text: '翻牌記憶小遊戲', link: '/jquery/cards' }, + ] + }, + { + text: 'JavaScript 進階', + items: [ + { text: 'HTTP 請求', link: '/advanced/ajax' }, + { text: '進階語法', link: '/advanced/advanced' }, + ] + }, + { + text: 'Node.js', + items: [ + { text: '認識 Node.js', link: '/node/intro' }, + { text: 'LINE 機器人', link: '/node/line' }, + ] + }, + { + text: 'Vue.js', + items: [ + { text: '基礎語法', link: '/vue/basic' }, + { text: 'Vite 與單元件檔案', link: '/vue/vite-sfc' }, + { text: '套件', link: '/vue/packages' }, + { text: '路由、狀態與 PWA', link: '/vue/router-pinia-pwa' }, + ] + }, + { + text: '資料庫 API', + items: [ + { text: 'MongoDB 的安裝與操作', link: '/database/mongo' }, + { text: '基礎 API', link: '/database/api-basic' }, + { text: '進階 API', link: '/database/api-advanced' }, + ] + }, + { + text: '其他', + items: [ + { text: 'VuePress 網站', link: '/others/vuepress' }, + { text: 'Git', link: '/others/git' }, + { text: 'Pug', link: '/others/pug' }, + { text: 'Nuxt.js', link: '/others/nuxt' }, + ] + }, + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/rogeraabbccdd/F2E-book' } + ], + search: { + provider: 'local', + options: { + detailedView: true, + translations: { + button: { + buttonText: '搜尋內容', + buttonAriaLabel: '搜尋內容' + }, + modal: { + displayDetails: "顯示詳細結果", + resetButtonTitle: '清除查詢條件', + backButtonTitle: '關閉搜尋', + noResultsText: '無法找到相關結果', + searchBox: { + resetButtonTitle: '清除查詢條件', + resetButtonAriaLabel: '清除查詢條件', + cancelButtonText: '取消', + cancelButtonAriaLabel: '取消' + }, + startScreen: { + recentSearchesTitle: '搜尋記錄', + noRecentSearchesText: '沒有搜尋記錄', + saveRecentSearchButtonTitle: '儲存至搜尋記錄', + removeRecentSearchButtonTitle: '從搜尋記錄中移除', + favoriteSearchesTitle: '收藏', + removeFavoriteSearchButtonTitle: '從收藏中移除' + }, + errorScreen: { + titleText: '無法擷取結果', + helpText: '你可能需要檢查你的網路連線' + }, + footer: { + selectText: '選擇', + navigateText: '切換', + closeText: '關閉', + searchByText: '搜尋提供者' + }, + noResultsScreen: { + noResultsText: '無法找到相關結果', + suggestedQueryText: '你可以嘗試查詢', + reportMissingResultsText: '你認為這個查詢應該有結果?', + reportMissingResultsLinkText: '回報問題' + } + } + } + } + }, + docFooter: { + prev: '上一頁', + next: '下一頁' + }, + outline: { + label: '頁面導覽' + }, + lastUpdated: { + text: '最後更新於', + formatOptions: { + dateStyle: 'short', + timeStyle: 'medium' + } + }, + langMenuLabel: '多語言', + returnToTopLabel: '回到頂部', + sidebarMenuLabel: '選單', + darkModeSwitchLabel: '主題', + lightModeSwitchTitle: '切換到淺色模式', + darkModeSwitchTitle: '切換到深色模式' + }, + lastUpdated: true, + markdown: { + lineNumbers: true, + config: (md) => { + md.use(flowchartPlugin) + md.use(MarkdownItContainer, 'demo', demoBlockPlugin) + } + }, + vite: { + resolve: { + alias: [ + { + find: /^.*\/VPContent\.vue$/, + replacement: fileURLToPath( + new URL('./components/theme/VPContent.vue', import.meta.url) + ) + } + ] + } + }, + head: [ + [ + "link", + { + rel: "icon", + href: "favicon.ico" + } + ] + ] +}) diff --git a/docs/.vitepress/plugins/demo-block/common/constants.mjs b/docs/.vitepress/plugins/demo-block/common/constants.mjs new file mode 100644 index 00000000..ef94f2d4 --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/common/constants.mjs @@ -0,0 +1,32 @@ +export const END_TYPE = 'container_demo_close' + +export const CLASS_WRAPPER = 'vitepress-plugin-demo-block__wrapper' +export const CLASS_DISPLAY = 'vitepress-plugin-demo-block__display' +export const CLASS_CODE = 'vitepress-plugin-demo-block__code' +export const CLASS_FOOTER = 'vitepress-plugin-demo-block__footer' +export const CLASS_HORIZONTAL = 'vitepress-plugin-demo-block__horizontal' +export const CLASS_H_CODE = 'vitepress-plugin-demo-block__h_code' + +export const CLASS_APP = 'vitepress-plugin-demo-block__app' +export const CLASS_SHOW_LINK = 'vitepress-plugin-demo-block__show-link' +export const CLASS_EXPAND = 'vitepress-plugin-demo-block__expand' +export const CLASS_OUTLINK = 'vitepress-plugin-demo-block__out-link' +export const CLASS_CODEPEN = 'vitepress-plugin-demo-block__codepen' +export const CLASS_JSFIDDLE = 'vitepress-plugin-demo-block__jsfiddle' +export const CLASS_BUTTON = 'vitepress-plugin-demo-block__button' + +export const DEFAULT_SETTINGS = { + jsLib: [], + cssLib: [], + jsfiddle: true, + codepen: true, + codepenLayout: 'left', + codepenJsProcessor: 'babel', + codepenEditors: '101', + horizontal: false, + vue: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js', + react: 'https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js', + reactDOM: 'https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js', +} + +export const SETTINGS_KEY = '$VUEPRESS_DEMO_BLOCK' diff --git a/docs/.vitepress/plugins/demo-block/common/utils.mjs b/docs/.vitepress/plugins/demo-block/common/utils.mjs new file mode 100644 index 00000000..c277c128 --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/common/utils.mjs @@ -0,0 +1,140 @@ +import { DEFAULT_SETTINGS, SETTINGS_KEY } from './constants.mjs' + +const _once = {} + +const getHtmlTpl = html => `
+${html} +
` + +const getVueJsTpl = js => { + const jsContent = js + .replace(/export\s+default\s*?\{\n*/, '') + .replace(/\n*\}\s*$/, '') + .trim() + return ` + Vue.createApp({ + ${jsContent} + }).mount('#app') + ` +} + +const toArray = value => Array.prototype.slice.call(value) + +export const getSettings = key => + window[SETTINGS_KEY] && window[SETTINGS_KEY][key] !== undefined + ? window[SETTINGS_KEY][key] + : DEFAULT_SETTINGS[key] + +export const h = (tag, attrs, children) => { + const node = document.createElement(tag) + attrs && + Object.keys(attrs).forEach(key => { + if (!key.indexOf('data')) { + const k = key.replace('data', '') + node.dataset[k] = attrs[key] + } else { + node[key] = attrs[key] + } + }) + children && + children.forEach(({ tag, attrs, children }) => { + node.appendChild(h(tag, attrs, children)) + }) + return node +} + +export const $ = (parent, node, returnArray) => { + const result = toArray(parent.querySelectorAll(`.${node}`)) + return result.length === 1 && !returnArray ? result[0] : result +} + +const getVueScript = (js, html) => { + const scripts = js.split(/export\s+default/) + const scriptStrOrg = `(function() {${scripts[0]} ; return ${scripts[1]}})()` + const scriptStr = window.Babel + ? window.Babel.transform(scriptStrOrg, { presets: ['es2015'] }).code + : scriptStrOrg + const scriptObj = [eval][0](scriptStr) + scriptObj.template = html + return scriptObj +} + +const getVanillaScript = js => { + return window.Babel + ? window.Babel.transform(js, { presets: ['es2015'] }).code + : js +} + +export const getVueDetail = (code, config) => { + const cssBlock = code.match(/ +` \ No newline at end of file diff --git a/docs/.vitepress/plugins/demo-block/icons/codepen.mjs b/docs/.vitepress/plugins/demo-block/icons/codepen.mjs new file mode 100644 index 00000000..42e49f12 --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/icons/codepen.mjs @@ -0,0 +1,3 @@ +export default ` + +` \ No newline at end of file diff --git a/docs/.vitepress/plugins/demo-block/icons/jsfiddle.mjs b/docs/.vitepress/plugins/demo-block/icons/jsfiddle.mjs new file mode 100644 index 00000000..a11ba1dc --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/icons/jsfiddle.mjs @@ -0,0 +1,3 @@ +export default ` + +` \ No newline at end of file diff --git a/docs/.vitepress/plugins/demo-block/main.mjs b/docs/.vitepress/plugins/demo-block/main.mjs new file mode 100644 index 00000000..c6a01ba6 --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/main.mjs @@ -0,0 +1,108 @@ +import { + CLASS_APP, + CLASS_CODE, + CLASS_DISPLAY, + CLASS_EXPAND, + CLASS_FOOTER, + CLASS_HORIZONTAL, + CLASS_H_CODE, + CLASS_SHOW_LINK, + CLASS_WRAPPER, +} from './common/constants.mjs'; +import { + $, + getReactDetail, + getSettings, + getVanillaDetail, + getVueDetail, + h, + injectCss, +} from './common/utils.mjs'; +import codepen from './online/codepen.mjs'; +import jsfiddle from './online/jsfiddle.mjs'; + +export default function webController() { + const nodes = $(document, CLASS_WRAPPER, true); + if (!nodes.length) { + setTimeout(_ => { + webController() + }, 300) + return + } + nodes.forEach(node => { + if (node.dataset.created === 'true') return; + node.style.display = 'block'; + + const codeNode = $(node, CLASS_CODE); + const displayNode = $(node, CLASS_DISPLAY); + const footerNode = $(node, CLASS_FOOTER); + const appNode = $(displayNode, CLASS_APP); + + const code = decodeURIComponent(node.dataset.code); + let config = decodeURIComponent(node.dataset.config); + let type = decodeURIComponent(node.dataset.type); + config = config ? JSON.parse(config) : {}; + const height = codeNode.querySelector('div').clientHeight; + const detail = + type === 'react' + ? getReactDetail(code, config) + : type === 'vanilla' + ? getVanillaDetail(code, config) + : getVueDetail(code, config); + const expandNode = createExpandNode(); + footerNode.appendChild(expandNode); + expandNode.addEventListener( + 'click', + expandHandler.bind(null, expandNode, height, codeNode, footerNode) + ); + + if (getSettings('jsfiddle')) { + footerNode.appendChild(jsfiddle(detail)); + } + if (getSettings('codepen')) { + footerNode.appendChild(codepen(detail)); + } + + const horizontalConfig = + config.horizontal !== undefined + ? config.horizontal + : getSettings('horizontal'); + + if (horizontalConfig) { + node.classList.add(CLASS_HORIZONTAL); + const hCodeNode = codeNode.firstChild.cloneNode(true); + hCodeNode.classList.add(CLASS_H_CODE); + displayNode.appendChild(hCodeNode); + } + + detail.css && injectCss(detail.css); + if (type === 'react') { + ReactDOM.render(React.createElement(detail.js), appNode); + } else if (type === 'vue') { + const Comp = Vue.extend(detail.script); + const app = new Comp().$mount(); + appNode.appendChild(app.$el); + } else if (type === 'vanilla') { + appNode.innerHTML = detail.html; + new Function(`return (function(){${detail.script}})()`)(); + } + node.dataset.created = 'true'; + }); +} + +function createExpandNode() { + return h('button', { + className: `${CLASS_EXPAND}`, + }); +} + +function expandHandler(expandNode, height, codeNode, footerNode) { + const isExpand = expandNode.dataset.isExpand !== '1'; + codeNode.style.height = isExpand ? `${height}px` : 0; + if (isExpand) { + footerNode.classList.add(CLASS_SHOW_LINK); + } else { + footerNode.classList.remove(CLASS_SHOW_LINK); + } + expandNode.dataset.isExpand = isExpand ? '1' : '0'; +} diff --git a/docs/.vitepress/plugins/demo-block/online/codepen.mjs b/docs/.vitepress/plugins/demo-block/online/codepen.mjs new file mode 100644 index 00000000..717837e6 --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/online/codepen.mjs @@ -0,0 +1,40 @@ +import { CLASS_BUTTON, CLASS_CODEPEN } from '../common/constants.mjs' +import { getSettings, h } from '../common/utils.mjs' +import codepenIcon from '../icons/codepen.mjs' +export default function getCodepenBtn({ css, htmlTpl, jsTpl, jsLib, cssLib }) { + const value = JSON.stringify({ + css: css, + html: htmlTpl, + js: jsTpl, + js_external: jsLib.concat(getSettings('jsLib')).join(';'), + css_external: cssLib.concat(getSettings('cssLib')).join(';'), + layout: getSettings('codepenLayout'), + js_pre_processor: getSettings('codepenJsProcessor'), + editors: getSettings('codepenEditors') + }) + const form = h( + 'form', + { + className: CLASS_CODEPEN, + target: '_blank', + action: 'https://codepen.io/pen/define', + method: 'post' + }, + [ + { + tag: 'input', + attrs: { type: 'hidden', name: 'data', value } + }, + { + tag: 'button', + attrs: { + type: 'submit', + innerHTML: codepenIcon, + className: CLASS_BUTTON, + datatip: 'Codepen' + } + } + ] + ) + return form +} diff --git a/docs/.vitepress/plugins/demo-block/online/jsfiddle.mjs b/docs/.vitepress/plugins/demo-block/online/jsfiddle.mjs new file mode 100644 index 00000000..f58f0863 --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/online/jsfiddle.mjs @@ -0,0 +1,55 @@ +import { CLASS_BUTTON, CLASS_JSFIDDLE } from '../common/constants.mjs' +import { getSettings, h } from '../common/utils.mjs' +import jsfiddleIcon from '../icons/jsfiddle.mjs' +export default function getJsfiddleBtn({ css, htmlTpl, jsTpl, jsLib, cssLib }) { + const resource = jsLib + .concat(cssLib) + .concat(getSettings('cssLib')) + .concat(getSettings('jsLib')) + .join(',') + const form = h( + 'form', + { + className: CLASS_JSFIDDLE, + target: '_blank', + action: 'https://jsfiddle.net/api/post/library/pure/', + method: 'post' + }, + [ + { + tag: 'input', + attrs: { type: 'hidden', name: 'css', value: css } + }, + { + tag: 'input', + attrs: { type: 'hidden', name: 'html', value: htmlTpl } + }, + { + tag: 'input', + attrs: { type: 'hidden', name: 'js', value: jsTpl } + }, + { + tag: 'input', + attrs: { type: 'hidden', name: 'panel_js', value: 3 } + }, + { + tag: 'input', + attrs: { type: 'hidden', name: 'wrap', value: 1 } + }, + { + tag: 'input', + attrs: { type: 'hidden', name: 'resources', value: resource } + }, + { + tag: 'button', + attrs: { + type: 'submit', + className: CLASS_BUTTON, + innerHTML: jsfiddleIcon, + datatip: 'JSFiddle' + } + } + ] + ) + return form +} diff --git a/docs/.vitepress/plugins/demo-block/plugin.mjs b/docs/.vitepress/plugins/demo-block/plugin.mjs new file mode 100644 index 00000000..7f67ee08 --- /dev/null +++ b/docs/.vitepress/plugins/demo-block/plugin.mjs @@ -0,0 +1,52 @@ +import { + CLASS_APP, + CLASS_CODE, + CLASS_DISPLAY, + CLASS_FOOTER, + CLASS_WRAPPER, + END_TYPE +} from './common/constants.mjs' + +export const demoBlockPlugin = { + render: (tokens, idx) => { + const { nesting, info } = tokens[idx] + if (nesting === -1) { + return ` + +
+ + ` + } + let codeStr = '' + let configStr = '' + let typeStr = ~info.indexOf('react') + ? 'react' + : ~info.indexOf('vanilla') + ? 'vanilla' + : 'vue' + for (let i = idx; i < tokens.length; i++) { + const { type, content, info } = tokens[i] + if (type === END_TYPE) break + if (!content) continue + if (type === 'fence') { + if (info === 'json') { + configStr = encodeURIComponent(content) + } else { + codeStr = encodeURIComponent(content) + } + } + } + return ` +