---
# System prepended metadata

title: 前端初期架構規劃
tags: [工作上的點點滴滴]

---

---
tags: 工作上的點點滴滴
---

# 前端初期架構規劃

> 這邊想來紀錄一下自己第一次規劃架構的經驗當作一個里程碑。在專案規劃的前期有很多需要考量的地方，但礙於自己沒有真的規劃過一個大型專案，所以只能先參考大神們的文章來思考哪些東西是有加入的必要。

### 專案構思

思路流程：

1. 是否需要 SEO
2. 打包效率
3. 編碼檢查工具、自動編排工具
4. CSS 框架與 UI 框架的選擇
5. 有沒有需要將 Sass 模組化

### 專案所需功能

- i18 多國語系切換 => [後續優化](https://hackmd.io/kC-5K27NSrWTu00SY427rQ)
- 提升打包效率 - Vite
- coding style 檢查工具(<span class="red">Eslint</span>)、自動編排工具(<span class="red">prettier</span>)
- 路由控制器
- Vuex 作為狀態管理及應用程序中所有組件的集中存儲區 => 這邊後續優化更改成使用 Pinia 套件，因為 Vuex 在使用變數內容時要打的 code 太多了(<span class="red">store.state.isLoading</span>, 很長一串ＸＤ)
- API 路徑 module
- UI 框架 - <span class="red">Element^+^</span>
- CSS 框架 - <span class="red">Tailwind</span>
- 單元測試套件 - Jest ([踩坑心得分享](https://hackmd.io/uHyz0jaMTXaJT19LwOBrxw))
- Sass 模組化
- mock API
- 全域的錯誤處理([middleware 異常狀態處理](https://hackmd.io/UCtXtiRfQJOfjA0Sx1K6TA))

### Vite + Vue3

[Vite 官方中文文檔](https://cn.vitejs.dev/guide/#scaffolding-your-first-vite-project)

安裝指令:
```
npm create vite@latest my-vue-app -- --template vue
```

### eslint + prettier 配置

[一篇文章搞定vite+vue3+eslint+prettier](https://juejin.cn/post/7020653715363217445)
[eslint 和 prettier 配置文件不能是 .js 文件，必须是.cjs 文件怎么回事 ？](https://segmentfault.com/q/1010000042298464)
:::warning
上述的配置問題因為 vite 新版在 package.json 裡面會配置 type: "module", 於是 .js 被默認為使用了 ES module 規範，如果自動生成的配置文件使用了 CommonJS，就會報錯。.cjs 的檔案會告诉 node.js 它使用了 CommonJS 規範，所以就不會報錯。
:::
:::danger
解決方法：
1. 檔案名稱更改為 *.cjs
2. 必須使用 module.exports 來匯出設定檔
3. 必須至擴充設置那邊將檔案名稱一併調整為 *.cjs, 不然會一直報「找不到檔案」的錯誤
:::
```javascript=
// .prettierrc 檔案
{
  /* 
   * 這邊將 semi 改為 true，eslint 會跳 warning。
   * 後續將 Vscode 的 Vetur 關掉之後，改用 Volar 擴充元件就沒問題
  */
  "semi": true, 
  "singleQuote": true,
  "endOfLine": "auto",
  "printWidth": 100
}
```

### Router

[使用Vite构建Vue3项目，对路由Vue Router 4.x的设置](https://blog.csdn.net/xjtarzan/article/details/119736309)

:::warning
在這篇文章中，讓我覺得有趣的地方是**步驟4的 alias 設置**，讓我了解了以往我們為什麼用 @ 這個符號，它會直接編譯為 src 資料夾的路徑。
原來是因為有特別針對這個符號去做一些路徑的處理
:::

[Router 集成設置參考](https://www.cnblogs.com/liuqin-always/p/14690044.html)


透過這篇文章了解到如何將路由模組化，範例如下：
:::spoiler 路由模組化代碼
```javascript=
// modules/home.js
export default {
  path: '/',
  name: 'index',
  title: '首頁',
  component: () => import('@/views/HomeView.vue'),
};
```
```javascript=
// routes.js
import home from './modules/home';

const routes = [home];

export default routes;
```
```javascript=
// index.js
import { createRouter, createWebHashHistory } from 'vue-router';

import routes from './routes';

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});
// 這邊的想法是：
// 1. 有 token 的情況下，只要路由想要到 login 都會被導回首頁。 
// 2. 沒有 token 的情況下，只要不是 login 路由，都會自動導回到 login
router.beforeEach((to, from) => {
  const access = Cookies.get('token');
  if (access && to.path === '/login') {
    return { name: 'HomeView' };
  } else if (!access && to.path !== '/login') {
    return { name: 'login' };
  }
});

export default router;

```
:::

#### 步驟流程：
1. 在 modules 裡面定義各頁面路由
2. 接著透過 routes.js 導入
3. 最後由 index.js 去建立一個 Router 實體，並把它匯出
4. 方便於 main.js 去引用。

![](https://i.imgur.com/L9rrm7b.png)

[路由的後續優化](https://hackmd.io/XGnLB8CfTwi8UAMRvJOPsg)

### Tailwind CSS
[Install Tailwind CSS with Vue 3 and Vite](https://tailwindcss.com/docs/guides/vite)

### Vuex
[Vue3 的資料狀態管理，provide / inject、vuex、props](https://penueling.com/%E6%8A%80%E8%A1%93%E7%AD%86%E8%A8%98/vue3-%E7%9A%84%E8%B3%87%E6%96%99%E7%8B%80%E6%85%8B%E7%AE%A1%E7%90%86%EF%BC%8Cprovide-inject%E3%80%81vuex/) => 參考 Vuex 集成設置

:::spoiler Vuex 相關代碼
```javascript=
// modules/auth.js
export const state = {
  user: JSON.parse(sessionStorage.getItem('user')),
  password: '123',
};
export const actions = {};

export const mutations = {
  setUser(state, payload) {
    sessionStorage.setItem('user', JSON.stringify(payload));
    state.user = payload;
  },
};
export const getters = {
  isAuthenticated: (state) => !!state.user || !!sessionStorage.getItem('user'),
};

export default {
  state,
  actions,
  mutations,
  getters,
  namespaced: true,
};

```
```javascript=
import { createStore } from 'vuex';
import auth from './modules/auth';

// Create a new store instance.
export default createStore({
  modules: { auth },
});
```
:::

![](https://i.imgur.com/9CLprIF.png)


### axios 集成設置
- [從 0 開始手把手帶你搭建一套規範的 Vue3 項目工程環境](https://www.readfog.com/a/1635089631515086848)

==這裡只有先寫一些註解讓大家清楚每個區塊所負責的功能是什麼，這樣未來如果要擴充或是新增功能的話，比較知道在哪個地方添加。==
```javascript=
// api/axios.js
import Axios from 'axios';
import { ElMessage, ElLoading } from 'element-plus';

const baseURL = 'https://desolate-gorge-38580.herokuapp.com';

const service = Axios.create({
  baseURL,
  timeout: 20000, // 請求超時 20s
});

service.interceptors.request.use(
  config => {
    // 發送請求前會做的事情
    // 這邊目前想到2件事可以先做： 
    // 1. 加入全域的 loading 效果在每次發請求前
    // 2. 將 token 帶入 config.headers.Authorization 裏面
    const loadingInstance = ElLoading.service({
      lock: true,
      text: 'Loading',
      background: 'rgba(0, 0, 0, 0.7)'
    });
    const token = Cookies.get('token');
    if (token) {
      config.headers.common.Authorization = `Bearer ${token}`;
      loadingInstance.close();
    }
    return config;
  },
  (error) => {
    // 請求錯誤要做的事情
    return Promise.reject(error);
  }
);

// 後置攔截器（獲取到響應時的攔截）
service.interceptors.response.use(
  // 狀態碼為 2xx 時觸發的 function
  (response) => {
    return response;
  },
  // 狀態碼非 2xx 時觸發的 function
  (error) => {
    if (error.response && error.response.data) {
      const code = error.response.status;
      const msg = error.response.data.message;
      ElMessage.error(`Code: ${code}, Message: ${msg}`);
      console.error(`[Axios Error]`, error.response);
    } else {
      ElMessage.error(`${error}`);
    }
    return Promise.reject(error);
  }
);

export default service;
```

- [Vue3.x中集成axios的方法](https://shawnzhou.world/2021/07/28/vue3-axios/)

```javascript=
// api/home.js
import service from './axios';

export function successTest() {
  return service({
    url: '/posts',
    method: 'get',
  });
}

export function errorTest() {
  return service({
    url: '/rooms',
    method: 'get',
  });
}

```


```javascript=
// 這邊在 Vite 的文件配置失效，所以後續在各個 .vue 檔案引入 api 時
// 就直接沿用前面 Vite.config 裡面所設置的 alias
proxy: {
  "/desktop": {
    target: "http://你的ip:你的端口/",
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/api/, ""),
  },
},
```
```javascript=
// views/HomeView.vue
import { successTest, errorTest } from '@/api/home';
```
![](https://i.imgur.com/YY3DPSp.png)


### i18n
[i18n什麼的交給前端來處理吧](https://ithelp.ithome.com.tw/articles/10262689)

[將 i18n Global](https://blog.flycode.com/step-by-step-how-to-create-a-vue-multi-language-app-with-vue-i18n)

```javascript=
export default createI18n({
  legacy: false,
  globalInjection: true,
  locale: process.env.VUE_APP_I18N_LOCALE || "en",
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en",
  messages: loadLocaleMessages(),
});
```
> 需要添加 globalInjection: true，然後系統會自動將其全域化，透過 $t 就可以獲得內容。

```htmlmixed=
<!-- template 獲取 i18n value 方式 -->
<template>
  <h1>{{ $t('title') }}</h1>
  <p>{{ $t('description') }}</p>
</template>
```

==這邊紀錄一下，後續可能會需要處理關於切換語系後，畫面需要 reload 的部分==

### 最終資料夾內容呈現樣貌
![](https://i.imgur.com/fuS7z6Q.png)


<style>
    .red{
        color: red
    }
</style>