├── .yarnrc ├── components ├── section.module.scss ├── rating.module.scss ├── input.module.scss ├── blogimage.module.scss ├── rating.tsx ├── warning.tsx ├── blogimage.tsx ├── badge.tsx ├── tags.module.scss ├── pagetransition.tsx ├── pageheader.tsx ├── postswitch.tsx ├── badge.module.scss ├── tags.tsx ├── input.tsx ├── blog │ ├── parallaxcover.tsx │ ├── parallaxcover.module.scss │ ├── rating.module.scss │ └── rating.tsx ├── pageheader.module.scss ├── seo.tsx ├── postlist.module.scss ├── hitcounter.tsx ├── warning.module.scss ├── section.tsx ├── likebutton.module.scss ├── page.module.scss ├── button.tsx ├── likebutton.tsx ├── messages.tsx ├── segmentedcontrol.tsx ├── segmentedcontrol.module.scss ├── themechanger.module.scss ├── button.module.scss ├── parallax.tsx ├── image.module.scss ├── project.tsx ├── postlist.tsx ├── subscribe.module.scss ├── header.module.scss ├── project.module.scss ├── header.tsx ├── animatedmessages.tsx ├── page.tsx ├── messages.module.scss ├── subscribe.tsx ├── postswitch.module.scss └── themechanger.tsx ├── public ├── avatar.png ├── nicky.jpeg ├── robots.txt ├── favicon.ico ├── feinsight.jpg ├── blog │ ├── awesome │ │ └── og.png │ └── default │ │ └── image.png ├── percentagechange.png ├── projects │ ├── roothub.png │ ├── leekfund.png │ ├── frontend-box.png │ └── vscode-codegen.png ├── qrcode_for_giscafer.jpg └── fonts │ ├── GTWalsheimPro-Bold.woff2 │ ├── GTWalsheimPro-Medium.woff2 │ └── GTWalsheimPro-Regular.woff2 ├── scripts ├── sync-post.js ├── generate-rss.js ├── github │ ├── job.js │ └── syncPost.js └── generate-sitemap.js ├── .env.example ├── lib ├── fetcher.js ├── formatdate.ts ├── linear.ts ├── webmentions.ts ├── gtag.ts └── localstorage.ts ├── next-env.d.ts ├── prettier.config.js ├── data └── blog │ ├── post-6.mdx │ ├── post-25.mdx │ ├── post-15.mdx │ ├── post-17.mdx │ ├── post-7.mdx │ ├── post-9.mdx │ ├── post-12.mdx │ ├── post-38.mdx │ ├── post-32.mdx │ ├── post-56.mdx │ ├── post-48.mdx │ ├── post-61.mdx │ ├── post-13.mdx │ ├── post-5.mdx │ ├── post-20.mdx │ ├── post-65.mdx │ ├── post-49.mdx │ ├── post-21.mdx │ ├── post-69.mdx │ ├── post-40.mdx │ ├── post-30.mdx │ ├── post-18.mdx │ ├── post-80.mdx │ ├── post-67.mdx │ ├── post-55.mdx │ ├── post-36.mdx │ ├── post-19.mdx │ ├── post-35.mdx │ ├── post-59.mdx │ ├── post-33.mdx │ ├── post-51.mdx │ ├── post-58.mdx │ ├── post-72.mdx │ ├── post-24.mdx │ ├── post-50.mdx │ └── post-39.mdx ├── pages ├── about.module.scss ├── 404.tsx ├── api │ ├── subscribe.ts │ ├── register-hit.ts │ ├── stats.ts │ └── likes.ts ├── _app.tsx ├── _document.tsx ├── blog │ ├── index.module.scss │ ├── tag │ │ └── [slug].tsx │ ├── post.module.scss │ └── index.tsx ├── index.tsx └── about.tsx ├── .prettierignore ├── tsconfig.json ├── .gitignore ├── hooks └── useinterval.js ├── next.config.js ├── LICENSE ├── .github └── workflows │ └── sync-post.yml ├── README.md ├── .eslintrc.js ├── contentlayer.config.ts └── package.json /.yarnrc: -------------------------------------------------------------------------------- 1 | --registry "https://registry.yarnpkg.com" 2 | -------------------------------------------------------------------------------- /components/section.module.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | position: relative; 3 | } 4 | -------------------------------------------------------------------------------- /public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/avatar.png -------------------------------------------------------------------------------- /public/nicky.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/nicky.jpeg -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Sitemap: https://giscafer.com/sitemap.xml 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/feinsight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/feinsight.jpg -------------------------------------------------------------------------------- /public/blog/awesome/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/blog/awesome/og.png -------------------------------------------------------------------------------- /public/percentagechange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/percentagechange.png -------------------------------------------------------------------------------- /public/projects/roothub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/projects/roothub.png -------------------------------------------------------------------------------- /public/blog/default/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/blog/default/image.png -------------------------------------------------------------------------------- /public/projects/leekfund.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/projects/leekfund.png -------------------------------------------------------------------------------- /public/projects/frontend-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/projects/frontend-box.png -------------------------------------------------------------------------------- /public/qrcode_for_giscafer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/qrcode_for_giscafer.jpg -------------------------------------------------------------------------------- /public/projects/vscode-codegen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/projects/vscode-codegen.png -------------------------------------------------------------------------------- /scripts/sync-post.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const syncPost = require('./github/syncPost') 3 | 4 | syncPost() 5 | -------------------------------------------------------------------------------- /public/fonts/GTWalsheimPro-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/fonts/GTWalsheimPro-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/GTWalsheimPro-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/fonts/GTWalsheimPro-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/GTWalsheimPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giscafer/blog/HEAD/public/fonts/GTWalsheimPro-Regular.woff2 -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GH_USER= 2 | GH_PROJECT_NAME= 3 | FAUNA_SECRET_KEY= 4 | REVUE_API_KEY= 5 | NEXT_PUBLIC_GA_TRACKING_ID= 6 | LINEAR_API_KEY= 7 | -------------------------------------------------------------------------------- /lib/fetcher.js: -------------------------------------------------------------------------------- 1 | export default async function Fetcher(...args) { 2 | const res = await fetch(...args) 3 | 4 | return res.json() 5 | } 6 | -------------------------------------------------------------------------------- /lib/formatdate.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (date: string) => 2 | new Date(date).toLocaleString('zh-CN', { 3 | month: 'short', 4 | day: '2-digit', 5 | year: 'numeric', 6 | }) 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /components/rating.module.scss: -------------------------------------------------------------------------------- 1 | .star { 2 | width: 16px; 3 | height: auto; 4 | margin-right: 2px; 5 | opacity: 0.15; 6 | fill: var(--text); 7 | stroke: none; 8 | } 9 | 10 | .filledStar { 11 | composes: star; 12 | opacity: 1; 13 | fill: var(--star); 14 | } 15 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 140, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | arrowParens: 'avoid', 11 | } 12 | -------------------------------------------------------------------------------- /components/input.module.scss: -------------------------------------------------------------------------------- 1 | .input { 2 | padding: 8px 12px; 3 | color: var(--text); 4 | font-size: 17px; 5 | font-family: var(--fontFamily); 6 | background-color: var(--bg); 7 | background-color: var(--inputBg); 8 | border: 1px solid var(--border); 9 | border-radius: 6px; 10 | -webkit-appearance: none; 11 | } 12 | -------------------------------------------------------------------------------- /lib/linear.ts: -------------------------------------------------------------------------------- 1 | import { LinearClient } from '@linear/sdk' 2 | 3 | const linearClient = new LinearClient({ 4 | apiKey: process.env.LINEAR_API_KEY, 5 | }) 6 | 7 | export const getAllIssues = async () => { 8 | const issues = await linearClient.issues() 9 | 10 | if (issues.nodes.length) { 11 | return issues.nodes 12 | } 13 | return [] 14 | } 15 | -------------------------------------------------------------------------------- /lib/webmentions.ts: -------------------------------------------------------------------------------- 1 | export const getMentionsForSlug = async (slug: string) => { 2 | const webmentions = await fetch(`https://webmention.io/api/mentions?target=https://giscafer.com/blog/${slug}&per-page=10000`) 3 | const mentions = await webmentions.json() 4 | const numberOfmentions = mentions?.links?.length 5 | 6 | return numberOfmentions > 0 ? numberOfmentions : 0 7 | } 8 | -------------------------------------------------------------------------------- /data/blog/post-6.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular1.x + ES6 开发 3 | publishedAt: 2016-08-15T01:05:04Z 4 | summary: 查看全文>> 5 | tags: ["AngularJS"] 6 | --- 7 | 8 | Angular 1.x和ES6的结合 9 | https://github.com/xufei/blog/issues/29 10 | 11 | Angular1.x + ES6 开发风格指南 12 | https://github.com/kuitos/kuitos.github.io/issues/34 13 | 14 | 15 | --- 16 | 本人自动发布于:[https://github.com/giscafer/blog/issues/6](https://github.com/giscafer/blog/issues/6) 17 | -------------------------------------------------------------------------------- /components/blogimage.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: block; 3 | margin-right: -15px; 4 | margin-bottom: 15px; 5 | margin-left: -15px; 6 | overflow: hidden; 7 | border: 1px solid var(--border); 8 | border-right-width: 0; 9 | border-left-width: 0; 10 | 11 | @media (min-width: 480px) { 12 | margin-right: -20px; 13 | margin-bottom: 30px; 14 | margin-left: -20px; 15 | border-width: 1px; 16 | border-radius: 12px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /components/rating.tsx: -------------------------------------------------------------------------------- 1 | import { Star } from 'react-feather' 2 | import styles from './rating.module.scss' 3 | 4 | const MAX_RATING = 5 5 | 6 | const Rating = ({ rating }: { rating: number }): JSX.Element => ( 7 |
8 | {Array.from(Array(MAX_RATING).keys()).map((_, i) => ( 9 | 10 | ))} 11 |
12 | ) 13 | 14 | export default Rating 15 | -------------------------------------------------------------------------------- /components/warning.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { AlertTriangle, Info } from 'react-feather' 3 | 4 | import styles from './warning.module.scss' 5 | 6 | const Warning = ({ children, type }: { children: ReactNode; type: 'warning' | 'info' }): JSX.Element => ( 7 |
8 | {type === 'info' ? : } 9 |

{children}

10 |
11 | ) 12 | 13 | export default Warning 14 | -------------------------------------------------------------------------------- /pages/about.module.scss: -------------------------------------------------------------------------------- 1 | .image { 2 | width: 600px !important; 3 | height: 800px !important; 4 | border-radius: 13px; 5 | } 6 | 7 | .text { 8 | margin-bottom: 30px; 9 | padding-top: 30px; 10 | p { 11 | font-weight: 400; 12 | font-size: 17px; 13 | line-height: 27px; 14 | } 15 | a { 16 | color: var(--brand); 17 | transition: color 200ms ease-out; 18 | 19 | &:hover, 20 | &:focus { 21 | color: var(--brandActive); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /components/blogimage.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import cn from 'classnames' 3 | import styles from './blogimage.module.scss' 4 | 5 | type BlogImageProps = { 6 | src: string 7 | alt: string 8 | className?: string 9 | } 10 | 11 | const BlogImage = ({ src, alt, className }: BlogImageProps): JSX.Element => ( 12 | 13 | {alt} 14 | 15 | ) 16 | 17 | export default BlogImage 18 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import Page from 'components/page' 3 | import PageHeader from 'components/pageheader' 4 | import Button from 'components/button' 5 | 6 | const Custom404 = (): JSX.Element => ( 7 | 8 | 9 | 404 | Giscafer.com 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | 17 | export default Custom404 18 | -------------------------------------------------------------------------------- /components/badge.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import { FC, ReactNode } from 'react' 3 | import styles from './badge.module.scss' 4 | 5 | type Props = { 6 | className?: string 7 | onClick?: () => void 8 | children: ReactNode 9 | } 10 | 11 | const Badge: FC = ({ className, children, onClick, ...otherProps }: Props) => { 12 | return ( 13 | 16 | ) 17 | } 18 | 19 | export default Badge 20 | -------------------------------------------------------------------------------- /components/tags.module.scss: -------------------------------------------------------------------------------- 1 | .tags { 2 | display: flex; 3 | justify-content: center; 4 | margin: 0 auto; 5 | margin-bottom: 60px; 6 | padding-left: 0; 7 | list-style: none; 8 | 9 | li { 10 | margin: 0 15px; 11 | 12 | &:first-of-type { 13 | margin-left: 0; 14 | } 15 | 16 | &:last-of-type { 17 | margin-right: 0; 18 | } 19 | 20 | a { 21 | opacity: 0.7; 22 | transition: opacity 250ms ease-out; 23 | 24 | &:hover, 25 | &:focus { 26 | opacity: 1; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | *.mdx 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | -------------------------------------------------------------------------------- /components/pagetransition.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { motion } from 'framer-motion' 3 | 4 | const variants = { 5 | initial: { 6 | opacity: 0, 7 | y: 8, 8 | }, 9 | enter: { 10 | opacity: 1, 11 | y: 0, 12 | transition: { 13 | duration: 0.4, 14 | ease: [0.61, 1, 0.88, 1], 15 | }, 16 | }, 17 | } 18 | 19 | const PageTransition = ({ children }: { children: ReactNode }): JSX.Element => ( 20 | 21 | {children} 22 | 23 | ) 24 | 25 | export default PageTransition 26 | -------------------------------------------------------------------------------- /components/pageheader.tsx: -------------------------------------------------------------------------------- 1 | import styles from './pageheader.module.scss' 2 | 3 | type PageHeaderProps = { 4 | title: string | JSX.Element 5 | description?: string | JSX.Element 6 | children?: JSX.Element 7 | compact?: boolean 8 | } 9 | 10 | const PageHeader = ({ title, description, children, compact }: PageHeaderProps): JSX.Element => ( 11 |
12 |

{title}

13 | {description &&

{description}

} 14 | {children} 15 |
16 | ) 17 | 18 | export default PageHeader 19 | -------------------------------------------------------------------------------- /data/blog/post-25.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 他人的学习方法论 3 | publishedAt: 2019-03-17T04:18:41Z 4 | summary: 查看全文>> 5 | tags: ["Learning"] 6 | --- 7 | 8 | 纪录一些网友分享的学习经验,提供给大家参考。 9 | 不管是工作还是业余,总会有新人问你是如何学习的,或者是你带团队了,给团队同学进行指导辅导,有些别人实践的经验可以借鉴来作为指导,另外,尽管你自认为是老司机,也应该了解别人的学习方式,借鉴、思考、实践、总结、到演变;知识、技能、学习的方式都应该在不停的进步或改变的路上,你才能走的更远! 10 | 11 | ## 前端 12 | 13 | - [我如何零基础转行成为一个自信的前端](https://juejin.im/post/5c75d34851882564965edb23) 14 | - [一名【合格】前端工程师的自检清单](https://juejin.im/post/5cc1da82f265da036023b628) 15 | 16 | 17 | 18 | 19 | --- 20 | 本人自动发布于:[https://github.com/giscafer/blog/issues/25](https://github.com/giscafer/blog/issues/25) 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "downlevelIteration": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /components/postswitch.tsx: -------------------------------------------------------------------------------- 1 | import Switch from 'rc-switch' 2 | import { FC } from 'react' 3 | import styles from './postswitch.module.scss' 4 | 5 | type PostListSwitchProps = { 6 | checked: boolean 7 | // eslint-disable-next-line no-unused-vars 8 | onChange: (v: boolean) => void 9 | } 10 | 11 | const PostListSwitch: FC = ({ checked = false, onChange }: PostListSwitchProps) => { 12 | return ( 13 |
14 | onChange(v)} checkedChildren="显示封面" unCheckedChildren="隐藏封面" /> 15 |
16 | ) 17 | } 18 | 19 | export default PostListSwitch 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | public/sitemap.xml 18 | public/feed.xml 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | .eslintcache 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # vercel 37 | .vercel 38 | 39 | 40 | # Contentlayer 41 | .contentlayer 42 | -------------------------------------------------------------------------------- /components/badge.module.scss: -------------------------------------------------------------------------------- 1 | .tag { 2 | display: inline-flex; 3 | align-items: center; 4 | box-sizing: border-box; 5 | padding-top: 0.125rem; 6 | padding-right: 0.625rem; 7 | padding-bottom: 0.125rem; 8 | padding-left: 0.625rem; 9 | color: var(--tag-color); 10 | font-size: 0.75rem; 11 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; 12 | line-height: 1rem; 13 | tab-size: 4; 14 | background-color: var(--tag-bg-color); 15 | border: 0 solid #eaeaea; 16 | border-radius: 9999px; 17 | cursor: pointer; 18 | -webkit-text-size-adjust: 100%; 19 | -webkit-font-smoothing: antialiased; 20 | } 21 | -------------------------------------------------------------------------------- /components/tags.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import styles from './tags.module.scss' 4 | 5 | type TagsProps = { 6 | tags: string[] 7 | } 8 | 9 | const Tags = ({ tags }: TagsProps): JSX.Element | null => { 10 | if (!tags?.length) { 11 | return null 12 | } 13 | return ( 14 |
    15 | {tags.map(tag => { 16 | return ( 17 |
  • 18 | {`#${tag}`} 19 | {/* {`#${tag}`} */} 20 |
  • 21 | ) 22 | })} 23 |
24 | ) 25 | } 26 | 27 | export default Tags 28 | -------------------------------------------------------------------------------- /hooks/useinterval.js: -------------------------------------------------------------------------------- 1 | /* https://overreacted.io/making-setinterval-declarative-with-react-hooks/ */ 2 | 3 | import { useEffect, useRef } from 'react' 4 | 5 | function useInterval(callback, delay) { 6 | const savedCallback = useRef() 7 | 8 | // Remember the latest callback. 9 | useEffect(() => { 10 | savedCallback.current = callback 11 | }, [callback]) 12 | 13 | // Set up the interval. 14 | useEffect(() => { 15 | function tick() { 16 | savedCallback.current() 17 | } 18 | if (delay !== null) { 19 | const id = setInterval(tick, delay) 20 | return () => clearInterval(id) 21 | } 22 | return null 23 | }, [delay]) 24 | } 25 | 26 | export default useInterval 27 | -------------------------------------------------------------------------------- /components/input.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from 'react' 2 | import cn from 'classnames' 3 | import styles from './input.module.scss' 4 | 5 | type InputProps = { 6 | type: string 7 | placeholder?: string 8 | disabled?: boolean 9 | value: string | number 10 | onChange: (e: ChangeEvent) => void // eslint-disable-line 11 | className?: string 12 | } 13 | 14 | const Input = ({ type = 'text', placeholder, disabled, value, onChange, className }: InputProps): JSX.Element => ( 15 | 23 | ) 24 | 25 | export default Input 26 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { withContentlayer } = require('next-contentlayer') // eslint-disable-line 2 | 3 | module.exports = withContentlayer()({ 4 | webpack5: true, 5 | images: { 6 | domains: [ 7 | 'user-images.githubusercontent.com', 8 | 'files.mdnice.com', 9 | 'cdn.nlark.com', 10 | 'wpimg.wallstcn.com', 11 | 'github.com', 12 | 'giscafer.com', 13 | 'ww1.sinaimg.cn', 14 | ], 15 | formats: ['image/avif', 'image/webp'], 16 | }, 17 | webpack: (config, { isServer }) => { 18 | if (isServer) { 19 | require('./scripts/generate-sitemap') // eslint-disable-line 20 | require('./scripts/generate-rss') // eslint-disable-line 21 | } 22 | 23 | return config 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /components/blog/parallaxcover.tsx: -------------------------------------------------------------------------------- 1 | import Parallax from 'components/parallax' 2 | import Image from 'next/image' 3 | import styles from './parallaxcover.module.scss' 4 | 5 | const ParallaxCover = (): JSX.Element => ( 6 |
7 |
8 | 9 | 10 | 11 |
12 | {/*
13 | default logo 14 |
*/} 15 |
16 | ) 17 | 18 | export default ParallaxCover 19 | -------------------------------------------------------------------------------- /components/pageheader.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | margin-bottom: 50px; 3 | 4 | // @media (min-width: 480px) { 5 | // margin-bottom: 90px; 6 | // } 7 | } 8 | 9 | .wrapperCompact { 10 | margin-bottom: 0; 11 | 12 | @media (min-width: 480px) { 13 | margin-bottom: 40px; 14 | } 15 | } 16 | 17 | .title { 18 | margin-bottom: 12px; 19 | font-weight: 800; 20 | font-size: 26px; 21 | letter-spacing: -0.3px; 22 | 23 | @media (min-width: 480px) { 24 | margin-bottom: 15px; 25 | font-size: 38px; 26 | } 27 | } 28 | 29 | .description { 30 | margin-bottom: 30px; 31 | font-weight: 500; 32 | font-size: 17px; 33 | line-height: 1.5; 34 | opacity: 0.8; 35 | 36 | @media (min-width: 480px) { 37 | font-size: 19px; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /components/seo.tsx: -------------------------------------------------------------------------------- 1 | import { DefaultSeo } from 'next-seo' 2 | 3 | const config = { 4 | title: 'Nicky Lao - Frontend Developer & Designer', 5 | description: 6 | '自驱型前端工程师,9年多大型复杂产品开发经验,4年前端团队管理经验,熟悉跨端APP 如 Ionic、React Native开发,熟悉 微前端、DevOps 等,具备全栈开发能力。', 7 | openGraph: { 8 | type: 'website', 9 | locale: 'zh_CN', 10 | url: 'https://www.giscafer.com', 11 | site_name: 'giscafer | Nicky Lao', 12 | images: [ 13 | { 14 | url: 'https://giscafer.com/avatar.png', 15 | alt: 'Nicky Lao', 16 | }, 17 | ], 18 | }, 19 | twitter: { 20 | handle: '@giscafer', 21 | site: '@giscafer', 22 | cardType: 'summary_large_image', 23 | }, 24 | } 25 | 26 | const SEO = (): JSX.Element => { 27 | return 28 | } 29 | 30 | export default SEO 31 | -------------------------------------------------------------------------------- /components/postlist.module.scss: -------------------------------------------------------------------------------- 1 | .list { 2 | margin: 0; 3 | margin-bottom: 60px; 4 | padding: 0; 5 | list-style: none; 6 | 7 | li { 8 | margin-bottom: 4rem; 9 | } 10 | } 11 | 12 | .title { 13 | display: block; 14 | margin-bottom: 10px; 15 | color: var(--brand); 16 | font-weight: 700; 17 | font-size: 20px; 18 | 19 | @media (min-width: 480px) { 20 | font-size: 22px; 21 | } 22 | } 23 | 24 | .summary { 25 | margin-bottom: 10px; 26 | font-size: 16px; 27 | opacity: 0.7; 28 | 29 | @media (min-width: 480px) { 30 | font-size: 14px; 31 | // font-size: 18px; 32 | } 33 | } 34 | 35 | .meta { 36 | font-size: 0.95em; 37 | opacity: 0.5; 38 | } 39 | 40 | .noResults { 41 | margin-bottom: 100px; 42 | font-weight: 500; 43 | font-size: 22px; 44 | text-align: center; 45 | } 46 | -------------------------------------------------------------------------------- /lib/gtag.ts: -------------------------------------------------------------------------------- 1 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages 2 | export const pageview = (url: string): void => { 3 | window.gtag('config', process.env.NEXT_PUBLIC_GA_TRACKING_ID, { 4 | page_path: url, 5 | }) 6 | } 7 | 8 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 9 | export const event = ({ action, category, label, value }: { action: string; category?: string; label?: string; value?: string }): void => { 10 | window.gtag('event', action, { 11 | event_category: category, 12 | event_label: label, 13 | value, 14 | }) 15 | } 16 | 17 | // https://developers.google.com/gtagjs/reference/event#search 18 | export const search = (value: string): void => { 19 | window.gtag('event', 'search', { 20 | search_term: value, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /data/blog/post-15.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reactive Programming Learning 3 | publishedAt: 2017-06-06T06:51:57Z 4 | summary: 查看全文>> 5 | tags: ["RxJS"] 6 | --- 7 | 8 | ## 博文&教程 9 | 10 | - [译文 The introduction to Reactive Programming you've been missing](http://www.lightskystreet.com/2015/11/29/translate-introduction-to-reactive/) 11 | 12 | - [rx-book](http://xgrommx.github.io/rx-book/content/guidelines/introduction/index.html) 13 | 14 | - [30 天精通 RxJS 系列](https://ithelp.ithome.com.tw/users/20103367/ironman/1199) 15 | 16 | ## 使用技巧 17 | - [angular-rxjs-when-should-i-unsubscribe-from-subscription](https://stackoverflow.com/questions/38008334/angular-rxjs-when-should-i-unsubscribe-from-subscription) 18 | 19 | --- 20 | 本人自动发布于:[https://github.com/giscafer/blog/issues/15](https://github.com/giscafer/blog/issues/15) 21 | -------------------------------------------------------------------------------- /pages/api/subscribe.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 4 | const { email } = req.body 5 | 6 | if (!email) { 7 | return res.status(400).json({ error: 'Email is required' }) 8 | } 9 | 10 | const result = await fetch('https://www.getrevue.co/api/v2/subscribers', { 11 | method: 'POST', 12 | headers: { 13 | Authorization: `Token ${process.env.REVUE_API_KEY}`, 14 | 'Content-Type': 'application/json', 15 | }, 16 | body: JSON.stringify({ email }), 17 | }) 18 | // console.log('result', result) 19 | const data = await result.json() 20 | 21 | if (!result.ok) { 22 | return res.status(500).json({ error: data.error.email[0] }) 23 | } 24 | 25 | return res.status(201).json({ error: '' }) 26 | } 27 | -------------------------------------------------------------------------------- /data/blog/post-17.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: React vs Angular vs Vue.js 3 | publishedAt: 2017-12-26T12:23:48Z 4 | summary: 查看全文>> 5 | tags: ["Vue","React","Angular"] 6 | --- 7 | 8 | # React vs Angular 9 | 10 | [Why we chose Angular 2 over React for our enterprise software development work](https://blog.biznas.io/why-we-chose-angular-2-over-react-for-our-enterprise-software-development-work-392e2c9e39a9) 11 | 12 | [React vs Angular: An In-depth Comparison](https://www.sitepoint.com/react-vs-angular/) 13 | 14 | [React vs Angular 2:冰与火之歌「译」](http://huangxuan.me/2016/02/01/React-vs-Angular2/) 15 | 16 | # Vue.js vs React vs Angular 17 | 18 | [Vue.js Comparison with Other Frameworks](https://vuejs.org/v2/guide/comparison.html) 19 | 20 | 21 | 22 | --- 23 | 本人自动发布于:[https://github.com/giscafer/blog/issues/17](https://github.com/giscafer/blog/issues/17) 24 | -------------------------------------------------------------------------------- /components/hitcounter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const HitCounter = ({ slug }: { slug: string }): JSX.Element => { 4 | const [hits, setHits] = useState(undefined) 5 | 6 | useEffect(() => { 7 | // Don't count hits on localhost 8 | if (process.env.NODE_ENV !== 'production') { 9 | return 10 | } 11 | // Invoke the function by making a request. 12 | // Update the URL to match the format of your platform. 13 | fetch(`/api/register-hit?slug=${slug}`) 14 | .then(res => res.json()) 15 | .then(json => { 16 | if (typeof json.hits === 'number') { 17 | setHits(json.hits) 18 | } 19 | }) 20 | }, [slug]) 21 | 22 | return ( 23 | <> 24 | · {typeof hits === 'undefined' ? '--' : hits} 阅读量 25 | 26 | ) 27 | } 28 | 29 | export default HitCounter 30 | -------------------------------------------------------------------------------- /components/warning.module.scss: -------------------------------------------------------------------------------- 1 | .warning { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: flex-start; 5 | padding: 10px 15px; 6 | overflow-x: auto; 7 | font-weight: 500; 8 | font-size: 1rem; 9 | background-color: var(--boxBg); 10 | border-radius: 5px; 11 | 12 | @media (min-width: 480px) { 13 | padding: 16px 20px; 14 | font-size: 1rem; 15 | } 16 | 17 | svg { 18 | flex: 0 0 20px; 19 | width: 20px; 20 | margin-top: 6px; 21 | margin-right: 10px; 22 | 23 | @media (min-width: 480px) { 24 | flex: 0 0 30px; 25 | width: 30px; 26 | margin-top: 10px; 27 | margin-right: 15px; 28 | } 29 | } 30 | 31 | path, 32 | line, 33 | circle { 34 | stroke: var(--brand); 35 | } 36 | 37 | p.text { 38 | margin: 0; 39 | 40 | pre { 41 | max-width: 75vw; 42 | margin: 1em 0 0.5em 0; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /data/blog/post-7.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: MVVM的理解 3 | publishedAt: 2016-08-17T15:28:27Z 4 | summary: 查看全文>> 5 | tags: ["AngularJS"] 6 | --- 7 | 8 | ### MVVM与MVC的区别? 9 | 10 | 在MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 MVC模型关注的是Model的不变,所以,在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。 11 | 12 | MVVM在概念上是真正将页面与数据逻辑分离的模式,它把数据绑定工作放到一个JS里去实现,而这个JS文件的主要功能是完成数据的绑定,即把model绑定到UI的元素上。 13 | 14 | 有人做过测试:使用Angular(MVVM)代替Backbone(MVC)来开发,代码可以减少一半。 15 | 16 | 此外,MVVM另一个重要特性,双向绑定 17 | ### MVC,MVP 和 MVVM 的图示 18 | 19 | http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html 20 | 21 | ### MVVM框架性能优化 ng1的动态模板为例 22 | 23 | ![ng1mvvm](https://user-images.githubusercontent.com/8676711/46901209-3dd37880-cee2-11e8-9d6b-f66b5779e4c7.png) 24 | 25 | 26 | --- 27 | 本人自动发布于:[https://github.com/giscafer/blog/issues/7](https://github.com/giscafer/blog/issues/7) 28 | -------------------------------------------------------------------------------- /components/section.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, FC } from 'react' 2 | 3 | type Heading = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' 4 | 5 | interface TitleProps { 6 | as?: Heading 7 | action?: ReactNode 8 | } 9 | 10 | type PageProps = { 11 | children: ReactNode 12 | } 13 | 14 | const Title: FC = ({ as: TitleComponent = 'h1', action, children }: TitleProps & PageProps) => { 15 | return ( 16 |
17 | {children} 18 | {action} 19 |
20 | ) 21 | } 22 | 23 | const Content: FC = ({ children }: PageProps) => { 24 | return
{children}
25 | } 26 | 27 | const SectionRoot: FC = ({ children }: PageProps) => { 28 | return
{children}
29 | } 30 | 31 | export const Section = Object.assign(SectionRoot, { Title, Content }) 32 | -------------------------------------------------------------------------------- /components/blog/parallaxcover.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | height: 0; 4 | margin-right: -15px; 5 | margin-bottom: 15px; 6 | margin-left: -15px; 7 | padding-bottom: 40%; 8 | background-color: #00000c; 9 | transform: translateZ(0); // Safari overflow bugfix 10 | 11 | @media (min-width: 480px) { 12 | margin-right: -20px; 13 | margin-bottom: 30px; 14 | margin-left: -20px; 15 | padding-bottom: 50%; 16 | overflow: hidden; 17 | border-radius: 12px; 18 | } 19 | } 20 | 21 | .parallaxContainer { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | bottom: 0; 26 | left: 0; 27 | overflow: hidden; 28 | 29 | img { 30 | width: 100%; 31 | } 32 | } 33 | 34 | .logo { 35 | position: absolute; 36 | top: 50%; 37 | left: 50%; 38 | width: 50px; 39 | height: 50px; 40 | transform: translate(-50%, -50%); 41 | @media (min-width: 540px) { 42 | width: 120px; 43 | height: 120px; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /components/likebutton.module.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | transform-origin: center bottom; 3 | transition: fill 200ms ease-out, stroke 200ms ease-out; 4 | animation-name: bounce; 5 | animation-duration: 1s; 6 | animation-fill-mode: both; 7 | fill: var(--brand); 8 | stroke: var(--brand); 9 | } 10 | 11 | @keyframes bounce { 12 | from, 13 | 20%, 14 | 53%, 15 | to { 16 | transform: translate3d(0, 0, 0); 17 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 18 | } 19 | 20 | 40%, 21 | 43% { 22 | transform: translate3d(0, -30px, 0) scaleY(1.1); 23 | animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 24 | } 25 | 26 | 70% { 27 | transform: translate3d(0, -15px, 0) scaleY(1.05); 28 | animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); 29 | } 30 | 31 | 80% { 32 | transform: translate3d(0, 0, 0) scaleY(0.95); 33 | transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 34 | } 35 | 36 | 90% { 37 | transform: translate3d(0, -4px, 0) scaleY(1.02); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/generate-rss.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { promises: fs } = require('fs') 3 | const path = require('path') 4 | const RSS = require('rss') 5 | const matter = require('gray-matter') 6 | 7 | async function generate() { 8 | const feed = new RSS({ 9 | title: 'Nicky Lao', 10 | site_url: 'https://giscafer.com', 11 | feed_url: 'https://giscafer.com/feed.xml', 12 | }) 13 | 14 | const posts = await fs.readdir(path.join(__dirname, '..', 'data', 'blog')) 15 | 16 | await Promise.all( 17 | posts.map(async name => { 18 | const content = await fs.readFile(path.join(__dirname, '..', 'data', 'blog', name)) 19 | const frontmatter = matter(content) 20 | 21 | feed.item({ 22 | title: frontmatter.data.title, 23 | url: 'https://giscafer.com/blog/' + name.replace(/\.mdx?/, ''), 24 | date: frontmatter.data.publishedAt, 25 | description: frontmatter.data.summary, 26 | }) 27 | }), 28 | ) 29 | 30 | await fs.writeFile('./public/feed.xml', feed.xml({ indent: true })) 31 | } 32 | 33 | generate() 34 | -------------------------------------------------------------------------------- /components/page.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: var(--siteWidth); 3 | margin: 0 auto; 4 | padding: 0 15px; 5 | 6 | @media (min-width: 480px) { 7 | padding: 0 30px; 8 | } 9 | } 10 | 11 | .main { 12 | margin-bottom: 50px; 13 | 14 | @media (min-width: 480px) { 15 | margin-bottom: 80px; 16 | } 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | padding: 30px 0; 22 | border-top: 1px solid var(--border); 23 | 24 | p { 25 | opacity: 0.3; 26 | } 27 | } 28 | .bottomInfo { 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | } 33 | .copyright { 34 | text-align: center; 35 | } 36 | 37 | .links { 38 | display: grid; 39 | margin: 0; 40 | margin-bottom: 50px; 41 | padding: 0; 42 | list-style: none; 43 | grid-template-columns: repeat(3, 1fr); 44 | grid-gap: 20px; 45 | 46 | li { 47 | margin-bottom: 0; 48 | color: var(--textTinted); 49 | font-weight: 500; 50 | transition: color 0.2s ease-out; 51 | 52 | &:hover, 53 | &:focus { 54 | color: var(--text); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pages/api/register-hit.ts: -------------------------------------------------------------------------------- 1 | import faunadb from 'faunadb' 2 | 3 | module.exports = async (req, res) => { 4 | const q = faunadb.query 5 | const client = new faunadb.Client({ 6 | secret: process.env.FAUNA_SECRET_KEY || '', 7 | }) 8 | const { slug } = req.query 9 | if (!slug) { 10 | return res.status(400).json({ 11 | message: 'Article slug not provided', 12 | }) 13 | } 14 | // Check and see if the doc exists. 15 | const doesDocExist = await client.query(q.Exists(q.Match(q.Index('hits_by_slug'), slug))) 16 | if (!doesDocExist) { 17 | await client.query( 18 | q.Create(q.Collection('hits'), { 19 | data: { slug, hits: 0 }, 20 | }), 21 | ) 22 | } 23 | // Fetch the document for-real 24 | const document = (await client.query(q.Get(q.Match(q.Index('hits_by_slug'), slug)))) as { ref: string; data: { hits: number } } 25 | await client.query( 26 | q.Update(document.ref, { 27 | data: { 28 | hits: document.data.hits + 1, 29 | }, 30 | }), 31 | ) 32 | return res.status(200).json({ 33 | hits: document.data.hits, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /lib/localstorage.ts: -------------------------------------------------------------------------------- 1 | export const safeLocalStorageSetItem = (key: string, item: string): void => { 2 | let storageAccessible = false 3 | try { 4 | localStorage.setItem('testkey', 'testvalue') 5 | localStorage.removeItem('testkey') 6 | storageAccessible = true 7 | } catch (e) { 8 | storageAccessible = false 9 | } 10 | if (storageAccessible) { 11 | localStorage.setItem(key, item) 12 | } 13 | } 14 | 15 | export const safeLocalStorageGetItem = (key: string): string => { 16 | if (typeof Storage !== 'undefined') { 17 | try { 18 | return localStorage.getItem(key) || '' 19 | } catch (e) { 20 | return '' 21 | } 22 | } 23 | return '' 24 | } 25 | 26 | export const safeLocalStorageRemoveItem = (key: string): string => { 27 | if (typeof Storage !== 'undefined') { 28 | try { 29 | localStorage.removeItem(key) 30 | } catch (e) { 31 | return '' 32 | } 33 | } 34 | return '' 35 | } 36 | 37 | export const safeLocalStorage = { 38 | getItem: safeLocalStorageGetItem, 39 | setItem: safeLocalStorageSetItem, 40 | removeItem: safeLocalStorageRemoveItem, 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nicky Lao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { ThemeProvider } from 'next-themes' 4 | import type { AppProps } from 'next/app' 5 | import { AnimatePresence } from 'framer-motion' 6 | import Head from 'next/head' 7 | import SEO from 'components/seo' 8 | import * as gtag from 'lib/gtag' 9 | import '../styles/globals.scss' 10 | 11 | function MyApp({ Component, pageProps }: AppProps): JSX.Element { 12 | const router = useRouter() 13 | 14 | useEffect(() => { 15 | const handleRouteChange = (url: string) => { 16 | gtag.pageview(url) 17 | } 18 | router.events.on('routeChangeComplete', handleRouteChange) 19 | return () => { 20 | router.events.off('routeChangeComplete', handleRouteChange) 21 | } 22 | }, [router.events]) 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default MyApp 37 | -------------------------------------------------------------------------------- /data/blog/post-9.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: angularjs单页面动态ng-include模块页面 3 | publishedAt: 2016-08-31T01:52:09Z 4 | summary: 查看全文>> 5 | tags: ["AngularJS"] 6 | --- 7 | 8 | ### 需求描述 9 | 10 | 1、单页面,左侧类似树状导航,右侧是视图的布局; 11 | 2、点击左侧树功能节点,右侧切换显示对应功能页面;显示模式有两种,一种是**替换展示**,新点击的功能替换原来的功能页,展示最新点击的功能页;另一种是**追加展示**,左侧功能相当于多选,右侧视图展示`选择`的功能的所有页面,追加显示再最后; 12 | ### 实现 13 | 14 | 1、angularjs路由无法解决这个问题;使用ng-include页面,动态获取页面的路径,提前注入所有页面的controller。 15 | 2、一开始用了字符串拼接,然后$compile一下,使得子页面的controller的$scope作用域被修改,获取不到模型数据。需要注意的是controller的作用域就好了,采用以下方式解决。 16 | 17 | ``` javascript 18 |
19 |
20 | 22 |
23 |
24 |
25 | ``` 26 | 27 | 思路来自:http://stackoverflow.com/questions/17801988/dynamically-loading-controllers-and-ng-include 28 | 29 | --- 30 | 31 | 2016-8-31 09:51:42 32 | 33 | 34 | --- 35 | 本人自动发布于:[https://github.com/giscafer/blog/issues/9](https://github.com/giscafer/blog/issues/9) 36 | -------------------------------------------------------------------------------- /scripts/github/job.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const cron = require('node-cron') 3 | const syncPost = require('./syncPost') 4 | 5 | // https://www.npmjs.com/package/node-cron 6 | // # ┌────────────── second (optional) 7 | // # │ ┌──────────── minute 8 | // # │ │ ┌────────── hour 9 | // # │ │ │ ┌──────── day of month 10 | // # │ │ │ │ ┌────── month 11 | // # │ │ │ │ │ ┌──── day of week 12 | // # │ │ │ │ │ │ 13 | // # │ │ │ │ │ │ 14 | // # * * * * * * 15 | // const timeString = '15 * * * *' 16 | const timeString = '* 0 1 * * *' // Running a job at 01:00 every day 17 | 18 | let job = null 19 | 20 | function stopJob() { 21 | if (job) { 22 | job.stop() 23 | job = null 24 | } 25 | } 26 | 27 | function startJob() { 28 | stopJob() 29 | job = cron.schedule( 30 | timeString, 31 | () => { 32 | // eslint-disable-next-line no-console 33 | console.log('🚀🚀 同步issue到mdx文件') 34 | syncPost() 35 | }, 36 | { 37 | scheduled: true, 38 | timezone: 'Asia/Shanghai', 39 | }, 40 | ) 41 | 42 | job.start() 43 | console.log('====================================') 44 | console.log('🚀🚀 同步代码定时任务已开启') 45 | console.log('====================================') 46 | } 47 | 48 | // syncPost() // 先执行一次 49 | 50 | export { startJob } 51 | -------------------------------------------------------------------------------- /components/button.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import cn from 'classnames' 3 | import styles from './button.module.scss' 4 | 5 | type ButtonProps = { 6 | children: JSX.Element | JSX.Element[] | string 7 | type?: 'button' | 'submit' | 'reset' 8 | href?: string 9 | onClick?: () => void 10 | variant?: 'transparent' | 'like' 11 | disabled?: boolean 12 | } 13 | 14 | const Button = ({ children, type, href, variant, onClick, disabled }: ButtonProps): JSX.Element => { 15 | const classes = cn(styles.button, styles[variant]) 16 | if (onClick || !href) { 17 | return ( 18 | 21 | ) 22 | } 23 | if (href.startsWith('/')) { 24 | return ( 25 | 26 | 27 | 30 | 31 | 32 | ) 33 | } 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ) 40 | } 41 | 42 | export default Button 43 | -------------------------------------------------------------------------------- /components/likebutton.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Heart } from 'react-feather' 3 | import Button from 'components/button' 4 | import { safeLocalStorage as localStorage } from 'lib/localstorage' 5 | 6 | import useSWR, { mutate } from 'swr' 7 | import fetcher from 'lib/fetcher' 8 | 9 | import styles from './likebutton.module.scss' 10 | 11 | const LikeButton = ({ slug }: { slug: string }): JSX.Element | null => { 12 | const [mounted, setMounted] = useState(false) 13 | const { data } = useSWR(`/api/likes?slug=${slug}`, fetcher) 14 | const likes = data?.likes 15 | const liked = localStorage.getItem(slug) === 'true' 16 | 17 | useEffect(() => setMounted(true), []) 18 | 19 | const onLike = async () => { 20 | // 本地记录了喜欢过本文章 21 | localStorage.setItem(slug, 'true') 22 | // 请求更新like 23 | mutate(`/api/likes?slug=${slug}`, { ...data, likes: likes + 1 }, false) 24 | await fetch(`/api/likes?slug=${slug}`, { method: 'POST' }) 25 | } 26 | 27 | if (!mounted) return null 28 | 29 | return ( 30 | 33 | ) 34 | } 35 | 36 | export default LikeButton 37 | -------------------------------------------------------------------------------- /components/messages.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import styles from './messages.module.scss' 3 | 4 | export const TailBreakdown = () => ( 5 |
    6 |
  1. Hello
  2. 7 |
8 | ) 9 | 10 | const messages = [ 11 | { text: "Hey there! What's up", sent: true }, 12 | { text: 'Checking out iOS7 you know..' }, 13 | { text: 'Check out this bubble!', sent: true }, 14 | { text: "It's pretty cool!" }, 15 | { text: "And it's in css?" }, 16 | { text: "Yeah it's pure CSS & HTML", sent: true }, 17 | { text: '(ok.. almost, I added a tiny bit of JS to remove sibling message tails)', sent: true }, 18 | { text: "Wow that's impressive. But what's even more impressive is that this bubble is really high." }, 19 | ] 20 | 21 | const Messages = (): JSX.Element => ( 22 |
    23 | {messages.map(({ text, sent }, i) => { 24 | const isLast = i === messages.length - 1 25 | const noTail = !isLast && messages[i + 1]?.sent === sent 26 | return ( 27 |
  1. 28 | {text} 29 |
  2. 30 | ) 31 | })} 32 |
33 | ) 34 | 35 | export default Messages 36 | -------------------------------------------------------------------------------- /components/segmentedcontrol.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { motion, AnimateSharedLayout } from 'framer-motion' 3 | import styles from './segmentedcontrol.module.scss' 4 | 5 | type SegmentedControlProps = { 6 | items: Array 7 | } 8 | 9 | const SegmentedControl = ({ items }: SegmentedControlProps): JSX.Element => { 10 | const [activeItem, setActiveitem] = useState(0) 11 | return ( 12 | 13 |
    14 | {items.map((item, i) => { 15 | const isActive = i === activeItem 16 | return ( 17 | 22 | 26 | 27 | ) 28 | })} 29 |
30 |
31 | ) 32 | } 33 | 34 | export default SegmentedControl 35 | -------------------------------------------------------------------------------- /scripts/generate-sitemap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fs = require('fs') 3 | const globby = require('globby') 4 | const prettier = require('prettier') 5 | 6 | ;(async () => { 7 | const prettierConfig = await prettier.resolveConfig('./.prettier.config.js') 8 | 9 | // Ignore Next.js specific files (e.g., _app.js) and API routes. 10 | const pages = await globby(['pages/*.tsx', 'data/**/*.mdx', '!pages/_*.tsx', '!pages/api', '!pages/404.tsx']) 11 | const sitemap = ` 12 | 13 | 14 | ${[...pages] 15 | .map(page => { 16 | const path = page.replace('pages', '').replace('data', '').replace('.tsx', '').replace('.mdx', '') 17 | const route = path === '/index' ? '' : path 18 | 19 | return ` 20 | 21 | ${`https://giscafer.com${route}`} 22 | 23 | ` 24 | }) 25 | .join('')} 26 | 27 | ` 28 | 29 | const formatted = prettier.format(sitemap, { 30 | ...prettierConfig, 31 | parser: 'html', 32 | }) 33 | 34 | fs.writeFileSync('public/sitemap.xml', formatted) 35 | })() 36 | -------------------------------------------------------------------------------- /.github/workflows/sync-post.yml: -------------------------------------------------------------------------------- 1 | name: Sync Post 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # schedule: 6 | # - cron: "30 1 * * *" 7 | # https://docs.github.com/cn/developers/webhooks-and-events/events/issue-event-types 8 | issues: 9 | types: 10 | - opened 11 | - closed 12 | - renamed 13 | - labeled 14 | - unlabeled 15 | - reopened 16 | - committed # 修改? 17 | workflow_dispatch: 18 | env: 19 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 20 | GH_USER: ${{ secrets.GH_USER }} 21 | GH_PROJECT_NAME: ${{ secrets.GH_PROJECT_NAME }} 22 | jobs: 23 | Publish: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 🛎️ 27 | uses: actions/checkout@v2 28 | 29 | - name: Git config 🔧 30 | run: | 31 | git config --global user.name "giscafer" 32 | git config --global user.email "giscafer@outlook.com" 33 | 34 | - name: Display runtime info ✨ 35 | run: | 36 | echo '当前目录:' 37 | pwd 38 | 39 | - name: Install 🔧 40 | run: yarn install 41 | 42 | # - name: Build ⛏️ 43 | # run: yarn build 44 | 45 | - name: Update blog files ⛏️ 46 | run: | 47 | yarn sync-post 48 | git add . 49 | git commit -m 'chore(ci): blog sync' 50 | git push 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # giscafer.com 2 | 3 | 我个人的 网站/博客。 技术栈 **Next.js/Typescript** & 部署在 **Vercel**。 博客数据来自 [issues 列表](https://github.com/giscafer/blog/issues) 4 | 5 | 博客原理:通过 ci 监听 issues 变更,自动更新 mdx 文件到项目 `data/blog/*.mdx` 文件夹中,Vercel 自动化构建更新。 6 | 7 | - https://www.giscafer.com 8 | 9 | ## Features 10 | 11 | - Github Issues 自动同步(见[实现文章介绍](https://mp.weixin.qq.com/s/sMNC20ei_J0XcVdJ0v3Fjw)) 12 | 13 | 14 | 15 | ## TODO 16 | 17 | 18 | 19 | - [x] 文章浏览量统计 20 | - [x] Tags 分类 21 | - [x] 评论功能 22 | - [ ] mdx 文件 自动获取 summary 字段展示 23 | - [ ] 图片点击放大预览 24 | - [ ] 优化同步脚步,只同步最近变更时间的 issues 25 | 26 | ## Usage 27 | 28 | > 以下配置可以直接修改 `scripts/github/syncPost.js` 里边对应变量,就不需要配置了 29 | 30 | - 本地运行博客时,测试博客文章同步脚本请修改`env` 环境变量 `GH_USER` 和 `GH_PROJECT_NAME`,保证 api 获取 issues 生成 mdx 文件 31 | - GitHub Action Secret 配置 `GH_USER` 和 `GH_PROJECT_NAME`,保证 CI 执行正常,也可以直接修改 32 | 33 | ## 微信公众号 34 | 35 | > ID: giscafer 36 | 37 | 38 | 39 | 42 | 43 | --- 44 | 45 | > [giscafer.com](http://giscafer.com)  ·  46 | > GitHub [@giscafer](https://github.com/giscafer)  ·  47 | > Twitter [@Nicky Lao](https://twitter.com/nicky_lao) 48 | -------------------------------------------------------------------------------- /components/segmentedcontrol.module.scss: -------------------------------------------------------------------------------- 1 | .list { 2 | display: inline-flex; 3 | margin: 0; 4 | padding: 3px; 5 | list-style: none; 6 | background-color: var(--boxBg); 7 | border-radius: 10px; 8 | 9 | .item { 10 | position: relative; 11 | margin-bottom: 0; 12 | line-height: 1; 13 | 14 | &:after { 15 | position: absolute; 16 | top: 15%; 17 | right: -0.5px; 18 | display: block; 19 | width: 1px; 20 | height: 70%; 21 | background-color: var(--border); 22 | transition: opacity 200ms ease-out; 23 | content: ''; 24 | } 25 | 26 | &:last-of-type { 27 | &:after { 28 | display: none; 29 | } 30 | } 31 | } 32 | } 33 | 34 | .itemNoDivider { 35 | composes: item; 36 | 37 | &:after { 38 | opacity: 0; 39 | } 40 | } 41 | 42 | .button { 43 | position: relative; 44 | margin: 0; 45 | padding: 7px 30px; 46 | color: var(--text); 47 | line-height: 1; 48 | background: transparent; 49 | border: none; 50 | outline: none; 51 | 52 | &:hover, 53 | &:focus { 54 | cursor: pointer; 55 | } 56 | } 57 | 58 | .label { 59 | position: relative; 60 | z-index: 2; 61 | } 62 | 63 | .active { 64 | position: absolute; 65 | top: 0; 66 | right: 0; 67 | bottom: 0; 68 | left: 0; 69 | z-index: 1; 70 | background-color: var(--inputBg); 71 | border-radius: 7px; 72 | box-shadow: var(--shadow); 73 | content: ''; 74 | } 75 | -------------------------------------------------------------------------------- /components/themechanger.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | display: none; 4 | width: 51px; 5 | height: 51px; 6 | margin-right: -3px; 7 | 8 | @media (min-width: 480px) { 9 | display: block; 10 | } 11 | } 12 | 13 | .menu { 14 | position: absolute; 15 | top: 0; 16 | right: 0; 17 | display: flex; 18 | padding: 3px; 19 | } 20 | 21 | .bg { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | bottom: 0; 26 | left: 0; 27 | width: 100%; 28 | background-color: black; 29 | border-radius: 9999px; 30 | box-shadow: 0 3px 13px rgba(0, 0, 0, 0.2); 31 | content: ''; 32 | } 33 | 34 | .button { 35 | position: relative; 36 | flex-shrink: 0; 37 | width: 45px; 38 | height: 45px; 39 | margin: 0; 40 | margin-right: 10px; 41 | padding: 0; 42 | color: white; 43 | background-color: transparent; 44 | border: none; 45 | 46 | &:last-of-type { 47 | margin-right: 0; 48 | } 49 | 50 | &:hover, 51 | &:focus { 52 | cursor: pointer; 53 | } 54 | } 55 | 56 | .buttonBackground { 57 | position: absolute; 58 | top: 0; 59 | right: 0; 60 | bottom: 0; 61 | left: 0; 62 | z-index: 1; 63 | display: block; 64 | width: 100%; 65 | height: 100%; 66 | background-color: var(--brand); 67 | border-radius: 9999px; 68 | } 69 | 70 | .buttonLabel { 71 | position: absolute; 72 | top: 50%; 73 | left: 50%; 74 | z-index: 2; 75 | transform: translate(-50%, -50%); 76 | } 77 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ['plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'airbnb', 'plugin:prettier/recommended'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | ecmaVersion: 12, 14 | sourceType: 'module', 15 | tsconfigRootDir: './', 16 | }, 17 | plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier', 'import'], 18 | rules: { 19 | 'react/react-in-jsx-scope': 0, 20 | 'react/jsx-filename-extension': [ 21 | 1, 22 | { 23 | extensions: ['.js', '.tsx'], 24 | }, 25 | ], 26 | 'react/jsx-one-expression-per-line': 0, 27 | 'react/require-default-props': 0, 28 | 'react/jsx-props-no-spreading': 0, 29 | 'jsx-a11y/anchor-is-valid': 0, // next-links require empty a tags 30 | 'jsx-a11y/label-has-associated-control': 0, 31 | 'import/prefer-default-export': 0, 32 | 'import/extensions': 0, 33 | 'jsx-a11y/click-events-have-key-events': 0, 34 | 'jsx-a11y/no-static-element-interactions': 0, 35 | 'jsx-a11y/interactive-supports-focus': 0, 36 | '@typescript-eslint/explicit-module-boundary-types': 0, 37 | 'no-underscore-dangle': 0, 38 | }, 39 | globals: { 40 | JSX: true, 41 | }, 42 | settings: { 43 | 'import/resolver': { 44 | typescript: {}, 45 | }, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /components/blog/rating.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | display: inline-flex; 4 | align-items: center; 5 | } 6 | 7 | .star { 8 | display: flex; 9 | width: 18px; 10 | margin-right: 2px; 11 | color: var(--brand); 12 | fill: var(--brand); 13 | 14 | &:last-of-type { 15 | margin-right: 0; 16 | } 17 | } 18 | 19 | .overlay { 20 | position: absolute; 21 | top: 0; 22 | right: 0; 23 | bottom: 0; 24 | z-index: 1; 25 | background-color: black; 26 | opacity: 0.7; 27 | 28 | @supports (mix-blend-mode: color) { 29 | opacity: unset; 30 | mix-blend-mode: color; 31 | } 32 | } 33 | 34 | .playground { 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | padding: 40px 20px 10px; 39 | background-color: var(--boxBg); 40 | 41 | @media (min-width: 480px) { 42 | padding: 40px 40px 20px; 43 | } 44 | 45 | svg { 46 | width: 24px; 47 | height: auto; 48 | } 49 | } 50 | 51 | .controls { 52 | display: flex; 53 | padding-top: 30px; 54 | 55 | label { 56 | display: flex; 57 | align-items: center; 58 | margin: 0 10px; 59 | } 60 | 61 | input { 62 | margin-bottom: 0; 63 | margin-left: 8px; 64 | padding: 6px 8px; 65 | font-size: 1em; 66 | font-family: inherit; 67 | background-color: var(--bg); 68 | border: none; 69 | border-radius: 4px; 70 | } 71 | } 72 | 73 | .pointer { 74 | color: var(--textTinted); 75 | font-size: 0.9em; 76 | text-align: center; 77 | } 78 | -------------------------------------------------------------------------------- /components/button.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-flex; 3 | align-items: center; 4 | padding: 10px 18px; 5 | color: var(--brand); 6 | font-weight: 700; 7 | font-size: 16px; 8 | font-family: var(--fontFamily); 9 | background-color: var(--brandTinted); 10 | border: none; 11 | border-radius: 9999px; 12 | transition: background-color 200ms ease-out, color 200ms ease-out; 13 | 14 | &:hover, 15 | &:focus { 16 | background-color: var(--brandTintedActive); 17 | cursor: pointer; 18 | } 19 | 20 | svg { 21 | width: 18px; 22 | margin-left: 8px; 23 | } 24 | 25 | &[disabled] { 26 | opacity: 0.5; 27 | pointer-events: none; 28 | } 29 | } 30 | 31 | .transparent { 32 | padding: 0; 33 | color: var(--brand); 34 | font-weight: 500; 35 | background-color: transparent; 36 | 37 | &:hover, 38 | &:focus { 39 | color: var(--brandActive); 40 | background-color: transparent; 41 | cursor: pointer; 42 | } 43 | } 44 | 45 | .like { 46 | margin-bottom: 50px; 47 | padding: 14px 60px; 48 | color: var(--text); 49 | font-weight: 700; 50 | font-size: 18px; 51 | line-height: 1; 52 | background-color: var(--likeButton); 53 | border-radius: 30px; 54 | transition: transform 200ms ease-out, background-color 200ms ease-out; 55 | 56 | &:hover, 57 | &:focus { 58 | background-color: var(--likeButtonHover); 59 | transform: scale(1.03); 60 | } 61 | 62 | &[disabled] { 63 | opacity: 1; 64 | } 65 | 66 | svg { 67 | width: 20px; 68 | margin-right: 10px; 69 | margin-left: 0; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /components/blog/rating.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Star } from 'react-feather' 3 | import styles from './rating.module.scss' 4 | 5 | type RatingProps = { 6 | value: number 7 | max?: number 8 | } 9 | 10 | const Rating = ({ value, max = 5 }: RatingProps) => { 11 | /* Calculate how much of the stars should be "filled" */ 12 | const percentage = Math.round((value / max) * 100) 13 | 14 | return ( 15 |
16 | {/* Create an array based on the max rating, render a star for each */} 17 | {Array.from(Array(max).keys()).map((_, i) => ( 18 | 19 | ))} 20 | {/* Render a div overlayed on top of the stars that are not filled */} 21 |
22 |
23 | ) 24 | } 25 | 26 | export const RatingPlayground = () => { 27 | const [value, setValue] = useState(4.6) 28 | const [max, setMax] = useState(5) 29 | 30 | return ( 31 |
32 | 33 |
34 | 38 | 42 |
43 |

Test the component by changing the values above 👆

44 |
45 | ) 46 | } 47 | 48 | export default Rating 49 | -------------------------------------------------------------------------------- /components/parallax.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useLayoutEffect, ReactNode } from 'react' 2 | import { motion, useViewportScroll, useTransform, useSpring, useReducedMotion } from 'framer-motion' 3 | 4 | type ParallaxProps = { 5 | children: ReactNode 6 | offset?: number 7 | clampInitial?: boolean 8 | clampFinal?: boolean 9 | } 10 | 11 | const Parallax = ({ children, offset = 50, clampInitial, clampFinal }: ParallaxProps): JSX.Element => { 12 | const prefersReducedMotion = useReducedMotion() 13 | const [elementTop, setElementTop] = useState(0) 14 | const [clientHeight, setClientHeight] = useState(0) 15 | const ref = useRef(null) 16 | 17 | const { scrollY } = useViewportScroll() 18 | 19 | const initial = elementTop - clientHeight 20 | const final = elementTop + offset 21 | 22 | const yRange = useTransform(scrollY, [initial, final], [clampInitial ? 0 : offset, clampFinal ? 0 : -offset]) 23 | const y = useSpring(yRange, { stiffness: 400, damping: 90 }) 24 | 25 | useLayoutEffect(() => { 26 | const element = ref.current 27 | const onResize = () => { 28 | if (element) { 29 | setElementTop(element?.getBoundingClientRect().top + window.scrollY || window.pageYOffset) 30 | setClientHeight(window.innerHeight) 31 | } 32 | } 33 | onResize() 34 | window.addEventListener('resize', onResize) 35 | return () => window.removeEventListener('resize', onResize) 36 | }, [ref]) 37 | 38 | // Don't parallax if the user has "reduced motion" enabled 39 | if (prefersReducedMotion) { 40 | return <>{children} 41 | } 42 | 43 | return ( 44 | 45 | {children} 46 | 47 | ) 48 | } 49 | 50 | export default Parallax 51 | -------------------------------------------------------------------------------- /data/blog/post-12.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: AngularJS 动态创建组件 3 | publishedAt: 2017-04-29T07:58:21Z 4 | summary: 查看全文>> 5 | tags: ["AngularJS"] 6 | --- 7 | 8 | 类似angularjs中的`ng-include`,或者是动态创建html,再$compile编译一下。这些语法在ng2+已经被去掉,去掉原因如下: 9 | 10 | > Something like ng-include can not be supported for several reasons: 11 | > 12 | > - In Angular 2 directives are declared on per component. Having ng-include would mean that the same ng-include would behave differently depending where it is included. The same goes for variable declarations. 13 | > - It is a security risk, in the sense of you may point it to a user input. 14 | > - It prevents Angular from doing offline compilation, hence speed / size improvements. 15 | > 16 | > The solution is that you need to wrap your templates into components, and then you can lazy load the components. This will work with offline compilation, does not have security concerns and still allows for offline compilation. 17 | 18 | 19 | 20 | 21 | ## 参考资料 22 | 23 | **Blog** 24 | https://blog.lacolaco.net/post/dynamic-component-creation-in-angular-2/ 25 | 26 | **NgComponentOutlet**(含代码举例) 27 | http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 28 | 29 | **Issues** 30 | [Final statement about ngInclude alternative](https://github.com/angular/angular/issues/7596) 31 | [Proposal: Declarative Dynamic Components](https://github.com/angular/angular/issues/9599) 32 | 33 | 34 | ## MVVM框架性能优化 ng1的动态模板为例 35 | 36 | ![ng1mvvm](https://user-images.githubusercontent.com/8676711/46901201-1d0b2300-cee2-11e8-9979-db7c5bae8fda.png) 37 | 38 | 39 | --- 40 | 本人自动发布于:[https://github.com/giscafer/blog/issues/12](https://github.com/giscafer/blog/issues/12) 41 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default class MyDocument extends Document { 4 | render(): JSX.Element { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | {/* Global Site Tag (gtag.js) - Google Analytics */} 12 | 95 | ``` 96 | 97 | ### 内容安全策略及子资源完整性 98 | 99 | 你可以根据`内容安全策略(CSP)`来配置你的服务器使得指定类型的文件遵守 `SRI`。这是通过在 `CSP 头部` 添加 `require-sri-for` 指令实现的: 100 | 101 | ``` 102 | Content-Security-Policy: require-sri-for script; 103 | ``` 104 | 这条指令规定了所有 `JavaScript` 都要有 **integrity** 属性,且通过验证才能被加载。 105 | 106 | 所以,只要文件变化了,浏览器就不会执行,有效避免了脚本攻击。 107 | 108 | 109 | 110 | --- 111 | 参考链接 112 | 113 | - https://developer.mozilla.org/zh-CN/docs/Web/Security 114 | - https://www.jianshu.com/p/217b11f5f953 115 | 116 | 117 | --- 118 | 本人自动发布于:[https://github.com/giscafer/blog/issues/35](https://github.com/giscafer/blog/issues/35) 119 | -------------------------------------------------------------------------------- /data/blog/post-59.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 编程中的23种设计模式 3 | publishedAt: 2022-10-11T11:10:03Z 4 | summary: 查看全文>> 5 | tags: ["设计模式"] 6 | --- 7 | 8 | > 王争《设计模式之美》专栏学习笔记 9 | 10 | 设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。 11 | 12 | 设计模式在于设计的意图,也就是应用场景。如果单纯看设计思路或者代码实现,一些模式是比较相似的,比如策略模式和工厂模式。 13 | 14 | 借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则,高内聚低耦合等特性,从此来控制和应对代码的复杂性,提供代码的可扩展性。 15 | ### 创建型模式 16 | > 解决对象的创建问题 17 | 18 | - 单例模式 19 | - 工厂模式 20 | - 建造者模式 21 | - 原型模式 22 | 23 | ### 结构性模式 24 | 25 | > 类或对象的组合或组装 26 | 27 | - 代理模式 28 | - 增强原始类无关的功能 29 | - 常用于:监控、统计、鉴权、限流、事务、幂等、日志、RPC、缓存等 30 | - 桥接模式 31 | - “抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。 32 | - 应用1:JDBC驱动(JDBC和Driver独立开发,组合在一起) 33 | - 应用2:API监控告警,通知渠道:邮件、短信、微信、自动语音电话;紧急程度:严重、紧急、普通、无关要紧;不同的紧急程度对应不同的通知渠道 34 | - 装饰器模式 35 | - 增强原始类相关的方法(所以需要继承同样的父类或者实现同样的接口),能嵌套多个装饰器 36 | - 应用1:Java IO 流 37 | - 应用2:增加缓存 38 | - 适配器模式 39 | - “补救策略”,提供跟原始类不同的接口。代理模式和装饰器模式提供的都是跟原始类相同的接口。 40 | - 应用: 41 | - 封装有缺陷的接口设计 42 | - 统一多个类的接口设计 43 | - 替换依赖的外部系统 44 | - 兼容老版本接口 45 | - 适配不同的数据格式 46 | - 门面模式 47 | - 解决的是接口易用性、性能、事务问题,而适配器解决的是原接口和目标接口不适配的兼容性问题。 48 | - 应用:子系统(或App)需要调用多个接口的情况;用户和钱包两个领域的分布式事务问题。 49 | - 组合模式 50 | - 应用:文件系统目录结构 51 | - 享元模式 52 | - 目的是共享使用对象,达到复用目的(通过一个List或Map缓存已经创建好的享元对象) 53 | - 应用1:棋牌模式、文本编辑器 54 | - 应用2:Java Integer(-127~127利用享元模式提前创建好)、String 利用享元模式复用字符串常量。 55 | 56 | ### 行为型模式 57 | 58 | > 类或对象之间的交互 59 | 60 | - 观察者模式 61 | - 也称为发布订阅模式,是一对多的关系。将观察者和被观察者代码解耦。 62 | - 应用1:RSS Feeds,邮件订阅 63 | - 应用2:进程内有同步阻塞或异步非阻塞实现方式,异步非阻塞如EventBus;跨进程的观察者模式如RPC接口调用,消息队列MQ 64 | - 与**生产者>消费者**的区别: 65 | - 生产者和消费者是以异步实现,多对多,**并且消费者存在竞争关系,一条消息只能被一个消费者消费。** 66 | - 发布订阅则可以同步也可以异步方式实现,**一对多,一条消息可以被多个订阅者消费**,订阅者没有竞争关系,发布和订阅两者也在流程上存在先后关系 67 | - 模板模式 68 | - 模板方法和回调应用场景是一致的,都是定义好算法骨架,并对外开放扩展点,符合开闭原则;两者的却别是代码的实现上不同,**模板方法是通过继承来实现,是自己调用自己**;**回调是类之间的组**合。 69 | - 应用:JdbcTemplate、RedisTemplate、RestTemplate 70 | - 策略模式 71 | - 解耦策略的定义、创建、使用,控制代码复杂度,让每个部分都不至于过复杂,代码行数过多。移除 if-else 和 switch-case 分支逻辑判断(策略工厂),保证代码的可维护性,和遵循KISS原则。 72 | - **应用**:对一个文件进行排序。文件中只有整数,并且相邻的数字通过逗号区隔。文件的大小可能:10GB(外部排序)、100GB(多线程外部排序)、 1TB(多机MapReduce)。 73 | - 职责链模式 74 | - 将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。 75 | - **应用**:敏感词过滤、Servlet Filter、Spring Interceptor 76 | - 状态模式 77 | - 又叫有限状态机,有3个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发转态的转移和动作的执行。动作可以不是必须的。**状态机的三种实现方式**:分支逻辑法、查表法、状态模式。 78 | - 应用:游戏、工作流引擎 79 | - 迭代器模式 80 | - 数组遍历器,在编程语言中已经提供为一种类库来使用 81 | - 应用:1、遍历集合的同时不要增删元素;2、设计一个支持快照功能的iterator 82 | - 访问者模式 83 | - 允许一个或多个操作应用到一组对象上,解耦操作和对象本身。双分派语言(Double Dispatch)不需要支持访问者模式 84 | - 应用:批量处理PDF、PPT、Word文件,提取内容放到 txt 文件中。定义访问者类(Extractor、Compressor),将对象和操作解耦,将业务操作抽离出来,使得符合开闭原则。 85 | - 备忘录模式 86 | - 也叫快照模式,在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,一遍之后方便恢复对象为先前状态。 87 | - 应用:防丢失、撤销、恢复等。通过**全量备份和增量备份结合**的方式(低频全量备份,高频增量备份),减少存储和时间的消耗。 88 | - 命令模式 89 | - 命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。 90 | - 应用:用来控制命令的执行,比如:异步、延迟、排队执行命令、存储命令、给命令记录日志等。 91 | - 解释器模式 92 | - 解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器来处理这个语法。 93 | - 应用:1、告警规则,如触发条件:key1 > 100 && key2 < 1000 || key3 == 200;2、计算表达式等,如 8 3 2 4 - + * 94 | - 中介模式 95 | - 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。 96 | - 应用:UI交互,如注册用户页面,切换用户登录和用户注册的UI,显示不同输入框等。 97 | - 与观察者模式的区别:中介模式一般可以控制参与者的执行顺序,而观察者一般控制不了。 98 | 99 | ### 总结 100 | 101 | 熟悉23种设计模式的原理、实现、设计意图和应用场景。避免过度设计,避免设计不足。设计的初衷是提高代码质量,不要脱离具体的场景去谈设计。 102 | 103 | ![设计模式](https://user-images.githubusercontent.com/8676711/195074843-ce384709-1ed1-4aeb-8a67-24e1aabe6984.png) 104 | 105 | 106 | 107 | 108 | --- 109 | 本人自动发布于:[https://github.com/giscafer/blog/issues/59](https://github.com/giscafer/blog/issues/59) 110 | -------------------------------------------------------------------------------- /data/blog/post-33.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Angular 工程 ng build 构建提速 3 | publishedAt: 2019-08-30T02:53:19Z 4 | summary: 查看全文>> 5 | tags: ["性能优化","Angular"] 6 | --- 7 | 8 | > 公司项目统一都使用 [angular-cli](https://github.com/angular/angular-cli) 来搭建工程环境,从 Angular2 到 Angular8 版本都经历了,老项目都基本升级到 Angular4、6,新一点的项目,ng 版本都是7、8了。许多项目构建的速度,一直都是正常的表现,某一两个项目表现的比较异常,这不得不采取相关改进措施。 9 | 10 | Angular 生产环境构建打包 `ng build --prod` 是开启了 `AOT` ([为什么要AOT编译](https://angular.cn/guide/aot-compiler#why-compile-with-aot)),`ng build` 构建配置项也比较多,含义介绍见文档:[build](https://angular.io/cli/build),常见配置属性设置如下: 11 | 12 | ```json 13 | "prod": { 14 | "optimization": true, 15 | "outputHashing": "all", 16 | "sourceMap": false, 17 | "extractCss": true, 18 | "namedChunks": false, 19 | "aot": true, 20 | "extractLicenses": true, 21 | "vendorChunk": false, 22 | "buildOptimizer": true, 23 | "fileReplacements": [ 24 | { 25 | "replace": "src/environments/environment.local.ts", 26 | "with": "src/environments/environment.prod.ts" 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | 同样的配置,在不同的项目,构建时间长短也是不一样的。所以影响项目构建时间的原因可能有: 33 | 34 | - **项目代码问题**(结构,路由划分,代码规范) 35 | 后期优化的可能性小,所以前期需要做好规范限制; 36 | - **代码量**(系统越大,ts文件越多,构建时间会越长) 37 | 和系统大小有关,建议根据模块和路由划分,来加载划分一些模块代码,对构建生成的chunk大小也有帮助; 38 | - **第三方依赖**( node_modules 依赖第三方的模块数量) 39 | - **Angular CLI 版本**(cli 自身问题) 40 | 关注版本变化更新日记 41 | - **angular.json 配置影响** 42 | - **系统硬件性能**( 16G 的iMac 明显比 16G内存的Windows 10系统快两倍多) 43 | - **变更脚手架** (改用SystemJS 和 Webpack 搭建环境,不用ng-cli) 44 | 45 | 46 | 47 | ## 解决方案探索 48 | 49 | _Tips: 我们构建默认都统一加大了node.js的执行内存_ 50 | 51 | 52 | 我们有个项目,再上线工单系统的页面之后,多出了10分钟的构建时间,构建时间在 15~20分钟 区间浮动。截图是 Gitlab CI/CD Build Job的(iMac 是8分钟内) 53 | 54 | angular-build-15min 55 | 56 | 本地iMac的构建速度: 57 | 58 | angular-build1 59 | 60 | 61 | 至于为什么 构建参数、配置不变的情况下,iMac 构建会比 gitlab runner(Linux 以及 Windows 系统)快很多,初步单纯认为是硬件性能的影响。我们项目持续集成服务器是 Linux 非 iMac,所以优化的时候,以CI/CD服务器构建效果的速度作为参考。 62 | 63 | ### 找到构建过程最慢的点 64 | 65 | 构建过程最慢的有两个地方分别是`79%` 和92%的操作: 66 | 67 | ```bash 68 | 79% chunk modules optimization ModuleConcatenationPlugin 69 | ``` 70 | ```bash 71 | 92% chuck asset optimization 72 | ``` 73 | 74 | 所有慢的构建都在这里浪费时间的,有网友是这么描述: 75 | 76 | > I've just upgraded my CLI to 1.7.2 (I've double checked the node_modules and --version) and a my build time has gone from about 15 seconds to infinity 77 | > It hangs forever at "92% chunk asset optimization", I've waited more than 10 mins before giving up 78 | > It's a very small app 79 | 80 | 理解这两个过程是干了什么,然后再去查找解决方案。官方文档是这么描述着两个属性配置的: 81 | 82 | 83 | buildOptimizer=true\|false | Enables '@angular-devkit/build-optimizer' optimizations when using the 'aot' option.Default: false 84 | -- | -- 85 | 86 | optimization=true\|false | Enables optimization of the build output. 87 | -- | -- 88 | 89 | ### 关闭 optimization 和 buildOptimizer 90 | 91 | 经实践,修改着两个配置属性为`false`后,构建就提速多倍,如图 92 | 93 | angular_optimization_false 94 | 95 | 又得必有失,从图中也可以看出来,提速了,但是单个文件代码体积明显提升了,因为关闭了optimization ,输出的文件体积没有做优化。 96 | 97 | ### 其他手段 98 | 99 | ……未完待续 100 | 101 | --- 102 | 参考资源: 103 | - https://www.bountysource.com/issues/46543750-build-with-ng-prod-is-extremely-slow 104 | - https://github.com/angular/angular-cli/issues/6795 105 | 106 | 107 | --- 108 | 109 | > Author: [@giscafer](https://github.com/giscafer),原文地址:[front-end-manual/issues/33](https://github.com/giscafer/front-end-manual/issues/33) , 110 | > 欢迎讨论 111 | 112 | 113 | 114 | --- 115 | 本人自动发布于:[https://github.com/giscafer/blog/issues/33](https://github.com/giscafer/blog/issues/33) 116 | -------------------------------------------------------------------------------- /data/blog/post-51.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 设计模式 - 单例模式 3 | publishedAt: 2021-08-11T06:04:04Z 4 | summary: 查看全文>> 5 | tags: ["设计模式"] 6 | --- 7 | 8 | **单例模式(Singleton Design Patterns)**:一个类只允许创建一个实例,单例一般用来处理资源访问冲突、或者是表示一个全局唯一类。 9 | 10 | ## 为什么说支持懒加载的双重检测不比饿汉式更优? 11 | 12 | - 饿汉模式:类加载时提前初始化静态实例,不支持延迟加载 13 | - 懒汉模式:支持延迟加载,但函数锁造成加锁解锁频繁,并发低,存在性能问题 14 | - 双重检测:在函数内部进行判断加类就级别锁,静态对象实例化之后不再触发加锁解锁的情况,并发高 15 | - 内部静态类:比双重检测简单。 16 | 17 | 在前端,由于js是单线程的,所以,不会存在锁的情况,不过也可以了解后端是通过锁来解决这个并发问题的。 18 | 19 | 20 | ### 饿汉模式 21 | 22 | > ts 代码 23 | 24 | ```ts 25 | class SingletonEhan { 26 | private id: number = 0; 27 | private static instance: SingletonEhan = new SingletonEhan(); 28 | private SingletonEhan() {} 29 | private static getInstance() { 30 | return SingletonEhan.instance; 31 | } 32 | 33 | getId() { 34 | return (this.id += 1); 35 | } 36 | } 37 | ``` 38 | 39 | ### 懒汉模式 40 | 41 | > ts 代码 42 | 43 | ```ts 44 | class SingletonLhan { 45 | private id: number = 0; 46 | private static instance: SingletonLhan; 47 | private SingletonLhan() {} 48 | // java 写的话函数加上 synchronized 锁,导致频繁加锁和解锁并发低 49 | // js 单线程所以不需要考虑此问题 50 | private static getInstance() { 51 | if (!this.instance) { 52 | this.instance = new SingletonLhan(); 53 | } 54 | 55 | return this.instance; 56 | } 57 | 58 | getId() { 59 | return (this.id += 1); 60 | } 61 | } 62 | ``` 63 | 64 | ### 双重检测 65 | 66 | > ts 代码 67 | 68 | ```ts 69 | class SingletonLhan2 { 70 | private id: number = 0; 71 | private static instance: SingletonLhan; 72 | private SingletonLhan() {} 73 | 74 | private static getInstance() { 75 | if (!this.instance) { 76 | // java 写的话函数加上 synchronized 锁,解决频繁加锁和解锁并发低问题 77 | // js 单线程所以不需要考虑此问题 78 | // synchronized(SingletonLhan2.class){ 79 | // if (!this.instance) { 80 | // this.instance = new SingletonLhan(); 81 | // } 82 | // } 83 | this.instance = new SingletonLhan(); 84 | } 85 | 86 | return this.instance; 87 | } 88 | 89 | getId() { 90 | return (this.id += 1); 91 | } 92 | } 93 | ``` 94 | 95 | ### 静态内部类 96 | 97 | > java 代码 98 | 99 | ```java 100 | 101 | public class SingletonInner{ 102 | private int id=0; 103 | 104 | private constructor(){} 105 | 106 | private static class Inner{ 107 | private static SingletonInner instance = new SingletonInner(); 108 | } 109 | 110 | private static SingletonInner getInstance(){ 111 | return Inner.instance; 112 | } 113 | 114 | public int getId(){ 115 | return id+=1; 116 | } 117 | } 118 | 119 | ``` 120 | 121 | --- 122 | 123 | 思考,如果不是用 typescript 来写,es6 写的话不存在 private 属性的东西,如何实现? 124 | 125 | —— 闭包 126 | 127 | ## 单例模式有哪些问题?有没有替代方案? 128 | 129 | ### 单例的问题 130 | 131 | - 单例对OOP特性(封装、抽象、继承、多态)编程不友好 132 | - 单例会隐藏类之间的依赖关系 133 | - 单例对代码的扩展性不友好 134 | 比如,如果设计数据库连接池为单例类,慢SQL优化的时候,想将慢SQL独立一个数据库连接池,这时候扩展性就比较差。不适合设计成单例模式。 135 | - 单例对代码的可测试性不友好 136 | 单例的局部变量是全局可变、被所有代码共享的,修改会影响到别的测试结果。 137 | - 单例不支持有参数的构造函数 138 | 实际上可扩展支持 139 | 140 | ### 替代方案 141 | 142 | 为了保证全局唯一性,除了单例类,还可以用以下方法: 143 | 144 | - 静态方法实现。 145 | - 将单例生成的对象,作为参数传入函数。 146 | - 工程模式、IOC 147 | 148 | 149 | 150 | ## 集群环境下的分布式单例模式 151 | 152 | #### 单例模式中的唯一性 153 | 154 | 一个单例类只能实例化一个实例对象。 155 | 156 | #### 线程唯一的单例 157 | 158 | 一个线程中单例实例是唯一的,线程间实例不同,可以用 `HashMap<线程id,单例实例对象>` 来存储区分 159 | 160 | #### 进程唯一的单例 161 | 162 | 一个进程中的单例实例是唯一的,进程中多线程都是使用同一个单例实例对象。进程间不唯一。因为应用程序最新执行单元就是进程起步,进程分配了独立的运行空间,不同进程间的环境和内存是独立隔离开的。 163 | 164 | #### 如何实现集群环境下的单例? 165 | 166 | 集群是多个服务器或者多个应用程序部署,程序运行环境和内存都是独立的,实际上就是多进程如何保证单例唯一。要使得集群应用的单例是唯一的,需要借助存储共享的能力。 167 | 168 | 使用共享存储区(比如文件),在进程使用到单例时,从共享存储区读取到内存,并反序列化为对象,然后再使用,使用完成之后还要再存储到外部共享存储区。 169 | 170 | 为了保证任何时间,进程之间只有一份对象存在,一进程在获取到对象之后,需要对对象进行加锁,避免其他进程再对其读取。并在使用完成这个对象之后,还需要显式的将对象从内存删除,并释放对对象的加锁。 171 | 172 | #### 如何实现一个多例模式? 173 | 174 | 使用HashMap 来控制 175 | 176 | --- 177 | 178 | Java中单例的唯一性作用的范围不是进程,而是类的加载器 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | --- 190 | 本人自动发布于:[https://github.com/giscafer/blog/issues/51](https://github.com/giscafer/blog/issues/51) 191 | -------------------------------------------------------------------------------- /data/blog/post-58.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 我的摸鱼划水实践方案 3 | publishedAt: 2022-08-28T10:57:08Z 4 | summary: 查看全文>> 5 | tags: ["技术方案"] 6 | --- 7 | 8 | ## 背景 9 | 10 | 对于中后台Web管理系统,不管需求怎么变,不管UED怎么设计,都是千变一律的列表+表单+详情页面展示。面对不同的产品,不同的设计师,可能会重复的去写这些千变一律的代码。这样的问题怎么去解决?有两个大方向:1、生成代码,尽量不写;2、区块、组件资源物料市场。 11 | 12 | **生成代码,尽量不写**。业界上的 Low-Code 和 No Code 方案框架也层出不穷,只是在不停的卷。但没有多少能真正解决中小型公司的问题。强行套用也只会变成维护难,技术债。对于这方面的框架学了解,可以阅读之前整理的文章 [《Low-Code开源项目调研》](https://github.com/giscafer/blog/issues/50) 13 | 14 | **区块、组件资源物料市场**。需要考验前端团队基建能力,以及日常开发代码规范,抽象能力,让物料市场持续丰富起来。在公司里的各部门直接共享是可以的。但是当脱离了公司,或者是换了公司,基本也是从0开始了。物料市场,之前也测试搞了个试验品,源码开源,了解前往:[roothub](https://github.com/RootLinkFE/roothub) 15 | 16 | 以上两个方案都有实践尝试过,在不投入更多的研发成本和时间积累成本,是无法做到真正的效益的。对于小团队,是否有别的选择呢?下边介绍一下我业余整的「**摸鱼最佳解决方案**」工程脚手架 17 | 18 | ## 为何叫「摸鱼」解决方案? 19 | 20 | 摸鱼划水。并不代表是贬义的,只要你是个工作认真负责,也有极客追求精神;摸鱼就可以认为是释放生产力,研发效能提升的代名词。多少时间是重复得浪费在前后端联调对接接口,CURD千篇一律,接口返回值处理,表单页面开发和表单验证逻辑处理等。只要不需要再写这些,研发时间就可以减少60%。 21 | 22 | 早之前尝试过百度 [amis](https://github.com/baidu/amis) 的前端低代码框架,获得了一些设计灵感。在试用amis时也写过一个 [sailor(水手)](https://github.com/giscafer/sailor)demo 。暗示:摸鱼划水选手! 23 | 24 | ## 摸鱼最佳解决方案 25 | 26 | 摸鱼最佳解决方案遵守的三个原则: 27 | 28 | - 1、**更少的代码**。 不写可能重复写的任何代码逻辑。 29 | - 2、**可扩展性,无技术债**。组件封装都无 breaking changed,**保留原生写法的前提**下扩展动态配置开发方式。 30 | - 3、**保证易维护性、复用性**。换团队时组件、配置化开发方式都能复用;只要你用 antd 就可以摸鱼了。 31 | 32 | 整个方案提效的两大点: 33 | - 列表开发 RhTable 34 | - 动态表单 RhDynamicToolkit 35 | 36 | 详细用法和 demo 请前往查看 [giscafer/rh-template-umi](https://github.com/giscafer/rh-template-umi.git) 37 | 38 | ## 实践测评 39 | 40 | 开发摸鱼方案之后,自己也在真是项目开发中实践使用了。业余时间或周末开发,总耗时约 5天左右完成一个后台管理系统。以下是整个系统的提交日记 41 | 42 | image 43 | 44 | 这种后台管理系统。**只要摸鱼方案解决了列表+表单的逻辑处理和接口CURD,其他内容的代码开发基本很少消耗开发者的时间** 。 45 | 46 | ### 列表开发和详情页面开发 47 | 48 | image 49 | image 50 | image 51 | 52 | ### 动态表单页面 53 | 54 | - 支持modal form、drawer form 和 单页表单 55 | - 表单编辑&新增逻辑不需要硬编码,配置api url 即可 56 | - 表单验证配置 57 | 58 | #### 表单验证 validator 规则 59 | 60 | `validator` 是个 数组,数组中每一项为一个对象,对象中的属性名称为可以是下面的任意一个,对象中还有一个 `message` 属性,用于描述错误信息。都有默认错误信息模版,满足可以不填,**但建议正则表达式填写错误提示信息,以便用户可以明确知道真正的输入格式。** 61 | 62 | - pattern 正则表达式(默认错误信息为:`格式不正确`) 63 | - range 数值区间(默认错误信息为:`请输入${range[0]}~${range[1]}之间的数字`) 64 | - rangeInt 整数数值区间(默认错误信息为:`请输入${range[0]}~${range[1]}之间的整数`) 65 | - maxLength 文本最大长度(默认错误信息为:`请输入${maxLength}个字符以内`) 66 | - min 数值最小值(默认错误信息为:`请输入大于等于${min}的数字`) 67 | - max 数值最大值(默认错误信息为:`请输入小于等于${max}的数字`) 68 | - expression 表达式验证器 (无默认错误信息,建议配置 `message` 字段) 69 | 70 | 举例: 71 | 72 | > 建议在正则校验规则里通过 `message` 自定义提示,才能让用户明确清楚要怎么填,否则只能提示默认模版的`格式不正确` 73 | 74 | ```js 75 | { 76 | "validator": [ 77 | { 78 | "type": "pattern", 79 | "value": "^[a-zA-Z_]w*$", 80 | "message": "只能输入字母、数字和下划线,不能以数字开头" 81 | }, 82 | // 支持多种规则,但如果有一种能验证完就用一种就好 83 | { "type": "maxLength", "value": 15 } 84 | ] 85 | } 86 | ``` 87 | 88 | `expression` 表达式验证器,支持用模版 `${数学表达式}` 来验证表单;程序内置了强大的表达式引擎,详细见文档:[表达式](https://github.com/giscafer/rh-template-umi/blob/main/packages/rh-components/src/RhDynamicToolkit/docs/expression.md) 89 | 90 | ```json 91 | { 92 | "validator": [ 93 | { 94 | "type": "expression", 95 | "value": "${collection.packagePrice+value<=60}", 96 | "message": "保证价格与运费价格之和不能超过60" 97 | } 98 | ] 99 | } 100 | ``` 101 | 102 | 一个表单配置文件举例: 103 | 104 | image 105 | 106 | 107 | ## 总结 108 | 109 | - 效能提升高达60%+ 110 | - 未来规划:继续完善摸鱼方案,实践中反哺;最后会结合 sawgger codegen 生成表单配置文件和列表配置代码,进行进一步提效。 111 | 112 | 113 | 114 | 115 | 116 | 117 | --- 118 | 本人自动发布于:[https://github.com/giscafer/blog/issues/58](https://github.com/giscafer/blog/issues/58) 119 | -------------------------------------------------------------------------------- /data/blog/post-72.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 前端学习AI应用开发路线 3 | publishedAt: 2025-03-06T07:44:14Z 4 | summary: 查看全文>> 5 | tags: ["AI"] 6 | --- 7 | 8 | 作为前端转型 AI 应用开发,你需要补充以下核心知识体系,并逐步建立 AI 工程化思维。以下是系统化学习路径和资源推荐: 9 | 10 | --- 11 | 12 | ### **一、基础理论储备** 13 | #### 1. **机器学习基础** 14 | + **必学概念**: 15 | - 监督学习 vs 无监督学习 16 | - 神经网络基础(感知机、激活函数、反向传播) 17 | - 损失函数与优化器(梯度下降、Adam) 18 | - 过拟合与正则化(Dropout、L1/L2) 19 | + **推荐资源**: 20 | - 📚 书籍:《机器学习实战:基于 Scikit-Learn、Keras 和 TensorFlow》(实践导向) 21 | - 🎥 课程:[吴恩达《机器学习》](https://www.bilibili.com/video/BV1W34y1i7xK/)(经典理论基础) 22 | - 📄 图解教程:[《Visualizing Machine Learning Concepts》](https://jalammar.github.io/)(适合视觉化学习) 23 | - 📄🎥 [aka.ms/ai-agents-beginners](https://aka.ms/ai-agents-beginners) 24 | - 📄🎥 [microsoft/generative-ai-for-beginners](https://github.com/microsoft/generative-ai-for-beginners) 25 | - 📄 [赋范2025大模型技术全景地图](https://kq4b3vgg5b.feishu.cn/wiki/MTuywZLzhioQhHk2yolcjgawnTg) 26 | 27 | #### 2. **深度学习核心** 28 | + **关键领域**: 29 | - NLP(Transformer、BERT、GPT 架构) 30 | - 计算机视觉(CNN、目标检测、图像生成) 31 | - 生成式模型(Diffusion Models、GAN) 32 | + **学习工具**: 33 | - 🛠️ [TensorFlow Playground](https://playground.tensorflow.org/)(神经网络可视化实验) 34 | - 🧩 [Colab Notebooks](https://colab.research.google.com/)(快速运行 AI 代码) 35 | 36 | --- 37 | 38 | ### **二、AI 工程化技能** 39 | #### 1. **AI 应用架构设计** 40 | + **典型架构模式**: 41 | 42 | ```bash 43 | 前端 → API 网关 → 模型服务 → 数据存储 44 | ↘ 缓存层 ↗ 45 | ``` 46 | 47 | + **关键技术点**: 48 | - 模型服务化(REST/gRPC 接口) 49 | - 流式传输(SSE/WebSocket) 50 | - 边缘计算(WebAssembly + ONNX 模型) 51 | 52 | #### 2. **大模型应用开发** 53 | + **核心能力**: 54 | - Prompt Engineering(结构化提示词设计) 55 | - RAG(检索增强生成)架构实现 56 | - Fine-tuning(基于 LoRA 的高效微调) 57 | + **实践工具**: 58 | - 🔧 [LangChain](https://python.langchain.com/)(AI 应用编排框架) 59 | - 🧠 [LlamaIndex](https://www.llamaindex.ai/)(知识库增强检索) 60 | - ⚡ [OpenAI API](https://platform.openai.com/docs)(商业模型快速接入) 61 | - [Model Context Protocol (MCP) ](https://modelcontextprotocol.io/introduction) 62 | 63 | #### 3. **浏览器端 AI** 64 | + **关键技术**: 65 | - TensorFlow.js 模型部署 66 | - WebGL 加速推理 67 | - ONNX Runtime Web 集成 68 | + **实战项目**: 69 | - 实现浏览器内图像分类(MobileNet) 70 | - 文字生成(GPT-2 量化模型) 71 | - 示例代码: 72 | 73 | ```javascript 74 | import * as tf from '@tensorflow/tfjs'; 75 | const model = await tf.loadLayersModel('model.json'); 76 | const pred = model.predict(tensorInput); 77 | ``` 78 | 79 | --- 80 | 81 | ### **三、工具链与技术栈** 82 | 83 | #### 1. **AI 开发工具** 84 | 85 | | 工具类型 | 推荐选择 | 学习重点 | 86 | | --- | --- | --- | 87 | | **开发框架** | PyTorch / TensorFlow.js | 模型部署与转换 | 88 | | **模型仓库** | Hugging Face / Model Zoo | 预训练模型下载与微调 | 89 | | **可视化工具** | Weights & Biases / TensorBoard | 训练过程监控 | 90 | | **边缘计算** | ONNX Runtime / WebDNN | 浏览器端推理优化 | 91 | 92 | 93 | #### 2. **全栈技术延伸** 94 | + **后端技能**: 95 | - Python FastAPI(模型服务化) 96 | - Node.js + TensorFlow.js(全 JS 栈 AI) 97 | - GraphQL(复杂数据查询) 98 | + **DevOps**: 99 | - 模型容器化(Docker + Triton Inference Server) 100 | - 监控(Prometheus + Grafana) 101 | 102 | --- 103 | 104 | ### **四、学习路径与资源** 105 | #### 1. **分阶段学习计划** 106 | 107 | ![举例阶段划分](https://github.com/user-attachments/assets/0fbfa73d-b998-4942-86b1-0906ee974673) 108 | 109 | #### 2. **精选资源列表** 110 | + **免费课程**: 111 | - [Fast.ai《面向程序员的深度学习》](https://course.fast.ai/)(实战优先) 112 | - [Google《生成式 AI 学习路径》](https://cloud.google.com/learn/pathways/generative-ai)(大模型专项) 113 | + **付费课程**: 114 | - [Udacity AI 纳米学位](https://www.udacity.com/school-of-ai)(系统化项目实战) 115 | + **开发文档**: 116 | - [Hugging Face 文档](https://huggingface.co/docs) 117 | - [TensorFlow.js 指南](https://www.tensorflow.org/js/guide) 118 | + **社区与资讯**: 119 | - [Papers With Code](https://paperswithcode.com/)(最新论文+代码) 120 | - [AI Weekly](https://aiweekly.co/)(行业动态简报) 121 | 122 | --- 123 | 124 | ### **五、项目实战推荐** 125 | #### 1. **入门级项目** 126 | + **智能聊天助手**: 127 | - 技术栈:Next.js + OpenAI API + LangChain 128 | - 扩展点:增加知识库检索(LlamaIndex) 129 | + **浏览器图像处理**: 130 | - 技术栈:TensorFlow.js + MobileNet 131 | - 功能:实时滤镜风格迁移 132 | 133 | #### 2. **进阶项目** 134 | + **文档智能分析工具**: 135 | - 技术栈:Python + LangChain + Pinecone 136 | - 功能:PDF 解析与语义搜索 137 | + **低代码 AI 工作流平台**: 138 | - 技术栈:React + FastAPI + Hugging Face 139 | - 功能:拖拽式模型编排 140 | 141 | --- 142 | 143 | ### **六、避坑指南** 144 | 1. **不要过度深入数学**: 145 | 聚焦应用层开发时,理解算法输入输出和调参方法比推导公式更重要。 146 | 2. **警惕技术镀金**: 147 | 优先掌握主流框架(PyTorch/TensorFlow),而非追逐最新论文模型。 148 | 3. **重视数据工程**: 149 | 实际项目中 60% 时间会花在数据清洗和特征工程上。 150 | 4. **工程化思维**: 151 | 学会设计可扩展的 AI 系统架构,而非只关注模型准确率。 152 | 153 | --- 154 | 155 | 转型 AI 应用开发的关键在于 **将前端工程化能力与 AI 模型落地经验结合**。保持对新技术的好奇心,同时深耕 1-2 个垂直领域(如 AIGC 工具开发、智能客服系统),逐步建立技术壁垒。 156 | 157 | 158 | 159 | --- 160 | 本人自动发布于:[https://github.com/giscafer/blog/issues/72](https://github.com/giscafer/blog/issues/72) 161 | -------------------------------------------------------------------------------- /data/blog/post-24.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 理解 Virtual DOM 3 | publishedAt: 2019-03-13T02:48:34Z 4 | summary: 查看全文>> 5 | tags: ["Review","React"] 6 | --- 7 | 8 | # Question List 9 | 10 | - 什么是Virtual DOM? 11 | - Virtual DOM 的对比过程 (preact为例) 12 | 13 | # Answer 14 | 15 | 16 | ## DOM介绍 & 存在问题 17 | 18 | DOM (Document Object Model)是一种通过对象来表示结构化文档的方法,它是一种跨平台的、与语言无关的约定,用于表示HTML、XML和其他格式的数据并与之交互。浏览器通过处理DOM来实现细节,我们可以使用 JavaScript、CSS来与它交互。可以搜索节点并更改它的详细信息,删除或者插入新节点。 19 | 20 | DOM 几乎是跨平台和跨浏览器的,那它有什么问题呢?主要问题是 **DOM 从来没有为创建动态UI进行优化**。 21 | 22 | 我们通过一张图来看浏览器是如何呈现web页面的: 23 | ![renderpage](https://user-images.githubusercontent.com/8676711/54594161-0fe2f900-4a6b-11e9-90a5-14e711749f15.png) 24 | 25 | 浏览器中的页面呈现引擎解析HTML网页以创建DOM,同时解析CSS,并将CSS应用于HTML,和DOM组成一个渲染树(Render Tree),这个过程称为**Attachment**。布局过程(Layout)为每个节点提供精确的坐标,节点在其中进行绘制和展示。我们对DOM进行操作的时候,浏览器就会重复上边的渲染过程。 26 | 27 | 我们可以使用JavaScript 和像 jQuery这样的库去处理DOM,但它们在解决性能问题方面做得很少。想象一下,像微博、Twitter、Facebook 这种社交平台,页面滚动一定情况后,用户浏览器下将有数万个节点,使这些节点之间进行有效的交互、动态UI是一个巨大的问题。 28 | 29 | 30 | ## 如何解决 DOM 性能问题 31 | 32 | ### Shadow DOM 33 | 34 | `Shadow DOM` 是W3C工作草案标准。该规范描述了将多个DOM树组合成一个层次结构的方法,以及这些树如何在文档中相互交互,从而实现更好的DOM组合 35 | 36 | 参考: 37 | - [Shadow DOM](https://www.w3.org/TR/shadow-dom/) 38 | - [Shadow DOM v1:独立的网络组件](https://developers.google.cn/web/fundamentals/web-components/shadowdom?hl=zh-cn) 39 | 40 | 41 | 42 | ### Virtual DOM 43 | 44 | ![vdom](https://user-images.githubusercontent.com/8676711/55935010-1711b700-5c65-11e9-89a1-2c628896d823.png) 45 | 46 | 47 | 指不是直接地接触DOM,而是构建它的`抽象`版本。这样我们使用DOM的某种`轻量副本`,就可以随意的修改它,然后保存到真正的DOM树种。保存时我们应该进行比较,找出DOM节点差异并更改(重新渲染)应该更改的内容。 48 | 49 | 它比直接操作DOM快得多,因为它不需要进入真正DOM的所有重量级部分。它工作得很好,但只有当我们以正确的方式使用它的时候。有两个问题需要解决:`何时重新渲染DOM`以及`如何有效的实现它`。 50 | 51 | **何时重新渲染DOM**——当数据发送更改并需要更新时。 52 | 53 | 但我们怎么知道数据被改变了呢? 54 | 55 | 我们有两个选择: 56 | 57 | - 第一种是 `脏值检测`(dirty checking),定期轮询检测,并递归检测数据结构中的所有值。 58 | - 第二种方式是监听观察状态变化 (observable),如果状态没有改变,我们不需要做什么;如果状态发生改变,我们确切地知道要更新什么了。 59 | 60 | **怎么做**才能真正快速。 61 | 62 | - 高效的 `diff算法` 63 | - 批处理DOM的读写操作 64 | - 只针对子树进行有效的更新 65 | - 使用可观察(Observable )的而不是脏检查来检测更改 66 | 67 | 68 | 总结:`Virtual DOM` 是一种技术和一组库/算法,它允许我们通过避免直接使用DOM和只使用模拟DOM树的轻量级JavaScript对象来提高前端性能。 69 | 70 | 71 | ## Virtual DOM 在 React 中的实现 72 | 73 | ReactJS 使用 `Observale` 来查找修改后的组件,每当在任何组件上调用 `setState()` 方法时,ReactJS 都会标记该组件为`dirty`,并重新渲染它。 74 | 75 | 无论何时调用 `setState()` 方法,ReactJS 都会从头创建整个 `Virtual DOM` ,创建整个`Virtual DOM`非常快,不影响性能。在任何给定的时间,ReactJS 维护两个 `Virtual DOM`,一个使用更新状态后的`Virtual DOM`,另一个使用之前(老的)状态的`Virtual DOM`。 76 | 77 | ReactJS 使用 `diff 算法` 比较两个 `Virtual DOM` 去查找更新真实DOM(Real DOM)的最小步骤。在两棵树之间寻找最小修改数的复杂度为`O(n^3)`。但是React使用启发式方法,并带有一些假设,使得问题的复杂度为 `O(n)`。 78 | 79 | ReactJS 使用以下步骤来查找两个`Virtual DOM`的不同之处: 80 | 81 | - **如果父状态更改,重新渲染所有子状态** 如果父组件状态变化了,ReactJS 会重新渲染所有子组件,不管子组件状态是否发生变化,所以 ReactJS 提供了 `shouldComponentUpdate()` 生命周期方法,用来有效的减少一些没必要的渲染,提升性能 82 | - **广度优先搜索(BFS)** 83 | - **Reconciliation** 确定 `Real DOM` 哪些需要更新的过程:1、不同的元素类型将产生不同的树;2、开发人员可以通过设置`key`属性来告知 ReactJS 哪些子元素可能是稳定的。详细见官方文档[docs/reconciliation](https://reactjs.org/docs/reconciliation.html) 84 | - **批量更新(Batch Update)** ReactJS 等待所有的事件循环完成,才批量将对比好的需要更新的元素更新到 `Real DOM` 85 | 86 | 执行完所有步骤后,React将重新绘制 `Real DOM`。这意味着在事件循环期间,只有一次绘制 `Real DOM`。因此,所有的布局过程只会按时运行,以更新 `Real DOM`。 87 | 88 | ## Virtual DOM Algorithm Flowchart For Preact 89 | 90 | ![1_TF0TZszVwpYc1Pba7Dbk7Q](https://user-images.githubusercontent.com/8676711/55934908-c39f6900-5c64-11e9-9284-54250fda3b1c.png) 91 | 92 | ## React Native 93 | 94 | 归功于VDOM,React Native 可以实现跨平台,详细:[Does React Native have a 'Virtual DOM'?](https://stackoverflow.com/questions/41804855/does-react-native-have-a-virtual-dom) 95 | 96 | --- 97 | 98 | - [浏览器的工作原理:新式网络浏览器幕后揭秘](https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/) 99 | - [A quick guide to learn React and how its Virtual DOM works](https://medium.freecodecamp.org/a-quick-guide-to-learn-react-and-how-its-virtual-dom-works-c869d788cd44) 100 | - [What is Virtual Dom](https://medium.com/@tony_freed/what-is-virtual-dom-c0ec6d6a925c) 101 | - [Performance Comparison for React, Angular and Knockout](http://chrisharrington.github.io/demos/performance/) 102 | - [网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?](https://www.zhihu.com/question/31809713) 103 | - [Medium上Virtual DOM的文章](https://medium.com/search?q=Virtual%20DOM) 104 | - [Virtual DOM in ReactJS](https://hackernoon.com/virtual-dom-in-reactjs-43a3fdb1d130) 105 | - [界面之下:还原真实的MV*模式](https://github.com/livoras/blog/issues/11) 106 | - [深度剖析:如何实现一个 Virtual DOM 算法](https://github.com/livoras/blog/issues/13) 107 | - [Matt-Esch/virtual-dom](https://github.com/Matt-Esch/virtual-dom) A Virtual DOM and diffing algorithm 108 | - https://en.wikipedia.org/wiki/Levenshtein_distance 109 | 110 | 111 | 112 | --- 113 | 本人自动发布于:[https://github.com/giscafer/blog/issues/24](https://github.com/giscafer/blog/issues/24) 114 | -------------------------------------------------------------------------------- /data/blog/post-50.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Low-Code开源项目调研整理 3 | publishedAt: 2021-02-21T03:04:03Z 4 | summary: 查看全文>> 5 | tags: [] 6 | --- 7 | 8 | # 开源的 Low-Code 项目调研 9 | 10 | 11 | 12 | **`Low-Code`** 一词早在 2014 年就由 Forrester 提出了。(见 [wikipedia](https://en.wikipedia.org/wiki/Low-code_development_platform))。 13 | 14 | 你 Google 搜关键词 **“Low-code development platform”** 可以找到很多关于 `Low-Code` 的内容。类似的词有** Pro-Code(纯代码)、Less Code(少写代码)、No-Code(无代码、自动化)**。Low-Code 应该介于 Less Code 与 No-Code 之间。 15 | 16 | 17 | ## 什么是低代码平台(Low-Code)❓ 18 | 19 | 20 | - 低代码开发平台能够实现业务应用的快速交付。也就是说,不只是像传统开发平台一样“能”开发应用而已,低代码开发平台的重点是开发应用更“快”。更重要的是,这个快的程度是颠覆性的:根据Forrester在2016年的调研,大部分公司反馈低代码平台帮助他们把开发效率提升了5-10倍。而且我们有理由相信,随着低代码技术、产品和行业的不断成熟,这个提升倍数还能继续上涨。 21 | 22 | - 低代码开发平台能够降低业务应用的开发成本。一方面,低代码开发在软件全生命周期流程上的投入都要更低(代码编写更少、环境设置和部署成本也更简单);另一方面,低代码开发还显著降低了开发人员的使用门槛,非专业开发者经过简单的IT基础培训就能快速上岗,既能充分调动和利用企业现有的各方面人力资源,也能大幅降低对昂贵专业开发者资源的依赖。 23 | 24 | 25 | 26 | 更多介绍>> [文章:什么是低代码平台?](https://mp.weixin.qq.com/s/tMC1Uas99F28_tU5lH0CNw) 27 | 28 | ## 低代码开源项目(一):平台、框架 29 | 30 | 31 | > 搜索相关类别的开源项目,可以利用 Github 主题分类的筛选方式,比如 [https://github.com/topics/low-code-development-platform](https://github.com/topics/low-code-development-platform),可以点击 Star 关注这类主题,日后 Github 会在你的个人主页推送相关的流行项目。 32 | 33 | 34 | 35 | ### [ice](https://ice.work/) 团队 36 | 37 | 38 | 阿里的物料平台、前端解决方案,详细介绍见 [关于飞冰](https://ice.work/docs/guide/about) 39 | 40 | 41 | **试用感受**:大而全。可以整合自己的物料,根据飞冰的规范、定制自己的物料生态。飞冰出来好几年了,但也未见很多团队去使用开来,有可能是大家使用源码自答了私服?团队太小,不适合使用,团队大了,自身业务和设计也有自己的特色,用不起来,不如自己搭建物料平台。没有自研能力的时候,可以去试用看是否能借飞冰来提升研发效能或者是沉淀自己的物料资源。 42 | 43 | 44 | ### [budibase](https://github.com/Budibase/budibase)  团队 45 | 46 | Budibase是一个开源的低代码开发平台 47 | 48 | **试用感受**:暂未体验,不过看官方感觉挺不错 49 | 50 | 51 | ### [formily](https://github.com/alibaba/formily)  团队 52 | 53 | 阿里巴巴集团统一表单解决方案。 54 | 55 | 56 | **试用感受**:是一个表单框架,主要是阿里业务中后台表单的沉淀,对于大众也比较试用,表单组件库以及schema表单都支持,是写代码的表单方案。 57 | 58 | ### [amis](https://github.com/baidu/amis)  团队 59 | 60 | 百度开源的前端低代码框架,通过 JSON 配置就能生成各种页面。 61 | 62 | 63 | **试用感受:**通过 JSON 配置就能生成各种后台页面,极大减少开发成本,甚至可以不需要了解前端。在一些只注重数据处理和展示的平台中,少定制化的情况下比较适合试用。 64 | 65 | 66 | ### [n8n](https://github.com/n8n-io/n8n) 团队  67 | 68 | n8n是一个可扩展的工作流自动化工具。通过 [Fair-code](https://faircode.io/) 的代码分发模式,可以自托管,并允许您添加自己的自定义功能代码逻辑和应用程序。n8n 基于节点的方法使它具有高度通用性,使您能够将任何东西连接到所有东西。 69 | 70 | 71 | **试用感受:**可以自己去看个2分钟的视频 [https://www.youtube.com/watch?v=3w7xIMKLVAg](https://www.youtube.com/watch?v=3w7xIMKLVAg) , 这类项目适合结合 DevOps 去实践。 72 | 73 | 74 | ### [ngx-formly](https://github.com/ngx-formly/ngx-formly) 团队 75 | 76 | 77 | 支持多UI组件,Angular 技术栈,团队持续维护 78 | 79 | 80 | 81 | 82 | ## 低代码开源项目(二):拖拽生成页面 83 | 84 | 85 | ### [react-visual-editor](https://github.com/brick-design/react-visual-editor) 独立开发者 86 | 87 | 88 | 组件可视化拖拽,页面搭建,源码生成工具,自由拖拽嵌套,可实现任何真实开发中的复杂页面,所见即所得,可完美还原UI设计,多平台展示支持。 89 | 90 | 91 | **试用感受**:支持手机和PC两种平台,生成源码 react + antd 的源码可直接使用,组件基本满足开发场景,对于没有物料市场、模板工程、脚手架的团队,可以说是一个方便的工具。 92 | 93 | 94 | ### [luban-h5](https://github.com/ly525/luban-h5) 独立开发者 95 | 96 | 97 | 类似易企秀的H5制作、建站工具、可视化搭建系统。 98 | 99 | **试用感受**:H5 页面完善度不错,可以支持多页H5编辑,是一个可以直接使用的开源项目。 100 | 101 | 102 | ### [quark-h5](https://github.com/huangwei9527/quark-h5) 独立开发者 103 | 104 | 105 | 基于vue2 + koa2的 H5制作工具。让不会写代码的人也能轻松快速上手制作H5页面。类似易企秀、百度H5等H5制作、建站工具。 106 | 107 | 108 | **试用感受**:H5页面只做,基本满足要求,可存储模板,组件和细节完善的话是可以使用到业务场景中的一个项目。 109 | 110 | 111 | ### [gods-pen](https://github.com/ymm-tech/gods-pen) 团队 112 | 113 | 114 | 基于vue的高扩展在线网页制作平台,可自定义组件,可添加脚本,可数据统计。 115 | 116 | 117 | **试用感受**:比上边几个同类H5好很多,还有组件商城。 118 | 119 | 120 | ### [sparrow](https://github.com/sparrow-js/sparrow) 独立开发者 121 | 122 | 核心目标仅有一条“提升研发效率”,目前提供基于vue、element-ui组件库中后台项目的实践,实时输出源代码。 123 | 124 | 125 | 感受:后续团队内部的物料平台搭建,可以考虑参考。 126 | 127 | 128 | ## 总结 129 | 130 | 几年前,为了提供团队内的研发效率,自己也研究过类似的小应用。比如基于 json schema 实现表单页面自动生成并产出源代码的学习工程: [ngx-form-builder](https://github.com/giscafer/ngx-form-builder) 131 | 132 | 现在 Low-Code 的流行,越来越多的开源项目可以参考学习。是否能被自己所用,还需要结合公司的产品业务、团队的情况去看,类似的时候,进行开源项目的改造也是可以探索一下研发效能的提升。 133 | 134 | 最近项目要搞一个可视化大屏编辑器,就是那种数据大屏,通过编辑器拖和编辑实现。也因为自己之前学习了解一些Low-Code平台的东西,才能短时间内在技术上可以hold住。短时间=几周实现一个编辑器,疯了吗?我都觉得疯了,项目时间不合理,只能硬着头往前赶。 135 | 136 | 开发不到一周的编辑器雏形: 137 | ![image](https://user-images.githubusercontent.com/8676711/108614267-0e2ec680-7434-11eb-8fe5-f30a3ab4b1ab.png) 138 | 139 | 140 | ## 相关资料 141 | 142 | 143 | - [国内外低代码平台简析](https://zhuanlan.zhihu.com/p/158551880?utm_source=wechat_session&utm_medium=social&s_r=0#showWechatShareTip) 144 | - [awesome-lowcode](https://github.com/taowen/awesome-lowcode) 国内低代码平台从业者交流 (更多好产品可以选择pr到此) 145 | 146 | --- 147 | 148 | 推荐阅读: 149 | 150 | 「可视化搭建系统」——从设计到架构,探索前端领域技术和业务价值 - Lucas HC的文章 - 知乎 https://zhuanlan.zhihu.com/p/164558106 151 | 152 | --- 153 | 本人自动发布于:[https://github.com/giscafer/blog/issues/50](https://github.com/giscafer/blog/issues/50) 154 | -------------------------------------------------------------------------------- /pages/blog/index.tsx: -------------------------------------------------------------------------------- 1 | import { pick } from '@contentlayer/client' 2 | // import Subscribe from 'components/subscribe' 3 | import Input from 'components/input' 4 | // Components 5 | import Page from 'components/page' 6 | import PageHeader from 'components/pageheader' 7 | import PostList from 'components/postlist' 8 | import { Section } from 'components/section' 9 | import Badge from 'components/badge' 10 | // Utils 11 | import * as gtag from 'lib/gtag' 12 | import debounce from 'lodash.debounce' 13 | import { GetStaticProps } from 'next' 14 | import Link from 'next/link' 15 | import { NextSeo } from 'next-seo' 16 | import { useCallback, useEffect, useState } from 'react' 17 | import { Search } from 'react-feather' 18 | import PostListSwitch from 'components/postswitch' 19 | import type { Post } from '.contentlayer/types' 20 | import { allPosts } from '.contentlayer/data' 21 | import styles from './index.module.scss' 22 | 23 | type BlogProps = { 24 | posts: Post[] 25 | tagList: string[] 26 | } 27 | 28 | const Blog = ({ posts, tagList }: BlogProps): JSX.Element => { 29 | const [currentSearch, setCurrentSearch] = useState('') 30 | const [hideCoverMode, setHideCoverMode] = useState(false) 31 | const [showBackToTop, setShowBackToTop] = useState(false) 32 | const trackSearch = useCallback( 33 | debounce((value: string) => gtag.search(value), 500), 34 | [], 35 | ) 36 | const seoTitle = 'Blog | Nicky Lao' 37 | const seoDesc = '关于大前端的技术文章,包含且不限于 React、Angular、React Native 等。' 38 | const filteredPosts = posts 39 | .sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()) 40 | .filter(({ title, summary, tags }) => { 41 | const searchString = `${title.toLowerCase()} ${summary.toLowerCase()} ${tags?.join(' ')}` 42 | return searchString.includes(currentSearch.toLowerCase()) 43 | }) 44 | 45 | const handleInputChange = e => { 46 | const searchString = e.target.value 47 | if (searchString !== '') { 48 | trackSearch(searchString) // Save what people are interested in reading 49 | } 50 | setCurrentSearch(searchString) 51 | } 52 | 53 | useEffect(() => { 54 | const handleScroll = () => { 55 | setShowBackToTop(window.scrollY > 200) 56 | } 57 | window.addEventListener('scroll', handleScroll) 58 | handleScroll() 59 | return () => { 60 | window.removeEventListener('scroll', handleScroll) 61 | } 62 | }, []) 63 | 64 | const handleBackToTop = () => { 65 | window.scrollTo({ top: 0, behavior: 'smooth' }) 66 | } 67 | 68 | return ( 69 | 70 | 83 | 84 |
85 |
86 | 87 | 88 |
89 |
90 | Tags 91 | 92 |
93 | {tagList.sort().map(tag => ( 94 | 95 | 96 | #{tag} 97 | 98 | 99 | ))} 100 |
101 |
102 |
103 |
104 |
105 | { 108 | setHideCoverMode(v) 109 | }} 110 | /> 111 | 112 | 120 |
121 | ) 122 | } 123 | 124 | export const getStaticProps: GetStaticProps = async () => { 125 | const posts = allPosts.map(post => pick(post, ['slug', 'title', 'summary', 'publishedAt', 'image', 'readingTime'])) 126 | const tags = new Set(allPosts.reduce((acc, cur) => acc.concat(cur.tags), [])) 127 | const tagList: string[] = [...tags] 128 | return { 129 | props: { posts, tagList }, 130 | } 131 | } 132 | 133 | export default Blog 134 | -------------------------------------------------------------------------------- /components/themechanger.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, Fragment } from 'react' 2 | import { useTheme } from 'next-themes' 3 | import { motion, AnimateSharedLayout, AnimatePresence } from 'framer-motion' 4 | import styles from './themechanger.module.scss' 5 | 6 | const variants = ['light', 'dark', 'system'] 7 | 8 | const getIcon = variant => { 9 | if (variant === 'light') { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | if (variant === 'dark') { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | const ThemeChanger = (): JSX.Element => { 49 | const [mounted, setMounted] = useState(false) 50 | const [active, setActive] = useState(false) 51 | const [hovered, setHovered] = useState('') 52 | const { setTheme, theme } = useTheme() 53 | 54 | // When mounted on client, now we can show the UI 55 | useEffect(() => setMounted(true), []) 56 | 57 | if (!mounted) return
// skeleton on server 58 | 59 | return ( 60 | 61 |
62 | 63 | 64 | {active && ( 65 | 72 | )} 73 | {variants.map(variant => { 74 | const selected = theme === variant 75 | const isHovered = hovered === variant 76 | return ( 77 | 78 | {(selected || active) && ( 79 | setHovered(variant)} 81 | layout 82 | animate={{ opacity: 1, scale: 1 }} 83 | exit={{ opacity: 0, scale: 0.85 }} 84 | initial={{ opacity: 0, scale: 0.85 }} 85 | type="button" 86 | title={variant} 87 | key={variant} 88 | className={styles.button} 89 | onClick={() => { 90 | if (!active) { 91 | return setActive(true) 92 | } 93 | setActive(false) 94 | return setTheme(variant) 95 | }} 96 | > 97 | {((!active && selected) || isHovered) && ( 98 | 99 | )} 100 | {getIcon(variant)} 101 | 102 | )} 103 | 104 | ) 105 | })} 106 | 107 | 108 |
109 |
110 | ) 111 | } 112 | 113 | export default ThemeChanger 114 | -------------------------------------------------------------------------------- /data/blog/post-39.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 浏览器中 JavaScript 的执行机制 3 | publishedAt: 2020-02-22T05:58:26Z 4 | summary: 查看全文>> 5 | tags: ["JavaScript","Review"] 6 | --- 7 | 8 | # 浏览器中 JavaScript 的执行机制 9 | 10 | - 变量提升 11 | - 调用栈 12 | - 作用域链 13 | - 闭包 14 | - this 15 | 16 | ## 变量提升 17 | 18 | 实际上变量和函数声明在代码里的位置是不会变的,而且是在编译阶段被 JavaScript 引擎放入内存中,一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。大致流程为:**JavaScript 代码片段** ——> **编译阶段** ——> **执行阶段—>**。 19 | 20 | **编译阶段**,每段执行代码会分为两部分,第一部分为变量提升部分的代码,第二部分为执行部分的代码。经过编译后,生成**执行上下文(Execution context)和 可执行代码**。 21 | 22 | ![image](https://user-images.githubusercontent.com/8676711/75086121-8cc7fe00-556b-11ea-81ac-533651bb95ef.png) 23 | 24 | **执行上下文** 是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入函数的执行上下文,从而确定该函数执行期间用到的如 this、变量、对象以及函数等。 25 | 26 | 执行上下文由 **变量环境(Variable Environment)** 和 **词法环境(Lexical Environment)**对象 组成,变量环境保存了代码中变量提升的内容,包括 `var` 定义和 `function` 定义的变量。而词法环境保存 `let` 和 `const` 定义块级作用域的变量。 27 | 28 | ![变量查找过程](https://user-images.githubusercontent.com/8676711/75086161-f516df80-556b-11ea-9393-5273b3c4e634.png) 29 | 30 | 块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了 31 | 32 | 变量查找过程:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。 33 | 34 | 变量声明提升补充: 35 | 36 | - var的创建和初始化被提升,赋值不会被提升。 37 | 38 | - let的创建被提升,初始化和赋值不会被提升。 39 | 40 | - function的创建、初始化和赋值均会被提升。 41 | 42 | 43 | 44 | ## 调用栈 45 | 46 | **调用栈**是用来管理函数调用关系的一种数据结构。在函数调用的时候,JavaScript 引擎会创建函数**执行上下文**,而全局代码下又有一个**全局执行上下文**,这些执行上下文会使用一种叫**栈**的数据结果来管理。 47 | 48 | 所以 JavaScript 的**调用栈**,其实就是 **执行上下文栈** 。举例代码执行,入栈如图所示: 49 | 50 | ```js 51 | var a = 2 52 | function add(b,c){ 53 | return b+c 54 | } 55 | function addAll(b,c){ 56 | var d = 10 57 | result = add(b,c) 58 | return a+result+d 59 | } 60 | addAll(3,6) 61 | ``` 62 | 63 | ![执行 add 函数时的调用栈](https://user-images.githubusercontent.com/8676711/75086295-90f51b00-556d-11ea-9deb-b040031cc405.png) 64 | 65 | 调用栈既然是一种数据结构,所以是存在大小的,超出了栈大小就会出现**栈溢出**报错,比如斐波那契数列,执行10000次,超过了最大栈调用大小(Maximum call stack size exceeded)。 66 | 67 | ```js 68 | function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { 69 | if( n <= 1 ) {return ac2}; 70 | 71 | return Fibonacci2 (n - 1, ac2, ac1 + ac2); 72 | } 73 | Fibonacci2(10000) // Maximum call stack size exceeded 74 | ``` 75 | 76 | 该函数是递归的,虽然只有一种函数调用,但是还是会一直创建执行上下文压入调用栈中,导致超过最大调用栈大小报错,可以通过 Chrome 调式看到 Call Stack 的情况 77 | 78 | ![image](https://user-images.githubusercontent.com/8676711/75086392-a3238900-556e-11ea-8221-bf65055baf4d.png) 79 | 80 | 总结: 81 | 82 | - 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。 83 | - 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。 84 | - 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。 85 | - 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。 86 | 87 | 所以,斐波那契数列函数优化的手段就是使用循环来减少函数调用,从而减少函数执行上下文的创建压入栈的情况,就可以解决栈溢出的报错了。(递归尾部优化无法解决问题,Chrome浏览器还是栈溢出),使用[蹦床函数]([https://es6.ruanyifeng.com/#docs/function#%E5%B0%BE%E8%B0%83%E7%94%A8%E4%BC%98%E5%8C%96](https://es6.ruanyifeng.com/#docs/function#尾调用优化))来解决: 88 | 89 | ```js 90 | function runStack (n) { 91 | if (n === 0) return 100; 92 | return runStack.bind(null, n- 2); // 返回自身的一个版本 93 | } 94 | // 蹦床函数,避免递归 95 | function trampoline(f) { 96 | while (f && f instanceof Function) { 97 | f = f(); 98 | } 99 | return f; 100 | } 101 | trampoline(runStack(1000000)) 102 | ``` 103 | 104 | ![image](https://user-images.githubusercontent.com/8676711/75086880-2f847a80-5574-11ea-8b54-8b0547cdde78.png) 105 | 106 | 可以看到,调用栈中一直是保持3个执行上下文而已,多余的都及时的pop掉了。 107 | 108 | ## 作用域链 109 | 110 | 每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部的引用称为 **outer**。 111 | 112 | 当一段代码使用一个变量是,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,如果找不到就会继续在 outer 所指向的执行上下文中查找。我们把这个查找的链条就称为**作用域链**。 113 | 114 | ![带有外部引用的调用栈示意图](https://user-images.githubusercontent.com/8676711/75087133-9bb4ad80-5577-11ea-8f0e-da90f7547440.png) 115 | 116 | ### 词法作用域 117 | 118 | 词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码阶段决定好的,和函数是怎么调用的没有关系。 119 | 120 | ![词法作用域](https://user-images.githubusercontent.com/8676711/75087205-390fe180-5578-11ea-922e-2ceb4f5c75c2.png) 121 | 122 | ### 块级作用域中的变量查找 123 | 124 | - 从当前执行上下文的词法环境,自顶向下查找(栈中的内存块),然后再从当前执行向下文中的变量环境中查找; 125 | - 查找不到,则继续在**outer**指向的执行上下文继续依次先从词法环境,再到变量环境查找。 126 | 127 | ## 闭包 128 | 129 | 有词法作用域的规则可以知道,内部函数总是可以访问他们的外部函数中的变量,当外部函数执行完毕后,pop stack了,遗留下了外部环境形成的闭包 Closure 环境,该环境内存中还保存着那些可以访问的变量,类似一个专属背包,除了内部函数访问,气氛方式无法访问该专属背包,我们就包这个背包称为外部函数的**闭包**(那些内部函数引用外部函数的变量依然保存在内存中,我们把这些变量的集合称为闭包)。 130 | 131 | ### 闭包是怎么回收的 132 | 133 | 如果引用闭包的函数是一个全局变量,那么闭包会一直存在知道页面关闭;如果这个闭包以后不再使用的话,就会造成内存泄漏。 134 | 135 | 如果引用闭包的函数是一个局部变量,等函数销毁后,下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块的内存。 136 | 137 | 使用闭包的原则:**如果闭包会一直使用,那么他可以作为全局变量而存在;但如果使用频率不高,而且占用内存有比较大的话,那就尽量让它成为一个局部变量。** 138 | 139 | ## this 140 | 141 | ```js 142 | let a = { name: 'this解释' } 143 | function foo() { 144 | console.log(this.name) 145 | } 146 | foo.bind(a)() // => 'this解释'' 147 | ``` 148 | 149 | ![this判断图](https://user-images.githubusercontent.com/8676711/75087424-edab0280-557a-11ea-91d8-d691e8135392.png) 150 | 151 | --- 152 | 153 | _参考资源:《浏览器的工作原理与实践》极客时间-李兵_ 154 | 155 | --- 156 | 本人自动发布于:[https://github.com/giscafer/blog/issues/39](https://github.com/giscafer/blog/issues/39) 157 | --------------------------------------------------------------------------------