HackMD
  • Beta
    Beta  Get a sneak peek of HackMD’s new design
    Turn on the feature preview and give us feedback.
    Go → Got it
      • Create new note
      • Create a note from template
    • Beta  Get a sneak peek of HackMD’s new design
      Beta  Get a sneak peek of HackMD’s new design
      Turn on the feature preview and give us feedback.
      Go → Got it
      • Sharing Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • More (Comment, Invitee)
      • Publishing
        Please check the box to agree to the Community Guidelines.
        Everyone on the web can find and read all notes of this public team.
        After the note is published, everyone on the web can find and read this note.
        See all published notes on profile page.
      • Commenting Enable
        Disabled Forbidden Owners Signed-in users Everyone
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
        • Everyone
      • Invitee
      • No invitee
      • Options
      • Versions and GitHub Sync
      • Transfer ownership
      • Delete this note
      • Template
      • Save as template
      • Insert from template
      • Export
      • Dropbox
      • Google Drive Export to Google Drive
      • Gist
      • Import
      • Dropbox
      • Google Drive Import from Google Drive
      • Gist
      • Clipboard
      • Download
      • Markdown
      • HTML
      • Raw HTML
    Menu Sharing Create Help
    Create Create new note Create a note from template
    Menu
    Options
    Versions and GitHub Sync Transfer ownership Delete this note
    Export
    Dropbox Google Drive Export to Google Drive Gist
    Import
    Dropbox Google Drive Import from Google Drive Gist Clipboard
    Download
    Markdown HTML Raw HTML
    Back
    Sharing
    Sharing Link copied
    /edit
    View mode
    • Edit mode
    • View mode
    • Book mode
    • Slide mode
    Edit mode View mode Book mode Slide mode
    Note Permission
    Read
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    Write
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    More (Comment, Invitee)
    Publishing
    Please check the box to agree to the Community Guidelines.
    Everyone on the web can find and read all notes of this public team.
    After the note is published, everyone on the web can find and read this note.
    See all published notes on profile page.
    More (Comment, Invitee)
    Commenting Enable
    Disabled Forbidden Owners Signed-in users Everyone
    Permission
    Owners
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Invitee
    No invitee
       owned this note    owned this note      
    Published Linked with GitHub
    Like BookmarkBookmarked
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    --- tags: 前端 --- # Webpack打包範例 - 我全包了 ## 寫網頁這麼簡單,為什麼沒事還要打包 不管是用傳統vallina JS,還是最新最潮es6~es10的語法,都有語法編譯、相容性、檔案優化的問題,您可能聽過pug(前身jade), sass, scss, babel, polyfill,這些東西可以讓寫網頁變得簡潔有力、易於快速修改、解決跨瀏覽器的問題,甚至可以避免讓辛苦寫好的code輕易被盜用,然而這些語法要個別去下指令,編譯成可執行的html, css, js檔案,實在是花太多功夫了;所以出現了前端打包工具,從pipeline風格的gulp、架構化的webpack,標榜零設定的parcel或snowpack,同時這個作法也逐漸流行起來,以下介紹目前最廣泛使用的webpack。 ## 啥是webpack ![](https://i.imgur.com/gzG9Gdl.png) 取自 https://webpack.js.org/ ,如上圖所示,我們的目標是要優化雜亂的檔案 過去處理一個個檔案就要一個個的去打指令,gulp的pipe功能讓一個指令可以輪流觸發這些動作;而webpack更是將這些處理模式有個架構,長得像這樣 | entry | module | plugins | output | |-|-|-|-| | 進入點 | 各類型檔案的前置處理 | 額外插件的後製 | 輸出點 | :::info **這個架構看起來夠直覺,讓我們先新增一個資料夾叫webpack-test來試試** ```bash= mkdir webpack-test cd webpack-test npm init -y npm install --save-dev webpack webpack-cli ``` **新增webpack.config.js**,讓webpack知道檔案在哪裡、要怎麼吃以及要吐到哪邊 ```javascript= const path = require('path'); // 用來確保webpack不會吐在奇怪的地方 module.exports = { entry: './src/index.js', // 這個檔案import了所有css和js module: { //放一些正規表達式和模組名稱(記得先安裝) rules: [] }, plugins: { //放一些類別(class) }, output: { // 吐出來的檔案預設在dist path: path.resolve(__dirname, 'dist'), filename: '[name].min.js' // 預設吐出來後把[name]換成main } } ``` **修改package.json的scripts** (要注意新版的webpack不設定mode會跳出警告) ```json= { "name": "webpack-test", "version": "1.0.0", "description": "", "main": "index.js", "devDependencies": { "webpack": "^4.29.6", "webpack-cli": "^3.2.3", }, "scripts": { "build": "webpack --mode production --config webpack.config.js", }, "author": "chuboy", "license": "ISC", } ``` ::: :::info **把專案丟到src資料夾,讓資料夾長這樣** * webpack-test * node_modules/ * src/ * css/ * bootstrap.css * index.html * index.js * package-lock.json * package.json * webpack.config.js ::: :::info **輸入`npm run build`開始打包,然後dist資料夾就變出來了** * webpack-test * dist/ * main.js ::: :::danger 咦怎麼只有吐出main.js,不是說webpack很方便嗎,怎麼感覺有點複雜? ::: :::info 沒關係只是因為我們還沒有加入module和plugins,它是有點小複雜,就讓我們繼續看下去。 ::: ## 可不可以邊寫邊自動打包 :::danger 剛剛如果每次改檔案都要再打一次`npm run build`太麻煩了吧,有沒有像vscode的[LiveServer](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)插件那種好東西,每次按ctrl+S都可以自動更新? ::: :::info 答案是有的,它叫做webpack-live-server **修改package.json** `npm i -D webpack-live-server` ```json= { "devDependencies": { ..., "webpack-dev-server": "^3.2.1" } "scripts": { "start": "webpack-dev-server --mode development --config webpack.config.js", "build": "webpack --mode production --config webpack.config.js", }, } ``` **修改webpack.config.js** ```javascript= module.exports = { ..., devServer: { contentBase: path.join(__dirname, 'dist'), compress: true, port: 9000 } } ``` **終端機** `npm run start` ```bash= i 「wds」: Project is running at http://localhost:9000/ i 「wds」: webpack output is served from / i 「wds」: Content not from webpack is served from C:\Users\ChuBoy\Desktop\webpack-test\dist i 「wdm」: Hash: 8b2111a5fca1e0ccf7b2 ``` ctrl+左鍵點擊http://localhost:9000/ 就可以進入網頁 而HTML處理最複雜,以下分別按照js>css>html說明如何處理 ::: ## JS & Babel > 隨著Javascript的語法快速更新,es6, es7的新潮語法讓寫code更具易讀性,像是寫個類別 ```javascript= class fileHandler{ constructor(inputElement){ this.inputElement = inputElement } async readFile(file){ // async一定得放在function外且回傳Promise const reader = new FileReader() return new Promise((resolve,reject)=>{ reader.onload = ()=>{ resolve(reader.result) } reader.readAsArrayBuffer(file) }) } getBlob(file){ const blob = await this.readFile(file) // 異步執行完才會換下一行 return blob } } ``` :::info 很可惜的是,這些好用的新語法,IE瀏覽器幾乎一概看不懂,因此為了要把語法都編譯成舊版瀏覽器可讀的格式,babel是最為流行的解決方案 **修改webpack.config.js** `npm i -D @babel/core @babel/preset-env babel-loader` ```javascript= module.exports = { ..., module: { rules: [ //webpack會用正規表達式去找這個檔案,/.js$/代表檔名尾端是.js { test: /.js$/, use: ['babel-loader'] } ] } } ``` ::: :::danger 等等,打包出來的東西根本只是複製貼上,沒有變啊 ::: :::info 喔因為剛剛還沒加進babel設定檔,所以要把use改成物件並增加options.presets **修改webpack.config.js** ```javascript= module.exports = { ..., module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: [ ['@babel/plugin-proposal-decorators', { legacy: true }], ] } } }, ] } } ``` 附帶一提,webpack本身不會清掉dist裡面的舊檔案,也許是怕會刪錯東西,因此要加個插件 `npm i -D clean-webpack-plugin` ```javascript= const CleanWebpackPulgin = require('clean-webpack-plugin') module.exports = { ..., plugins: [ new CleanWebpackPlugin(), ] } ``` ::: :::danger 我在IE上測試網頁還是怪怪的耶,有時候甚至一片空白。 ::: :::info 除了語法問題,原生API也不是每個瀏覽器都通用,這時候最強工具Polyfill就要出馬,能讓不支援API的瀏覽器也能正常瀏覽(但功能還是不一定會有)。 `npm i -D @babel/polyfill` ```javascript= module.exports = { entry: ['@babel/polyfill','./src/index.js'], } ``` 再附帶一提,如果想要讓打包後的js不讓別人輕易盜取,可用terser-webpack-plugin(webpack第五版已經內建) `npm i -D terser-webpack-plugin` ```javascript= const TerserPlugin = require('terser-webpack-plugin') module.exports = { plugins: [ new TerserPlugin(), ] } ``` ::: :::danger 如果我的HTML長這樣,要保留這些js檔有解嗎? ```html <body> ... <script src="js/skrollr.js"/> <script src="js/textFx.js"/> <script src="js/chart.js"/> </body> ``` ::: :::info 這種狀況就要改entry的設定,讓webpack一次吐一堆js檔,其中output的[name]值會對應到entry的自定義的命名。 ```javascript= module.exports = { entry: { skrollr: './src/js/skrollr.js', textFx: './src/js/textFx.js', chart: './src/js/chart.js' }, ..., output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } } ``` ::: ## Css/Sass/Scss 新語法有一個我覺得比較不直觀的地方,就是加入css要在js中設定檔案位置,然後從html中移除 `<link href="css/bootstrap.css" type="stylesheet">`,而且需要特定的loader與plugin來編譯,最後webpack才會自動加入`<link>`標籤,導入方式如下 :::info **修改./src/index.js** ```javascript= import 'css/bootstrap.css' import 'css/index.css' import Chart from 'chart.js' ... ``` **修改webpack.config.js** `npm i -D css-loader mini-css-extract-plugin` ```javascript= const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { module: { rules: [ { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader' ] }, ] }, plugins: [ new MiniCssExtractPlugin({filename: '[name].css'}) ] } ``` ::: :::danger 可是瑞凡,我用SASS ::: :::info 這還不簡單 **修改webpack.config.js** `npm i -D node-sass sass-loader` ```javascript= module.exports = { module: { rules: [ { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader','sass-loader' ] }, ] }, } ``` 如果要優化css的話,use的陣列再加一項css的前處理器 `npm i -D postcss-loader autoprefixer cssnano` ```javascript= { loader: 'postcss-loader', options: { plugins: ()=>([ require('autoprefixer'), // 自動添加 -webkit- -moz- -ms- 這類東西 require('cssnano') // css最小化 ]) } ``` ::: ## Html/Pug :::info html因為會包含靜態檔案、css、js,也因此處理的時候會有非常多情況與選擇性。 `npm i -D html-webpack-plugin` ```javascript= const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { ..., plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', //輸入html filename: 'index.html', //打包完的名稱 minify: { collapseBooleanAttributes: true, collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true, removeEmptyAttributes: true, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, minifyCSS: true, minifyJS: true, sortAttributes: true, useShortDoctype: true }, }), ] } ``` ::: :::danger 筆者不是很愛用PUG嗎? ::: :::info `npm i -D html-loader pug-html-loader` ```javascript= module.exports = { module: { rules: [ { test: /\.pug$/, use: ['html-loader','pug-html-loader'] }, ] }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.pug', //改成pug filename: 'index.html', //output name }), ] } ``` ::: :::danger 為什麼我的html中 <input type="file" onchange="handleFile(event.target.files)"/> handleFile這個變數會不見 ::: :::info 因為打包後會重新更改變數名稱,很可惜沒有一個套件可以自動解決,其中一個用硬派解法是在entry的js中將函式定義為window的全域變數。 ```javascript= function handleFile(){...} // 將其定義為window的函數 window.handleFile = handleFile ``` ::: :::danger 那靜態檔案怎麼處理? ::: :::info 可以設定資料夾位置,將同一類型的靜態檔案通通丟在一起 `npm i -D file-loader` ```javascript= module.exports = { module: { rules: [ { test: /\.(png|svg|jpg|gif)$/, use: { loader:'file-loader', options: { name: '[name].[ext]', outputPath: './assets/', publicPath: './assets/' } } }, ] }, } ``` ::: ## 完整範例 **package.json** ```json= { "name": "webpack-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "yarn && webpack serve --mode development --config webpack.config.js --progress", "build": "yarn && webpack --mode production --config webpack.config.js --progress", }, "dependencies": { "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0" }, "devDependencies": { "@babel/core": "^7.0.0-0", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/preset-env": "^7.13.5", "@babel/preset-react": "^7.12.13", "babel-loader": "^8.2.2", "babel-plugin-transform-react-pug": "^7.0.1", "core-js": "^3.12.0", "css-loader": "^5.2.1", "cssnano": "^4.1.10", "dotenv": "^8.2.0", "file-loader": "^6.2.0", "glob": "^7.1.6", "html-webpack-plugin": "^5.3.1", "process": "^0.11.10", "regenerator-runtime": "^0.13.7", "style-loader": "^2.0.0", "webpack": "^5.33.2", "webpack-cli": "^4.6.0", "webpack-dev-server": "^3.11.2" } "author": "chuboy", "license": "ISC", } ``` **webpack.config.js** ```javascript= const fs = require('fs'); const path = require('path'); const glob = require('glob').sync; require('dotenv').config(); const webpack = require('webpack'); const ESLintPlugin = require('eslint-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; class PostBuildPlugin { // 後處理插件 constructor(fn) { this.handler = fn; } apply(compiler) { if (compiler.hooks) compiler.hooks.done.tap('webpack-arbitrary-code', this.handler); else compiler.plugin('done', this.handler); } } // webpack設定 module.exports = function (env, argv = {}) { // 取得.env設定 const mode = argv.mode || 'development'; const BASE_URL = process.env.BASE_URL || '/DSICWEBAP'; const PUBLIC_URL = process.env.PUBLIC_URL || '/resources/react'; const WEBAP_PATH = process.env.WEBAP_PATH || path.join(__dirname, 'build'); const WEBPACK_PORT = process.env.WEBPACK_PORT || 9000; const WEBAP_URL = process.env.WEBAP_URL || 'http://localhost:8080'; const ANALYZE = process.env.ANALYZE || false; // 靜態檔位置,尾端要加個斜線,否則js路徑會少個斜線 const PUBLIC_PATH = mode == 'production' ? BASE_URL + PUBLIC_URL + '/' : ''; const moveFile = (dir) => ({ loader: 'file-loader', options: { name: '[name].[ext]', outputPath: `./${dir}/`, publicPath: `${PUBLIC_PATH}${dir}/`, }, }); return { // 使用polyfill才能支援async entry: ['./src/index.js'], // webpack5必須加這行才能編譯成es2015,但target必須是web才會在開發時自動刷新 target: mode == 'production' ? ['web', 'es5'] : 'web', module: { rules: [ { test: /\.js$/, exclude: /node_modules/, // 所有的起始點babel-loader use: [ { loader: 'babel-loader', options: { // JS編譯設定 presets: [['@babel/preset-env', { targets: { ie: '11' }, useBuiltIns: 'usage', corejs: 3 }], ['@babel/preset-react']], // JS轉換插件(二維陣列表示) plugins: [ ['@babel/plugin-proposal-decorators', { legacy: true }], ['@babel/plugin-proposal-class-properties'], ['babel-plugin-transform-react-pug'], ], }, }, ], }, { test: /\.(sa|sc|c)ss$/, // 必須透過style-loader才會引入html裡 use: ['style-loader', 'css-loader'], }, { test: /\.(png|svg|jpg|gif|ico)$/, use: moveFile('assets'), }, { test: /\.(eot|woff|woff2|[ot]tf)$/, use: moveFile('fonts'), }, ], }, plugins: [ // 使src裡面的東西也能吃process.env,注意這邊要加JSON.stringify,不然編譯出來的字串會沒有引號 new webpack.DefinePlugin({ 'process.env.BASE_URL': JSON.stringify(BASE_URL), 'process.env.NODE_ENV': JSON.stringify(mode), }), // 需要「process」套件,Webpack5強制網頁必須加這行 new webpack.ProvidePlugin({ process: 'process/browser' }), // Webpack只編譯js檔,包裝index.js的html得另外使用套件來設定 new HtmlWebpackPlugin({ favicon: './src/assets/favicon.ico', template: './src/index.html', filename: 'index.html', minify: { removeComments: true, minifyCss: true }, }), // Linting工具 // new ESLintPlugin({ extensions: ['js', 'jsx', 'ts', 'tsx'] }), // 後處理,從[PUBLIC_URL]搬到WEB-INF資料夾裡 new PostBuildPlugin(() => { if (mode == 'production') { // 移動index.html let from = path.join(WEBAP_PATH, PUBLIC_URL, 'index.html'); let to = path.join(WEBAP_PATH, 'WEB-INF', 'index.html'); if (!fs.existsSync(path.dirname(to))) fs.mkdirSync(path.dirname(to)); fs.copyFileSync(from, to); console.log(`搬檔 ${from} -> ${to}`); // 移除LICENSE.txt let license = glob(path.join(WEBAP_PATH, PUBLIC_URL, '**/*.LICENSE.txt')); for (let txt of license) fs.unlinkSync(txt); // rmSync在node.js 14版才被加入 } }), // 靜態檔大小分析套件 new BundleAnalyzerPlugin({ analyzerMode: ANALYZE ? 'server' : 'disabled', openAnalyzer: mode == 'production', }), ], resolve: { // 避免crpyto-js跳出警告 fallback: { crypto: false }, // 將import的絕對路徑轉換為相對路徑,如import * from 'store/app'->./src/store/app.js alias: Object.fromEntries( fs .readdirSync('src') .filter((dir) => !dir.includes('.')) .map((dir) => [dir, path.resolve(__dirname, 'src', dir)]) ), }, output: { // 打包後存放的位置 path: path.join(WEBAP_PATH, PUBLIC_URL), // 伺服器裡客製靜態檔的路由 publicPath: mode == 'production' ? PUBLIC_PATH : '', // JS主檔案名稱,若設[name].[ext]會變成main.js filename: 'js/[name].js', }, devtool: 'inline-source-map', // 避免瀏覽器跳出檔案過大的黃底警告 devServer: { // API重新導向,避開跨域請求CORS的限制 proxy: { [BASE_URL + '/api']: { target: WEBAP_URL, secure: false, changeOrigin: true }, [BASE_URL + '/file']: { target: WEBAP_URL, secure: false, changeOrigin: true }, [BASE_URL + '/oet']: { target: 'http://localhost:3000', secure: false, changeOrigin: true }, }, // 開發站台自動導向導回首頁(否則會出現404) historyApiFallback: { index: BASE_URL }, // 允許所以傳輸的資源用gzip進行壓縮 compress: true, // 自動開啟網頁 open: true, // 開發站台port port: WEBPACK_PORT, }, }; }; ``` P.S.感謝[Magic Len大大的好文](https://magiclen.org/webpack/),讓我的學習之路有解。

    Import from clipboard

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lost their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template is not available.


    Upgrade

    All
    • All
    • Team
    No template found.

    Create custom template


    Upgrade

    Delete template

    Do you really want to delete this template?

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Tutorials

    Book Mode Tutorial

    Slide Mode Tutorial

    YAML Metadata

    Contacts

    Facebook

    Twitter

    Feedback

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions

    Versions and GitHub Sync

    Sign in to link this note to GitHub Learn more
    This note is not linked with GitHub Learn more
     
    Add badge Pull Push GitHub Link Settings
    Upgrade now

    Version named by    

    More Less
    • Edit
    • Delete

    Note content is identical to the latest version.
    Compare with
      Choose a version
      No search result
      Version not found

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub

        Please sign in to GitHub and install the HackMD app on your GitHub repo. Learn more

         Sign in to GitHub

        HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Available push count

        Upgrade

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Upgrade

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully